Keywords: modules, tooling, testing, CLI
อ่านแบบคน Python:
- ถ้าอยากเอา “ภาพรวม” ก่อน: มอง
lib= core logic,bin/main= I/O + wiring - ถ้าอยาก “ลงมือทำ”: ลองแยก 1 ฟังก์ชันออกจาก
mainไปไว้ในlibแล้วเขียน test สั้น ๆ - ถ้าติด: เปิด 12-learning-playbook.md
ไฟล์ Python แบบ monolith ขนาดใหญ่เป็นเรื่องที่เจอบ่อยในงาน scripting และโปรเจกต์ที่โตขึ้นเรื่อย ๆ
ปัญหาที่มักตามมา (คุ้น ๆ สำหรับคนทำ Python):
- import วนกัน (circular import) เมื่อไฟล์เริ่มแยกแบบไม่เป็นระบบ
- business logic ปนกับ I/O / printing / parsing จน test ยาก
- เปลี่ยนโครงสร้างทีหลังแล้วกระทบหลายจุด
Rust ทำให้จัดโครงสร้างได้เป็นระบบตั้งแต่แรก เพราะมันมี “ทางมาตรฐาน” ที่ชัด:
- binary crate (โปรแกรมรันได้) เริ่มที่
src/main.rs - library crate (โค้ด reusable/testable) เริ่มที่
src/lib.rs - แยก module เป็นไฟล์/โฟลเดอร์ แล้วให้ compiler ช่วย enforce ขอบเขต
หัวใจของบทนี้ (แบบ practical):
- แยก “core logic” ออกจาก “I/O/CLI” ให้เร็ว
- ทำให้โค้ดส่วนที่ test ยาก (I/O) เล็กที่สุด
- ทำให้ borrow/ownership ปวดหัวน้อยลงด้วยการ “ส่งข้อมูลผ่าน function boundaries” แบบชัด
สิ่งที่คุณจะเห็นแทบทุกโปรเจกต์:
Cargo.toml— metadata + dependenciessrc/main.rs— binary entrysrc/lib.rs— library entry (ถ้าทำเป็น lib)
เทียบ Python (เพื่อให้จับภาพได้ทันที):
Cargo.tomlคล้ายpyproject.toml/requirements.txtแต่รวม metadata + feature flags + dependency graphsrc/main.rsคล้ายจุดเริ่มmain.py
ทิป: โปรเจกต์จริงจำนวนมากแยกเป็น “lib + bin”:
- bin: parse args / wiring / I/O
- lib: core logic ที่ test ได้
เหตุผลที่สิ่งนี้ช่วยคนมาจาก Python มาก:
- คุณจะเลิก “ผูกทุกอย่างไว้กับ sys.argv + print” แล้วมีจุดเชื่อมที่ชัด
- เวลาเจอ borrow checker คุณจะแก้ด้วยการทำ boundary ให้สั้นลง/ชัดขึ้นได้ง่าย
Python (แนวคิดเทียบ):
- จากไฟล์เดียว → ค่อย ๆ แยกเป็นหลายไฟล์ เช่น
state.py,config.py - แล้ว import ใช้งาน
ตัวอย่าง (Python):
# main.py
from state import AppState
def main() -> None:
state = AppState(counter=0)
print(state)
if __name__ == "__main__":
main()Output (example):
AppState(counter=0)
# state.py
from dataclasses import dataclass
@dataclass
class AppState:
counter: intOutput (example):
(no output — this snippet defines a class)
Rust:
# example layout
src/main.rs
src/state.rs
src/config.rs
main.rs:
mod state;
mod config;
fn main() {
println!("ok");
}Output (example):
ok
state.rs:
pub struct AppState {
pub counter: u64,
}Output (example):
(no output — this snippet defines a struct)
- ไม่ใส่
pub→ คนข้างนอก module ใช้ไม่ได้ - ใส่
pub→ เปิดให้ module อื่นเรียก/เข้าถึงได้
มุมคิดแบบ Python:
- เหมือนคุณเลือก API ที่จะ export ออกไป และซ่อนรายละเอียดภายใน
ข้อควรระวังแบบ practical:
- ถ้าคุณใส่
pubทุกอย่างตั้งแต่แรก โค้ดจะกลายเป็น “ทุกคนเข้าถึงได้หมด” แล้วคุม invariant ยาก - ถ้าคุณเริ่มจาก
pubน้อย ๆ แล้วค่อยเปิดทีละจุด จะ maintain ง่ายกว่า
model/state: struct/enum ที่แทนข้อมูลio/cli: อ่าน args/อ่านไฟล์/พิมพ์ผล
เหตุผล:
- test ง่าย (ทดสอบ model + functions ที่รับ/คืนค่า)
- ลด coupling (เปลี่ยนวิธีอ่านไฟล์/พิมพ์ผล ไม่กระทบ core)
แนวคิดที่ช่วยดูแลง่าย:
- core logic ควรบอก “เหตุการณ์/ผลลัพธ์” แบบเป็นข้อมูล
- ส่วน formatting/file sink เป็นเรื่องของ boundary (ดูบท
08-logging-design.md)
src/main.rsทำแค่ parse + callsrc/lib.rs(หรือ modules ในนั้น) ทำ logic จริง
สิ่งนี้ช่วยลดปัญหา borrow checker เพราะ boundary ชัด: input → owned types → logic
- สร้างโปรเจกต์ใหม่ แล้วแยก
state.rsออกมา - ทำ
config.rsที่มีฟังก์ชันload_config()คืนResult
ถ้าอยากทำให้เหมือนงานจริงมากขึ้น:
- ให้
load_config()แยกเป็นparse_config_str(&str)กับload_config_file(&Path) - แล้วเขียน unit test ที่ test เฉพาะ
parse_config_str(ไม่ต้องอ่านไฟล์จริง)