Skip to content

Latest commit

 

History

History
333 lines (225 loc) · 14.2 KB

File metadata and controls

333 lines (225 loc) · 14.2 KB

11 — Mini Projects (ฝึกแบบปลอดภัย)

TOC · Prev · Next

Keywords: mini projects, tooling, error handling, testing

อ่านแบบคน Python:

  • ถ้าอยากเอา “ภาพรวม” ก่อน: หนึ่งโปรเจกต์ = หนึ่ง crate เพื่อให้ทดลองได้เต็มที่
  • ถ้าอยาก “ลงมือทำ”: ทำให้ compile ผ่านก่อน แล้วค่อยเพิ่ม test ทีละเคส
  • ถ้าติด: เปิด 12-learning-playbook.md

บทนี้คือชุด mini-projects สำหรับฝึก Rust แบบค่อยเป็นค่อยไป โดยตั้งใจให้ ปลอดภัย และ โฟกัสแก่นภาษา:

  • ทำงานแบบ local/offline
  • ไม่แตะ network/proxy
  • ไม่ควบคุมระบบ/โปรเซส
  • ไม่ยุ่งกับข้อมูลลับ (token/key/credential)

เป้าหมายไม่ใช่ “ทำของให้ใหญ่” แต่คือ “ฝึกโครงสร้างที่ถูก” ให้เป็นนิสัย: parse → validate → run → test


TL;DR (วิธีใช้บทนี้ให้คุ้ม)

  • หนึ่งโปรเจกต์ = หนึ่ง crate (cargo new ...) เพื่อแยกสนามทดลอง
  • ทำงานเป็นวงจร: cargo check → แก้ทีละจุด → เติม tests → cargo fmt/cargo clippy
  • ฝึก 4 แกนหลัก:
    1. error path (ทำให้พังแล้วบอกชัด)
    2. owned vs borrowed (รับเป็น &str แต่เก็บเป็น String)
    3. enum/match (บังคับให้เคสครบ)
    4. tests (พิสูจน์พฤติกรรมแทนการเดา)

ก่อนเริ่ม: กติกาที่ทำให้เรียนไว (ไม่หลงทาง)

1) ทำให้ปัญหา “เล็ก” ก่อน

  • อย่าเริ่มจาก feature ครบทุกอย่าง
  • เริ่มจากเวอร์ชันที่รันได้ แล้วค่อย tighten ทีละชั้น
  • ถ้าเริ่มรู้สึกว่าโค้ดเริ่มมี if/else ยาว ๆ ให้หยุดแล้วหา “type ที่ควรมี” (เช่น enum Command, struct Config)

2) แก้ทีละ error (อย่ากวาดทีเดียว)

Rust compiler ให้ข้อมูลเยอะมาก ถ้าคุณแก้ทีละจุด จะเห็น pattern เร็ว

  • อย่า refactor + เปลี่ยน API + เพิ่ม feature พร้อมกัน
  • ถ้าจะแก้เรื่อง ownership/borrow ให้แก้จน cargo check ผ่านก่อน แล้วค่อยเพิ่ม feature

3) Clone แบบมีเหตุผล (สำคัญมากสำหรับคนมาจาก Python)

แนวที่ปลอดภัยและชัด:

  • clone ที่ boundary:
    • ตอนรับ input แล้วต้องเก็บลง state (&strString)
    • ตอนต้องปล่อย borrow ให้สั้นลง (เพื่อไม่ชน borrow checker)

สิ่งที่ควรระวัง:

  • ถ้าเห็น clone() โผล่หลายจุดเพื่อ “ให้คอมไพล์ผ่าน” ให้หยุดและทบทวน ownership model

4) Done criteria (นิยามว่า “จบแล้ว”)

ถือว่าจบโปรเจกต์หนึ่งเมื่อ:

  • cargo check ผ่าน
  • มี cargo test อย่างน้อย 3 เคส (happy path + edge case + invalid)
  • boundary ชัด: parse / validate / run แยกกันพอสมควร
  • output ไม่ dump ทั้งโลก: พิมพ์เฉพาะ summary ที่คนใช้ต้องการ

Template โครงโปรเจกต์ (แนะนำ)

เริ่มแบบนี้ก่อน (พอทำเสร็จค่อยขยาย):

src/
  main.rs      # รับ args + I/O + print output
  lib.rs       # (optional) เอา logic ออกมาเพื่อให้ test ง่าย

