ข้ามไปยังเนื้อหา

Introduce Assertion

code หลายชิ้นทำงานถูกต้องได้ก็ต่อเมื่อบางอย่างเป็นจริงอยู่ก่อน เช่น ส่วนลดอยู่ระหว่าง 0 ถึง 1 list ไม่ว่าง หรือบัญชีเปิดใช้งานแล้ว ข้อสมมติพวกนี้มักอยู่ในหัว dev ไม่ได้อยู่ใน code Introduce Assertion เขียนข้อสมมตินั้นออกมาเป็นการตรวจสอบ ที่เงียบสนิทเมื่อทุกอย่างเป็นไปตามคาด และหยุดโปรแกรมเสียงดังเมื่อไม่เป็นอย่างนั้น

assertion คือคำประกาศว่าอะไร ต้อง เป็นจริง ณ จุดนั้นใน code ไม่ใช่การจัดการ error สำหรับ input ที่คุณคาดว่าจะผิดอยู่แล้ว แต่เป็นสายสะดุดสำหรับสถานการณ์ที่ควรเป็นไปไม่ได้ ถ้า assertion ทำงานเมื่อไร แปลว่า caller มีบั๊ก

คุณอ่านการคำนวณชิ้นหนึ่งแล้วคิดในใจว่า “อันนี้จะสมเหตุสมผลก็ต่อเมื่อ rate เป็นบวก” แต่ไม่มีอะไรใน code เขียนไว้เลย ข้อสมมตินั้นจึงล่องหน วันที่ค่าศูนย์หรือค่าลบเล็ดลอดเข้ามา function ก็ผลิตขยะออกมาอย่างหน้าตาเฉย ความเสียหายไปโผล่ห่างออกไปสามชั้นในยอดรวมที่ผิดแบบเงียบ ๆ แล้วคุณก็เสียเวลาทั้งบ่ายไล่ย้อนกลับมาหาต้นตอ เงื่อนไขที่ code พึ่งพาไม่เคยถูกเขียนไว้ จึงไม่มีอะไรคอยเฝ้า

function ตัวนี้ใช้ส่วนลดที่ต้องเป็นเศษส่วนระหว่าง 0 ถึง 1 ตอนแรกข้อจำกัดนั้นไม่ได้เขียนไว้ที่ไหน หลัง refactor assertion พูดออกมาตรง ๆ ค่า rate ที่ผิดจึงพังตั้งแต่หน้าประตู

// Before
function applyDiscount(price: number, rate: number): number {
// silently assumes 0 <= rate <= 1
return price - price * rate;
}
// After
function applyDiscount(price: number, rate: number): number {
if (rate < 0 || rate > 1) {
throw new Error(`discount rate must be in [0, 1], got ${rate}`);
}
return price - price * rate;
}
flowchart LR
  A["Caller passes bad value"] --> B{"Assertion present?"}
  B -->|"No"| C["Wrong math runs<br/>NaN spreads<br/>fails far away"]
  B -->|"Yes"| D["Stops here<br/>with a clear message"]
assertion หยุดข้อสมมติที่พังไว้ที่ต้นตอ
  1. หาข้อสมมติให้เจอ อ่าน code ที่น่าสงสัยแล้วเติมประโยค “อันนี้ทำงานได้ก็ต่อเมื่อ…” ให้จบ อนุประโยคนั้นคือ assertion ของคุณ
  2. เขียนออกมาเป็นการตรวจสอบที่เป็นจริงเสมอในกรณีปกติ แล้ววางให้ใกล้ code ที่พึ่งพาเงื่อนไขนั้นที่สุด ก่อนที่งานจริงจะเริ่ม
  3. ทำให้ความล้มเหลวดังและเจาะจง ใส่ค่าที่ผิดลงไปในข้อความด้วย ผู้อ่านในอนาคตจะได้เห็นว่า อะไร ทำลายข้อสมมติ ไม่ใช่รู้แค่ว่าพัง
  4. รัน test เส้นทางที่ปกติดีต้องผ่านต่อไปโดยไม่ถูกแตะต้อง — assertion ที่ถูกต้องไม่เปลี่ยนแปลงอะไรสำหรับ input ที่ถูกต้อง
  5. เพิ่ม test ที่ป้อนค่าที่ผิดกฎเข้าไป แล้วยืนยันว่า assertion ถูกจุดชนวน นี่พิสูจน์ว่าสายสะดุดถูกต่อไว้แล้ว
  6. ทำซ้ำสำหรับข้อสมมติถัดไปที่ไม่ได้กล่าวไว้ ต้านทานการ assert สิ่งที่ชัดเจนว่าถูกรับประกันโดยชนิดข้อมูลอยู่แล้ว ให้ assert สิ่งที่ caller อาจทำผิดได้จริง

ใส่ assertion เมื่อ code ส่วนหนึ่งพึ่งพาเงื่อนไขอยู่เงียบ ๆ และถ้าเงื่อนไขนั้นพัง ข้อมูลจะเสียหายในจุดที่ไกลจากต้นเหตุมาก assertion ยังทำหน้าที่เป็นเอกสารไปในตัว เพราะบอกผู้อ่านคนถัดไปตรง ๆ ว่า code คาดหวังอะไร และต่างจาก comment ตรงที่เคลื่อนห่างจากความจริงไม่ได้ เพราะอันที่เป็นเท็จจะพังให้เห็นเอง

เส้นแบ่งที่ต้องระวังคือ assertion กับการจัดการ error assertion คอยจับความผิดพลาดของ dev คือ state ที่ควรเป็นไปไม่ได้ถ้า caller ทุกตัวทำถูก ส่วน input ที่ผิดรูปได้เป็นปกติ เช่น ผู้ใช้พิมพ์ผิด หรือ payload จาก network เสีย ไม่ใช่หน้าที่ของ assertion ให้ validate แล้ว return error จริงออกไป ใน Rust คือเลือกระหว่าง assert!/debug_assert! กับ Result ส่วนใน Go คือเลือกระหว่าง panic กับ error ที่ return ออกมา และอย่าใส่ side effect ไว้ใน

assertion เพราะ build ที่ถอด assertion ออก (Python’s -O, Rust’s debug_assert!) จะเปลี่ยน behavior อย่างเงียบ ๆ

ใช้ Introduce Assertion เมื่อหลีกเลี่ยงเมื่อ
code พึ่งพาเงื่อนไขโดยไม่กล่าวไว้ชัดเจนinput นั้นมาจาก user หรือ network — ใช้ error handling แทน
ต้องการให้ violation หยุดที่ต้นตอ ไม่ใช่ propagateassertion ที่จะ trigger ใน production flow ปกติ
ข้อสมมติที่ละเมิดแล้วสร้างความเสียหายที่ห่างไกลassertion มี side effect — จะหายไปใน optimized build

⚠️ ไม่ควร Introduce Assertion เมื่อ:

  • เงื่อนไขนั้น validate input จาก user หรือ external system — ควร return error แทน
  • assertion ทำงานบ่อย ๆ ใน normal flow — แปลว่านั่นคือ expected condition
  • ใส่ side effect ใน assertion — behavior จะเปลี่ยนเมื่อ assertion ถูกปิด
assertion มีไว้เพื่อดักจับอะไร?
เนื่องจาก Go ไม่มีคีย์เวิร์ด assert บทเรียนนี้กล่าว invariant สำหรับบั๊กแบบ "เป็นไปไม่ได้" จริง ๆ อย่างไร?
ใน Rust แมโครใดรันการตรวจสอบเฉพาะใน build สำหรับการพัฒนา?
ทำไม assertion จึงไม่ควรมี side effect เลย?