Skip to content

Latest commit

 

History

History
173 lines (125 loc) · 7.38 KB

File metadata and controls

173 lines (125 loc) · 7.38 KB

05 — Modules & Project Structure (จาก monolith → mod/crate)

TOC · Prev · Next

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” แบบชัด

1) โครงสร้างพื้นฐานของโปรเจกต์ Rust

สิ่งที่คุณจะเห็นแทบทุกโปรเจกต์:

  • Cargo.toml — metadata + dependencies
  • src/main.rs — binary entry
  • src/lib.rs — library entry (ถ้าทำเป็น lib)

เทียบ Python (เพื่อให้จับภาพได้ทันที):

  • Cargo.toml คล้าย pyproject.toml/requirements.txt แต่รวม metadata + feature flags + dependency graph
  • src/main.rs คล้ายจุดเริ่ม main.py

ทิป: โปรเจกต์จริงจำนวนมากแยกเป็น “lib + bin”:

  • bin: parse args / wiring / I/O
  • lib: core logic ที่ test ได้

เหตุผลที่สิ่งนี้ช่วยคนมาจาก Python มาก:

  • คุณจะเลิก “ผูกทุกอย่างไว้กับ sys.argv + print” แล้วมีจุดเชื่อมที่ชัด
  • เวลาเจอ borrow checker คุณจะแก้ด้วยการทำ boundary ให้สั้นลง/ชัดขึ้นได้ง่าย

2) แยกโค้ดด้วย mod

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: int

Output (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)

2.1 pub คือ “ขอบเขตการมองเห็น”

  • ไม่ใส่ pub → คนข้างนอก module ใช้ไม่ได้
  • ใส่ pub → เปิดให้ module อื่นเรียก/เข้าถึงได้

มุมคิดแบบ Python:

  • เหมือนคุณเลือก API ที่จะ export ออกไป และซ่อนรายละเอียดภายใน

ข้อควรระวังแบบ practical:

  • ถ้าคุณใส่ pub ทุกอย่างตั้งแต่แรก โค้ดจะกลายเป็น “ทุกคนเข้าถึงได้หมด” แล้วคุม invariant ยาก
  • ถ้าคุณเริ่มจาก pub น้อย ๆ แล้วค่อยเปิดทีละจุด จะ maintain ง่ายกว่า

3) Guideline สำหรับคนย้ายจาก Python

3.1 แยก “data model” ออกจาก “I/O”

  • model/state: struct/enum ที่แทนข้อมูล
  • io/cli: อ่าน args/อ่านไฟล์/พิมพ์ผล

เหตุผล:

  • test ง่าย (ทดสอบ model + functions ที่รับ/คืนค่า)
  • ลด coupling (เปลี่ยนวิธีอ่านไฟล์/พิมพ์ผล ไม่กระทบ core)

3.2 แยก “business logic” ออกจาก “format/logging”

แนวคิดที่ช่วยดูแลง่าย:

  • core logic ควรบอก “เหตุการณ์/ผลลัพธ์” แบบเป็นข้อมูล
  • ส่วน formatting/file sink เป็นเรื่องของ boundary (ดูบท 08-logging-design.md)

3.3 ถ้าเริ่มไม่แน่ใจ ให้เริ่มจาก 2 ชั้น

  • src/main.rs ทำแค่ parse + call
  • src/lib.rs (หรือ modules ในนั้น) ทำ logic จริง

สิ่งนี้ช่วยลดปัญหา borrow checker เพราะ boundary ชัด: input → owned types → logic


4) แบบฝึกหัด

  1. สร้างโปรเจกต์ใหม่ แล้วแยก state.rs ออกมา
  2. ทำ config.rs ที่มีฟังก์ชัน load_config() คืน Result

ถ้าอยากทำให้เหมือนงานจริงมากขึ้น:

  • ให้ load_config() แยกเป็น parse_config_str(&str) กับ load_config_file(&Path)
  • แล้วเขียน unit test ที่ test เฉพาะ parse_config_str (ไม่ต้องอ่านไฟล์จริง)