แนวคิด:

  • main.rs = boundary (args/file/stdout)
  • lib.rs = pure-ish logic (parse/validate/run) ที่ทดสอบง่าย

Output (example):

(no output — structure suggestion)

ถ้าคุณทำ test แล้วรู้สึกว่า “ต้องเตรียม args/file เยอะ” นั่นคือสัญญาณว่า logic ควรถูกย้ายไป lib.rs


Project A: Config Loader (อ่าน JSON + validate + สรุปผล)

เป้าหมาย

  • รับ path ผ่าน CLI: app config path/to/config.json
  • อ่านไฟล์ (I/O)
  • parse JSON เป็น struct (schema)
  • validate semantics ที่ serde จับไม่ได้ (เช่นห้ามว่าง/ห้ามซ้ำ)
  • แสดงสรุป config แบบสั้น ๆ (ไม่ dump ทั้งไฟล์)

Focus ที่ควรฝึก

  • Result + ? + การออกแบบ error message
  • แยก “parse” ออกจาก “validate” (ทำให้ test ง่าย)
  • owned/borrowed: input เป็น &str แต่ state เก็บเป็น String

ข้อจำกัดด้านความปลอดภัย (ตั้งใจให้ปลอดภัย)

  • อ่านไฟล์ local เท่านั้น (ไม่รับ URL)
  • หลีกเลี่ยงการพิมพ์ secrets (ถ้ามี field แนว token, key ให้แสดงเป็น "(redacted)" หรือไม่แสดง)
  • ไม่ทำ network calls

สเปกขั้นต่ำของ Config (ตัวอย่าง)

  • app_name: String (ห้ามว่าง)
  • port: u16 (ควรอยู่ในช่วง 1..=65535)
  • log_level: String หรือ enum LogLevel (ถ้าพร้อม)
  • features: Vec<String> (ห้ามซ้ำ)

ตัวอย่าง JSON สำหรับลอง (ปลอดภัย)

{
  "app_name": "demo",
  "port": 8080,
  "log_level": "info",
  "features": ["pretty_print", "strict_validation"]
}

API ที่แนะนำ (เพื่อให้ย้ายไป test ได้ง่าย)

use std::path::Path;

fn parse_config_str(input: &str) -> Result<Config, ConfigError> { /* ... */ }
fn validate_config(cfg: &Config) -> Result<(), ConfigError> { /* ... */ }
fn load_config_file(path: &Path) -> Result<Config, ConfigError> { /* ... */ }

Output (example):

(no output — function signatures only)

Step-by-step (ทำทีละชั้น)

  1. ทำ I/O ให้จบก่อน
  • อ่านไฟล์เป็น String
  • ถ้าอ่านไม่ได้ ให้ error มี context ว่าอ่านไฟล์ไหน
  1. ทำ parse ให้ผ่าน
  • เริ่มจาก #[derive(Deserialize)] struct Config { ... }
  • ถ้า schema ยังไม่นิ่ง: เริ่มจาก serde_json::Value แล้วค่อย tighten (ย้อนดูบท 09)
  1. ทำ validate เพิ่ม (semantics)
  • app_name.trim().is_empty() ต้อง fail
  • features ห้ามซ้ำ (เช็คด้วย HashSet)
  1. ทำ output summary
  • พิมพ์เฉพาะ: app_name, port, log_level, feature_count

Test plan

  • parse_ok_and_validate_ok
  • parse_ok_but_validate_fail (เช่น app_name="" หรือ features ซ้ำ)
  • parse_fail_invalid_json

ต่อยอด (ถ้าอยากเพิ่มความท้าทาย)

  • เพิ่ม #[serde(deny_unknown_fields)] แล้วลองพิมพ์ key ผิด
  • แยก ConfigRaw และ ValidatedConfig (ตามบท 09: validate + convert)

Project B: Logger Pipeline (minimal, local-only)

เป้าหมาย

  • ออกแบบ LogLevel เป็น enum และกำหนด ordering ให้ชัด
  • filter log ตามระดับ (min_level)
  • format และพิมพ์ไปที่ console

Focus ที่ควรฝึก

  • enum + match + ordering
  • string → enum (FromStr หรือ TryFrom<&str>)
  • data ที่สื่อความหมาย: LogRecord

โครงสร้างขั้นต่ำที่แนะนำ

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum LogLevel { Error, Warn, Info, Debug }

#[derive(Debug, Clone)]
struct LogRecord {
    level: LogLevel,
    target: String,
    message: String,
}

fn should_emit(level: LogLevel, min_level: LogLevel) -> bool { /* ... */ }

Output (example):

(no output — type definitions only)

Pitfall ที่คน Python ชอบเจอ

  • ต้องการ min_level=Info แล้ว “ปล่อย Info/Warn/Error” แต่ไม่ปล่อย Debug
  • ถ้า ordering ไม่ชัด คุณจะ filter ผิดโดยไม่รู้ตัว

Test plan

  • parse: "INFO"LogLevel::Info
  • filter: min=Info ต้องไม่ปล่อย Debug
  • property ง่าย ๆ: should_emit(level, level) == true

ต่อยอด

  • แยก sink: info ไป stdout, error ไป stderr (โยงบท 08)
  • เติม field อย่าง timestamp แบบง่าย ๆ (ไม่ต้องใช้เวลา/clock จริงก็ได้ แค่ string placeholder)

Project C: AppState + Commands (add/list/reset แบบปลอดภัย)

เป้าหมาย

  • ออกแบบ AppState เก็บข้อมูล in-memory
  • ทำคำสั่ง CLI 3 แบบ: add, list, reset
  • ให้คำสั่ง “ทำอย่างเดียว” และคืน error ชัด

ไอเดียของโปรเจกต์

นี่คือการฝึก “รวมศูนย์ state” (centralized state) แทนการกระจาย mutable หลายจุดแบบ Python ซึ่งพอโตแล้วหลุดง่าย

โครงสร้างที่แนะนำ

#[derive(Default, Debug)]
struct AppState { items: Vec<String> }

enum Command {
    Add { text: String },
    List,
    Reset,
}

fn parse_args(args: &[String]) -> Result<Command, String> { /* ... */ }
fn run_command(state: &mut AppState, cmd: Command) -> Result<String, String> { /* ... */ }

Output (example):

(no output — type/function signatures only)

Step-by-step

  1. ทำ list ก่อน
  • ถ้าว่าง ให้คืน "(empty)"
  1. ทำ add
  • รวมข้อความจาก args ให้เป็น String เดียว (boundary)
  • เก็บลง state.items
  1. ทำ reset
  • clear() แล้วคืนข้อความยืนยัน

Test plan

  • parse args ถูกทุกคำสั่ง
  • run_command เปลี่ยน state ถูก (add แล้ว list มี)
  • reset แล้ว list กลับว่าง

ต่อยอด

  • เพิ่ม remove INDEX (ฝึก error message เวลา index ไม่ถูก)
  • ทำ output ให้ deterministic (เช่น list เรียงตามลำดับที่ add)

Project D: Thread-safe Counter (Arc + Mutex แบบเข้าใจจริง)

เป้าหมาย

  • ใช้ Arc<Mutex<usize>> แชร์ตัวนับระหว่างหลาย threads
  • spawn threads หลายตัวให้เพิ่มค่า แล้วรวมผล (join)

Focus ที่ควรฝึก

  • ทำไมต้อง Arc (shared ownership ระหว่าง threads)
  • ทำไมต้อง Mutex (กัน data race)
  • ทำ lock ให้สั้นที่สุด (ลด deadlock/risk)

Step-by-step

  1. ตั้งค่าทดลองให้เล็กก่อน
  • เช่น 4 threads, thread ละ 10_000 increments
  1. clone Arc เข้า thread

  2. ใน loop

  • lock → increment → drop lock
  1. join ทุก thread แล้ว assert ผลรวม

Test plan

  • เขียนฟังก์ชันที่คืนค่าผลลัพธ์ (ไม่ print) แล้ว test ด้วย assert_eq!
  • รันหลายครั้งเพื่อดูว่าผล deterministic

ต่อยอด

  • ลองเปลี่ยนเป็น AtomicUsize แล้วเทียบ mental model (ไม่ต้องทำ performance)

สรุปสิ่งที่ควรได้จาก mini projects

หลังทำครบ 4 โปรเจกต์ คุณควรจะ:

  • อ่าน compiler error ได้ไวขึ้น และแยกได้ว่าเป็น type/ownership/lifetime
  • ออกแบบ state ให้ชัดขึ้น (อะไรควร owned, อะไรควร borrowed)
  • ใช้ enum + match เพื่อบังคับให้เคสครบ (ไม่ลืม edge case)
  • เขียน tests เพื่อยืนยันพฤติกรรม (validate) แทนการเดา

ถ้าติดระหว่างทำ ให้เปิดบทถัดไป 12-learning-playbook.md แล้วไล่ checklist ทีละข้อ