Dijkstra และการเขียนโปรแกรมเชิงโครงสร้าง: วินัยที่ขยายได้
แนวคิดการเขียนโปรแกรมเชิงโครงสร้างของ Edsger Dijkstra อธิบายว่าทำไมโค้ดที่มีวินัยและเรียบง่ายถึงยังคงถูกต้องและดูแลรักษาได้เมื่อทีม ฟีเจอร์ และระบบเติบโตขึ้น

เหตุใด Dijkstra จึงยังสำคัญเมื่อซอฟต์แวร์เติบโต
ซอฟต์แวร์ไม่ค่อยล้มเหลวเพราะมันเขียนไม่ได้ แต่มักล้มเพราะผ่านไปหนึ่งปีแล้วไม่มีใครกล้าแก้ไขอย่างปลอดภัยอีกต่อไป
เมื่อโค้ดเบสเติบโต การปรับแต่ง “เล็กๆ” ทุกอย่างเริ่มส่งผลกระทบ: การแก้บั๊กหนึ่งอย่างอาจทำให้ฟีเจอร์อีกมุมพัง ข้อกำหนดใหม่บังคับให้ต้องเขียนใหม่ และการรีแฟกเตอร์ง่ายๆ กลายเป็นงานประสานนานเป็นสัปดาห์ สิ่งยากไม่ใช่การเพิ่มโค้ด—แต่คือการทำให้พฤติกรรมคงที่ในขณะที่สิ่งรอบๆ เปลี่ยนไป
ข้อสัญญา: ความถูกต้องและความเรียบง่ายลดต้นทุนในระยะยาว
Edsger Dijkstra โต้แย้งว่าความถูกต้องและความเรียบง่ายควรเป็นเป้าหมายหลัก ไม่ใช่แค่อะไรที่ดีถ้ามี ผลตอบแทนไม่ใช่เรื่องทฤษฎีล้วนๆ เมื่อระบบง่ายต่อการคิด ทีมใช้เวลาน้อยลงกับการดับเพลิงและมากขึ้นกับการสร้างสรรค์
- ความถูกต้อง ลดต้นทุนจากความผิดพลาด: เหตุการณ์น้อยลง การกลับมาทดสอบซ้ำ (regressions) น้อยลง และโค้ดแบบ “ห้ามแตะ” น้อยลง
- ความเรียบง่าย ลดต้นทุนจากการเปลี่ยนแปลง: สมมติฐานที่ซ่อนอยู่น้อยลง กรณีพิเศษน้อยลง และความประหลาดใจตอนรีวิวลดลง
“ขยายตัว” หมายถึงอะไรจริงๆ
เมื่อคนพูดว่าซอฟต์แวร์ต้อง “scale” มักคิดถึงประสิทธิภาพ แต่ข้อสังเกตของ Dijkstra ต่างออกไป: ความซับซ้อนก็ขยายตามเช่นกัน
การขยายตัวแสดงออกเป็น:
- ฟีเจอร์มากขึ้น: กระบวนการใหม่ กรณีขอบ และความคาดหวังของผู้ใช้
- คนมากขึ้น: การส่งต่อ งานสไตล์ที่ต่างกัน และบริบทที่ไม่เหมือนกัน
- การผสานรวมมากขึ้น: API ภายนอก แหล่งข้อมูล และโหมดความล้มเหลวที่หลากหลาย
- เวลาเพิ่มขึ้น: การตัดสินใจที่เป็นมรดก ข้อกำหนดที่เปลี่ยน และการเขียนบางส่วนซ้ำ
แนวคิดหลัก: โครงสร้างทำให้พฤติกรรมคาดเดาง่ายขึ้น
การเขียนโปรแกรมเชิงโครงสร้างไม่ใช่การเข้มงวดเพราะอยากเข้มงวด แต่มันคือการเลือกการควบคุมการไหลและการแยกส่วนที่ทำให้ตอบสองคำถามได้ง่าย:
- “ต่อไปเกิดอะไรขึ้น?”
- “ภายใต้เงื่อนไขใดบ้าง?”
เมื่อพฤติกรรมคาดเดาได้ การเปลี่ยนแปลงทำได้เป็นกิจวัตรแทนที่จะเสี่ยง นั่นคือเหตุผลที่ Dijkstra ยังมีความหมาย: วินัยของเขาเจาะจงคอขวดจริงของซอฟต์แวร์ที่เติบโต—การเข้าใจมันพอที่จะปรับปรุงได้
แนะนำสั้นๆ เกี่ยวกับ Edsger Dijkstra และเป้าหมายของเขา
Edsger W. Dijkstra (1930–2002) เป็นนักวิทยาการคอมพิวเตอร์ชาวดัตช์ที่มีบทบาทในการกำหนดวิธีคิดของโปรแกรมเมอร์เกี่ยวกับการสร้างซอฟต์แวร์ที่เชื่อถือได้ เขาทำงานกับระบบปฏิบัติการยุคแรก มีส่วนร่วมกับอัลกอริทึมหลายอย่าง (รวมถึงอัลกอริทึมเส้นทางสั้นที่สุดที่มีชื่อของเขา) และ—ที่สำคัญสำหรับผู้พัฒนาทั่วไป—ผลักดันแนวคิดว่า การโปรแกรมควรเป็นสิ่งที่เราสามารถ ให้เหตุผลได้ ไม่ใช่แค่ลองจนมันดูเหมือนจะทำงาน
โฟกัสหลักของเขา: การให้เหตุผล มากกว่า “มันรันได้บนเครื่องฉัน”
Dijkstra ไม่ได้ให้ความสำคัญกับว่าโปรแกรมจะผลิตผลลัพธ์ที่ถูกต้องในตัวอย่างไม่กี่เคสได้หรือไม่ แต่สนใจว่าเราจะ อธิบายได้ ว่ามันถูกต้องสำหรับกรณีที่สำคัญหรือไม่
ถ้าคุณระบุได้ว่าโค้ดชิ้นหนึ่งควรทำอะไร คุณควรสามารถอธิบายเป็นขั้นตอนว่าทำไมมันถึงทำได้ ความคิดแบบนี้นำไปสู่โค้ดที่ตามอ่านง่ายขึ้น ตรวจสอบง่ายขึ้น และพึ่งพาการดีบักแบบฮีโร่น้อยลง
ทำไมเขาถึงฟังดูเข้มงวด—และเหตุใดนั่นถึงช่วยได้
งานเขียนบางส่วนของ Dijkstra อ่านแล้วไม่ยอมลดมาตรฐาน เขาตำหนิเทคนิค “ฉลาดล้ำ” การควบคุมการไหลที่ยับยั้ง และนิสัยการเขียนโค้ดที่ทำให้การให้เหตุผลยาก ความเข้มงวดนี้ไม่ใช่การบอกว่าต้องทำตามสไตล์ แต่มันเพื่อลดความกำกวม เมื่อความหมายของโค้ดชัดเจน คุณจะเสียเวลาน้อยลงกับการถกเถียงเจตนา และมากขึ้นกับการยืนยันพฤติกรรม
การเขียนโปรแกรมเชิงโครงสร้างหมายถึงอะไร (ระดับสูง)
การเขียนโปรแกรมเชิงโครงสร้างคือการสร้างโปรแกรมจากชุดโครงสร้างควบคุมที่ชัดเจน—ลำดับ การเลือก (if/else) และการทำซ้ำ—แทนการกระโดดไปมาจนปวดหัว เป้าหมายคือทำให้เส้นทางผ่านโปรแกรมเข้าใจได้เพื่อที่จะอธิบาย บำรุงรักษา และเปลี่ยนแปลงด้วยความมั่นใจ
ความถูกต้อง: ฟีเจอร์ที่ผู้ใช้พึ่งพาโดยไม่รู้ตัว
คนมักพูดถึงคุณภาพซอฟต์แวร์เป็น “เร็ว”, “สวยงาม”, หรือ “มีฟีเจอร์มาก” ผู้ใช้ประสบความถูกต้องในรูปแบบที่เงียบ: เป็นความมั่นใจว่าแอปจะไม่ทำให้พวกเขาตกใจ เมื่อความถูกต้องมีอยู่ ไม่มีใครสังเกต แต่เมื่อขาดหาย ทุกอย่างอื่นไม่สำคัญ
“มันทำงานตอนนี้” กับ “มันยังทำงานต่อไป”
“มันทำงานตอนนี้” มักหมายความว่าคุณลองเส้นทางไม่กี่ทางแล้วได้ผลลัพธ์ที่คาดหวัง “มันยังทำงานต่อไป” หมายความว่ามันทำงานตามที่ตั้งใจในระยะยาว ครอบคลุมกรณีขอบ การรีแฟกเตอร์ การผสานรวม ปริมาณโหลดที่สูงขึ้น และคนใหม่ๆ ที่มาจับโค้ด
ฟีเจอร์หนึ่งอาจ “ทำงานตอนนี้” แต่เปราะบาง:
- ขึ้นกับว่าข้อมูลนำเข้าต้องสะอาดเสมอ
- สมมติว่าการเรียกเครือข่ายจะตอบกลับเร็วเสมอ
- ผ่านการทดสอบที่ครอบคลุมแค่เส้นทางปกติเท่านั้น
ความถูกต้องคือการเอาสมมติฐานที่ซ่อนอยู่ออก—หรือทำให้ชัดเจน
บั๊กเล็กๆ ทวีคูณในระบบใหญ่
บั๊กเล็กๆ แทบไม่คงอยู่เป็นเรื่องเล็กเมื่อซอฟต์แวร์โตขึ้น สถานะไม่ถูกต้อง ขอบเขต off-by-one หรือกฎการจัดการข้อผิดพลาดที่ไม่ชัดเจน มักถูกคัดลอกไปในโมดูลใหม่ ห่อด้วยบริการอื่น ถูกแคช หรือตั้งค่า retry หรือ “แก้ทาง” เมื่อเวลาผ่านไป ทีมเริ่มเลิกถามว่า “อะไรเป็นความจริง?” และเริ่มถามว่า “ปกติแล้วเกิดอะไรขึ้น?” นั่นคือเมื่อตอบเหตุการณ์กลายเป็นงานโบราณคดี
ตัวคูณคือลำดับการพึ่งพา: พฤติกรรมผิดเล็กๆ กลายเป็นพฤติกรรมผิดใน downstream หลายๆ จุด แต่ละจุดมีการแก้ไขชั่วคราวของตัวเอง
ความชัดเจนเป็นเครื่องมือความถูกต้อง (ไม่ใช่แค่รสนิยม)
โค้ดที่ชัดเจนช่วยความถูกต้องเพราะมันช่วยการสื่อสาร:
- การรีวิวโค้ด จะจับปัญหาจริงๆ ได้เมื่อเจตนาเห็นชัด
- การเริ่มงาน ทำได้เร็วขึ้นเมื่อกฎอ่านได้ ไม่ใช่ความรู้แบบชนเผ่า
- การตอบเหตุการณ์ ใช้เวลาน้อยลงเมื่อการไหลการควบคุมและโหมดความล้มเหลวติดตามง่าย
นิยามเชิงปฏิบัติของความถูกต้องสำหรับทีมผลิตภัณฑ์
ความถูกต้องหมายความว่า: สำหรับอินพุตและสถานการณ์ที่เราบอกว่ารองรับ ระบบให้ผลลัพธ์ที่เราสัญญาอย่างสม่ำเสมอ—และเมื่อไม่สามารถทำได้ ระบบล้มเหลวในวิธีที่คาดเดาและอธิบายได้
ความเรียบง่ายเป็นกลยุทธ์ ไม่ใช่รสนิยม
ความเรียบง่ายไม่ใช่การทำให้โค้ด “น่ารัก” หรือลดโค้ดให้เหลือน้อยที่สุดด้วยเหตุผลเดียว แต่มันคือการทำให้พฤติกรรมคาดเดา อธิบาย และแก้ไขได้โดยไม่ต้องกลัว Dijkstra ให้คุณค่าแก่ความเรียบง่ายเพราะมันช่วยให้เราคิดเหตุผลเกี่ยวกับโปรแกรมได้ดีขึ้น—โดยเฉพาะเมื่อโค้ดเบสและทีมขยายตัว
ความเรียบง่ายคืออะไร (และไม่ใช่อะไร)
โค้ดเรียบง่ายทำให้มีแนวคิดไม่มากที่ต้องติดตามพร้อมกัน: การไหลของข้อมูลชัดเจน การไหลการควบคุมชัดเจน และความรับผิดชอบชัดเจน มันไม่บังคับให้ผู้อ่านต้องจำลองเส้นทางแทนกันหลายทางในหัว
ความเรียบง่ายไม่ใช่:
- การลดบรรทัดโดยไม่คิด
- ทริก “ฉลาด” บรรทัดหนา หรือ abstraction หนัก
- การหลีกเลี่ยงโครงสร้างเพื่อดูยืดหยุ่น
ความซับซ้อนที่เกิดขึ้นโดยไม่ได้ตั้งใจ: สิ่งที่คุณไม่ได้ตั้งใจเพิ่ม
หลายระบบยากจะเปลี่ยนไม่ใช่เพราะโดเมนมีความซับซ้อนโดยธรรมชาติ แต่เพราะเรานำความซับซ้อนที่เกิดขึ้นโดยไม่ได้ตั้งใจเข้ามา: ธงที่มีปฏิสัมพันธ์ไม่คาดคิด แพตช์กรณีพิเศษที่ไม่เคยถูกลบ และชั้นต่างๆ ที่มีไว้แก้ข้อจำกัดจากการตัดสินใจก่อนหน้า
แต่ละข้อยกเว้นเพิ่มเติมคือภาษีบนความเข้าใจ ต้นทุนปรากฏเมื่อต้องแก้บั๊กและพบว่าการเปลี่ยนแปลงหนึ่งจุดทำให้หลายส่วนพังอย่างละเอียดอ่อน
ออกแบบเรียบง่ายลดความต้องการฮีโร่
เมื่อการออกแบบเรียบง่าย ความก้าวหน้ามาจากงานที่ต่อเนื่อง: การเปลี่ยนแปลงที่ตรวจสอบได้ diff เล็ก และการแก้ไขฉุกเฉินน้อย ทีมไม่ต้องพึ่งฮีโร่ที่จำทุกกรณีขอบในประวัติศาสตร์หรือสามารถดีบักตอนตีสองได้ แต่ระบบรองรับช่วงความสนใจตามปกติของมนุษย์
กฎสังเกต: กรณีพิเศษน้อยลง ความประหลาดใจน้อยลง
การทดสอบแบบปฏิบัติ: หากคุณเพิ่มข้อยกเว้นซ้ำๆ (“ยกเว้นเมื่อ…”, “ยกเว้นกรณีนี้…”, “เฉพาะสำหรับลูกค้านี้…”) คุณอาจกำลังสะสมความซับซ้อนโดยไม่ได้ตั้งใจ ชอบโซลูชันที่ลดการแตกแขนงของพฤติกรรม—กฎเดียวที่สม่ำเสมอชนะห้ากรณีพิเศษ แม้ว่ากฎเดียวจะกว้างกว่าสิ่งที่คิดตอนแรกเล็กน้อยก็ตาม
การเขียนโปรแกรมเชิงโครงสร้าง: การควบคุมการไหลที่เชื่อถือได้
การเขียนโปรแกรมเชิงโครงสร้างเป็นแนวคิดเรียบง่ายแต่มีผลใหญ่: เขียนโค้ดให้เส้นทางการทำงานตามโปรแกรมติดตามได้ง่าย พูดง่ายๆ โปรแกรมส่วนใหญ่สร้างจากบล็อกพื้นฐานสามอย่าง—ลำดับ, การเลือก, และ การทำซ้ำ—โดยไม่พึ่งพาการกระโดดที่ยุ่งเหยิง
บล็อกสามอย่าง (ในคำง่ายๆ)
- ลำดับ: ทำขั้นตอน A แล้ว B แล้ว C
- การเลือก: เลือกเส้นทางตามเงื่อนไข (เช่น
if/else,switch) - การทำซ้ำ: ทำชุดขั้นตอนซ้ำๆ ขณะที่เงื่อนไขเป็นจริง (เช่น
for,while)
เมื่อการควบคุมการไหลประกอบด้วยโครงสร้างเหล่านี้ โดยปกติคุณสามารถอธิบายสิ่งที่โปรแกรมทำได้โดยการอ่านจากบนลงล่าง โดยไม่ต้อง “กระโดด” ไปรอบไฟล์
สิ่งที่มันมาแทน: เส้นทางการทำงานแบบสปาเก็ตตี้
ก่อนที่การเขียนโปรแกรมเชิงโครงสร้างจะเป็นมาตรฐาน โค้ดจำนวนมากพึ่งพาการกระโดดแบบอิสระ (goto) ปัญหาไม่ใช่การกระโดดเสมอไป แต่คือการกระโดดแบบ ไม่จำกัด ที่สร้างเส้นทางการทำงานที่คาดเดายาก คุณมักจะต้องถามว่า “เรามาถึงตรงนี้ได้ยังไง?” และ “ตัวแปรนี้อยู่ในสถานะไหน?”—และโค้ดมักไม่ตอบอย่างชัดเจน
ทำไมความชัดเจนจึงสำคัญสำหรับทีมจริง
การควบคุมการไหลที่ชัดเจนช่วยให้คนสร้างแบบจำลองทางความคิดที่ถูกต้อง แบบจำลองนั้นคือสิ่งที่คุณพึ่งพาเมื่อดีบัก ตรวจสอบ pull request หรือเปลี่ยนพฤติกรรมภายใต้ความกดดัน
เมื่อโครงสร้างสม่ำเสมอ การเปลี่ยนแปลงปลอดภัยขึ้น: คุณสามารถเปลี่ยนสาขาหนึ่งโดยไม่กระทบอีกสาขา หรือรีแฟกเตอร์ลูปโดยไม่พลาดทางออกที่ซ่อนอยู่ ความอ่านง่ายไม่ใช่แค่ความงาม—มันคือพื้นฐานของการเปลี่ยนพฤติกรรมด้วยความมั่นใจโดยไม่ทำลายในสิ่งที่ทำงานได้แล้ว
เครื่องมือการให้เหตุผล: Invariants, Preconditions, และ Postconditions
Dijkstra ผลักแนวคิดง่ายๆ: ถ้าคุณอธิบายได้ว่าโค้ดถูกต้องอย่างไร คุณก็สามารถเปลี่ยนมันได้น้อยความกลัว เครื่องมือการให้เหตุผลสามอย่างทำให้สิ่งนี้ปฏิบัติได้—โดยไม่ต้องเปลี่ยนทีมให้กลายเป็นนักคณิตศาสตร์
Invariants: “ข้อเท็จจริงที่คงอยู่”
Invariant คือข้อเท็จจริงที่ยังคงเป็นจริงขณะรันโค้ด โดยเฉพาะในลูป ตัวอย่าง: คุณรวมราคาสินค้า Invariant ที่มีประโยชน์คือ: “total เท่ากับผลรวมของไอเท็มที่ประมวลผลแล้ว” ถ้ามันจริงในทุกขั้นตอน เมื่อจบลูป ผลลัพธ์ก็เชื่อถือได้
Invariants มีพลังเพราะมันดึงความสนใจไปที่ สิ่งที่ต้องไม่แตกหัก ไม่ใช่แค่สิ่งที่จะเกิดขึ้นถัดไป
Preconditions และ Postconditions: สัญญาในชีวิตประจำวัน
Precondition คือสิ่งที่ต้องเป็นจริงก่อนฟังก์ชันรัน Postcondition คือสิ่งที่ฟังก์ชันรับประกันหลังเสร็จ
ตัวอย่างในชีวิตประจำวัน:
- Precondition: “คุณสามารถถอนเงินได้ก็ต่อเมื่อบัญชีมีเงินเพียงพอ”
- Postcondition: “หลังถอน ยอดเงินลดลงเท่าจำนวนที่ถอน และไม่ติดลบ”
ในโค้ด Precondition อาจเป็น “ลิสต์นำเข้าเรียงแล้ว” และ Postcondition อาจเป็น “ลิสต์ที่ส่งออกเรียงแล้วและมีสมาชิกเดิมบวกรายการที่แทรกเข้ามา”
การเขียนมันลงเปลี่ยนการออกแบบและรีวิวอย่างไร
เมื่อคุณเขียนสิ่งเหล่านี้ลง (แม้แบบไม่เป็นทางการ) การออกแบบจะคมขึ้น: คุณตัดสินใจว่าฟังก์ชัน คาดหวังอะไร และ สัญญาอะไร และโดยธรรมชาติมันจะเล็กลงและมุ่งเป้ากว่า
ในการรีวิว มันเปลี่ยนการถกเถียงจากสไตล์ (“ฉันจะเขียนแบบนี้ต่างออกไป”) เป็นความถูกต้อง (“โค้ดนี้รักษา invariant หรือไม่?” “เราบังคับ precondition หรือแค่เอกสารประกอบ?”)
ปฏิบัติแบบน้ำหนักเบา: คอมเมนต์ตรงที่มักมีบั๊ก
คุณไม่จำเป็นต้องพิสูจน์อย่างเป็นทางการเพื่อให้ได้ประโยชน์ เลือกลูปที่มีบั๊กบ่อยที่สุดหรืออัปเดตสถานะที่สลับซับซ้อน และเติมคอมเมนต์ invariant หนึ่งบรรทัดเหนือมัน เมื่อคนแก้โค้ดทีหลัง คอมเมนต์นั้นจะทำหน้าที่เป็นราวจับ: ถ้าการเปลี่ยนแปลงทำลายข้อเท็จจริงนี้ โค้ดก็ไม่ปลอดภัยอีกต่อไป
การทดสอบกับการให้เหตุผล: แต่ละอย่างการันตีอะไรได้บ้าง
การทดสอบและการให้เหตุผลมุ่งสู่ผลลัพธ์เดียวกัน—ซอฟต์แวร์ที่ทำงานตามที่ตั้งใจ—แต่ทำงานต่างกันอย่างมาก การทดสอบ ค้นพบ ปัญหาโดยลองตัวอย่าง การให้เหตุผล ป้องกัน หมวดของปัญหาโดยทำตรรกะให้ชัดเจนและตรวจสอบได้
การทดสอบทำได้ดีอะไร
การทดสอบเป็นตาข่ายความปลอดภัยที่ปฏิบัติได้ มันจับ regression ยืนยันสถานการณ์จริง และเป็นเอกสารพฤติกรรมที่ทั้งทีมรันได้
แต่การทดสอบแสดงการมีบั๊กได้ ไม่สามารถพิสูจน์ว่าปราศจากบั๊กได้ ไม่มีชุดทดสอบใดครอบคลุมทุกอินพุต ทุกความแปรผันของเวลา หรือการโต้ตอบระหว่างฟีเจอร์ หลายกรณี “มันใช้ได้บนเครื่องฉัน” มาจากการรวมกันที่ไม่ได้ทดสอบ: อินพุตหายาก ลำดับการดำเนินการเฉพาะ หรือสถานะที่ปรากฏหลังหลายขั้นตอน
การให้เหตุผลการันตีอะไรได้ (และไม่ได้)
การให้เหตุผลคือการพิสูจน์คุณสมบัติของโค้ด: “ลูปนี้ยุติแน่นอน”, “ตัวแปรนี้ไม่ติดลบ”, “ฟังก์ชันนี้ไม่คืนค่าวัตถุไม่ถูกต้อง” เมื่อทำดี มันกีดกันหมวดของข้อบกพร่อง—โดยเฉพาะบริเวณขอบเขตและกรณีพิเศษ
ข้อจำกัดคือความพยายามและขอบเขต การพิสูจน์เชิงรูปแบบสำหรับผลิตภัณฑ์ทั้งชิ้นมักไม่คุ้มค่า การให้เหตุผลมีประสิทธิภาพเมื่อใช้คัดเลือก: อัลกอริทึมหลัก ฟลูว์ที่สำคัญด้านความปลอดภัย ลอจิกการเงิน และ concurrency
แนวทางสมดุลที่ขยายได้
ใช้การทดสอบอย่างกว้าง และนำการให้เหตุผลเชิงลึกไปใช้ในจุดที่ความล้มเหลวมีต้นทุนสูง
สะพานเชิงปฏิบัติระหว่างทั้งสองคือการทำให้เจตนาสามารถรันได้:
- Assertions สำหรับสมมติฐานภายใน (เช่น “index อยู่ในช่วง”)
- Preconditions และ postconditions (สัญญา) สำหรับอินพุต/เอาต์พุตของฟังก์ชัน
- Invariants สำหรับความจริงที่คงอยู่ (เช่น “ยอดรวมตะกร้าเท่ากับผลรวมไอเท็ม”)
เทคนิคเหล่านี้ไม่แทนที่การทดสอบ—แต่ทำให้ตาข่ายแน่นขึ้น เปลี่ยนความคาดหวังคลุมเครือเป็นกฎที่ตรวจสอบได้ ทำให้เขียนบั๊กยากขึ้นและวินิจฉัยได้ง่ายขึ้น
วินัย: ทีมหลีกเลี่ยง “หนี้ความเฉลียวฉลาด” อย่างไร
โค้ด “ฉลาด” มักรู้สึกชนะในตอนนั้น: บรรทัดน้อย ทริกเก๋ บรรทัดเดียวที่ทำให้ภูมิใจ ปัญหาคือความเฉลียวฉลาดไม่ขยายตัวตามเวลาและคน ผู้เขียนลืมทริกในหกเดือนต่อมา สมาชิกทีมใหม่อ่านแบบตัวหนังสือ ตีความผิดสมมติฐานที่ซ่อนอยู่ และเปลี่ยนมันจนพัง นั่นคือ “หนี้ความเฉลียวฉลาด”: ความเร็วระยะสั้นแลกกับความสับสนระยะยาว
วินัยคือตัวเร่งทีม
จุดของ Dijkstra ไม่ใช่ “เขียนโค้ดน่าเบื่อ” เป็นรสนิยม แต่คือข้อจำกัดที่มีวินัยทำให้โปรแกรมง่ายต่อการให้เหตุผล ในทีม ข้อจำกัดลดความเหนื่อยจากการตัดสินใจ ถ้าทุกคนรู้ค่าเริ่มต้นแล้ว (การตั้งชื่อ การจัดโครงสร้างฟังก์ชัน รูปแบบการจัดการข้อผิดพลาด) คุณเลิกถกเถียงเรื่องพื้นฐานในทุก pull request เวลาที่ได้กลับไปคือเวลาสำหรับงานผลิตภัณฑ์
วินัยแสดงออกในปฏิบัติประจำ:
- รีวิวโค้ด ที่ให้รางวัลความชัดเจนมากกว่าความใหม่ (“คนอื่นจะเปลี่ยนได้ปลอดภัยไหม?”)
- มาตรฐานร่วมกัน (รูปแบบ การตั้งชื่อ การจัดการข้อผิดพลาด) เพื่อให้โค้ดอ่านเป็นเสียงเดียว
- รีแฟกเตอร์เป็นการบำรุงรักษา ไม่ใช่ภารกิจช่วยชีวิต—การทำความสะอาดเล็กๆ ทำอย่างต่อเนื่อง
รูปลักษณ์ของ “มีวินัย” ในโค้ด
นิสัยบางอย่างช่วยป้องกันการสะสมหนี้ความเฉลียวฉลาด:
- ฟังก์ชันเล็กๆ ทำงานเดียว ชัดเจนอินพุต/เอาต์พุต
- ชื่อตัวแปรชัดเจน อธิบายเจตนา (เช่น
calculate_total()ดีกว่าdo_it()) - ไม่มีสถานะซ่อน: ลด global และ side effects; ส่งพารามิเตอร์ที่จำเป็นชัดเจน
- การไหลควบคุมตรงไปตรงมา: หลีกเลี่ยงตรรกะพึ่งลำดับ การใช้ค่ามหัศจรรย์ หรือ “มันทำงานถ้ารู้ทริก”
วินัยไม่ใช่ความสมบูรณ์แบบ—แต่คือการทำให้การเปลี่ยนครั้งถัดไปคาดเดาได้
โมดูลาร์และขอบเขต: ทำให้การเปลี่ยนแปลงเป็นเรื่องท้องถิ่น
โมดูลาร์ไม่ใช่แค่ “แยกโค้ดเป็นไฟล์” แต่มันคือการแยกการตัดสินใจไว้หลังขอบเขตที่ชัดเจนเพื่อให้ส่วนอื่นของระบบไม่จำเป็นต้องรู้หรือสนใจรายละเอียดภายใน โมดูลซ่อนความยุ่งเหยิง—โครงสร้างข้อมูล กรณีขอบ และทริกด้านประสิทธิภาพ—และเผยผิวหน้าเล็กๆ ที่คงที่
โมดูลลดรัศมีความเสียหายอย่างไร
เมื่อมีคำขอเปลี่ยน แผลเป็นอุดมคติคือ: โมดูลเดียวเปลี่ยน ส่วนที่เหลือไม่แตะ นั่นคือความหมายของ “ทำให้การเปลี่ยนเป็นท้องถิ่น” ขอบเขตป้องกันการ coupling โดยไม่ตั้งใจ—ที่การอัปเดตหนึ่งจุดทำให้สามอย่างพังเพราะพวกมันแชร์สมมติฐานกัน
ขอบเขตที่ดีทำให้การให้เหตุผลง่ายขึ้น ถ้าคุณสามารถระบุสิ่งที่โมดูลรับประกัน คุณก็คิดถึงโปรแกรมใหญ่ขึ้นได้โดยไม่ต้องอ่านการทำงานทั้งหมดทุกครั้ง
อินเทอร์เฟซคือคำสัญญา (และช่วยให้ทำงานคู่ขนานได้อย่างไร)
อินเทอร์เฟซเป็นคำสัญญา: “ด้วยอินพุตแบบนี้ ฉันจะให้เอาต์พุตแบบนี้และรักษากฎเหล่านี้ไว้” เมื่อคำสัญญานั้นชัดเจน ทีมสามารถทำงานพร้อมกัน:
- คนหนึ่งพัฒนาโมดูล
- อีกคนสร้าง caller โดยใช้อินเทอร์เฟซ
- QA ออกแบบการทดสอบตามพฤติกรรมที่สัญญาไว้
นี่ไม่ใช่การเพิ่มระเบียบราชการ แต่มันทำจุดประสานงานที่ปลอดภัยในโค้ดเบสที่โตขึ้น
การตรวจสอบโมดูลแบบง่ายๆ ที่ป้องกันการเบี่ยงเบน
คุณไม่ต้องมีการทบทวนสถาปัตยกรรมใหญ่เพื่อปรับปรุงความเป็นโมดูลาลส์ ลองเช็คลิสต์น้ำหนักเบาเหล่านี้:
- อินพุต/เอาต์พุต: คุณสรุปอินพุต เอาต์พุต และผลข้างเคียงของโมดูลในไม่กี่บรรทัดได้ไหม? ถ้าไม่ได้ แปลว่ามันทำมากเกินไป
- ความเป็นเจ้าของ: ใครรับผิดชอบพฤติกรรมและการเปลี่ยนแปลง? โมดูลที่ไม่มีเจ้าของกลายเป็นที่ทิ้งขยะ
- การพึ่งพา: มันพึ่งพา “ทุกอย่าง” หรือแค่สิ่งที่ต้องการจริงๆ? การพึ่งพาน้อยหมายถึงโอกาสพังน้อยลง
ขอบเขตที่วาดดีเปลี่ยน “การเปลี่ยน” จากเหตุการณ์ระดับระบบเป็นการแก้ไขเฉพาะจุด
ทำไมแนวคิดเหล่านี้ชนะเมื่อต้องขยาย (ทีม โค้ดเบส และเวลา)
เมื่อซอฟต์แวร์ยังเล็ก คุณอาจ “เก็บทุกอย่างไว้ในหัว” ได้ แต่เมื่อขยาย สิ่งนั้นหยุดเป็นจริง—และโหมดล้มเหลวที่คุ้นเคยก็ปรากฏ
อาการทั่วไปเช่น:
- การล่มที่ย้อนกลับไปยังกรณีขอบที่น่าประหลาดใจ
- การปล่อยช้าลงเพราะทุกการเปลี่ยนดูเสี่ยง
- การผสานรวมเปราะบางที่การอัปเดตเล็กๆ ทำให้ downstream พังหลายจุด
โครงสร้างลดภาระทางปัญญา
เดิมพันหลักของ Dijkstra คือมนุษย์คือคอขวด การไหลการควบคุมที่ชัดเจน ฟังก์ชันเล็กๆ ที่กำหนดอินพุต/เอาต์พุต และโค้ดที่คุณคิดเหตุผลได้ไม่ใช่ตัวเลือกด้านความงาม—มันคือเครื่องคูณความสามารถ
ในโค้ดเบสขนาดใหญ่ โครงสร้างทำหน้าที่เหมือนการบีบอัดความเข้าใจ ถ้าฟังก์ชันมีอินพุต/เอาต์พุตชัดเจน โมดูลมีขอบเขตที่ตั้งชื่อได้ และเส้นทางปกติไม่ถูกปนกับทุกกรณีขอบ นักพัฒนาจะเสียเวลาสร้างความตั้งใจน้อยลงและทำการเปลี่ยนได้อย่างมีสติยิ่งขึ้น
มันขยายตัวกับทีม ไม่ใช่แค่โค้ด
เมื่อทีมโต ค่าใช้จ่ายในการสื่อสารเพิ่มขึ้นเร็วกว่าจำนวนบรรทัด โค้ดที่มีวินัยและอ่านง่ายลดความรู้ชนเผ่าที่จำเป็นในการมีส่วนร่วมอย่างปลอดภัย
สิ่งนี้เห็นได้ชัดใน onboarding: วิศวกรใหม่ตามรูปแบบที่คาดเดาได้ เรียนรู้ชุดข้อบังคับเล็กๆ และเปลี่ยนโดยไม่ต้องทัวร์ยาวของ “จุดปัญหา” โค้ดสอนระบบเอง
การตอบเหตุการณ์ง่ายขึ้นและปลอดภัยที่จะย้อนกลับ
ในเหตุการณ์ เวลาและความมั่นใจหายาก โค้ดที่เขียนด้วยสมมติฐานชัดเจน (preconditions) การตรวจสอบที่มีความหมาย และการไหลควบคุมตรงไปตรงมาจะติดตามง่ายในความกดดัน
สำคัญยิ่งไปกว่านั้น การเปลี่ยนที่มีวินัยย้อนกลับได้ง่ายขึ้น การแก้ไขเล็กๆ ในท้องถิ่นพร้อมขอบเขตชัดเจนลดโอกาสที่การย้อนกลับจะก่อให้เกิดความล้มเหลวใหม่ ผลลัพธ์ไม่ใช่ความสมบูรณ์แบบ—แต่คือความประหลาดใจที่น้อยลง การฟื้นตัวที่เร็วขึ้น และระบบที่ยังคงดูแลรักษาง่ายเมื่อปีและผู้ร่วมพัฒนาสะสมขึ้น
นำ Dijkstra ไปใช้โดยไม่เคร่งครัดจนเกินไป
จุดของ Dijkstra ไม่ใช่ “เขียนโค้ดแบบโบราณ” แต่ว่า “เขียนโค้ดที่คุณ อธิบายได้” คุณสามารถยอมรับแนวคิดนี้โดยไม่เปลี่ยนทุกฟีเจอร์ให้เป็นการพิสูจน์อย่างเป็นทางการ
เปลี่ยนหลักการเป็นนิสัยประจำวัน
เริ่มจากทางเลือกที่ทำให้การให้เหตุผลถูกและถูกค่าใช้จ่ายต่ำ:
- ชอบการไหลควบคุมเรียบง่าย: ฟังก์ชันเล็กๆ หลายฟังก์ชัน มากกว่าฟังก์ชันกิ่งเดียวหลายเงื่อนไข
- ลด side effects: เก็บการมิวเทตไว้ใกล้จุดที่ต้องการ และอย่าให้ฟังก์ชันเปลี่ยนสถานะ global เงียบๆ
- ใช้สัญญาชัดเจน: กำหนดอินพุต เอาต์พุต และพฤติกรรมข้อผิดพลาดอย่างชัด (ผ่านชนิด ชื่อ หรือคอมเมนต์)
เฮอร์ริสติกที่ดี: ถ้าคุณสรุปสิ่งที่ฟังก์ชันรับประกันเป็นหนึ่งประโยคไม่ได้ มันอาจทำงานมากเกินไป
การอัปเกรดโครงสร้างเล็กๆ (ไม่ต้องรีไรท์)
คุณไม่จำเป็นต้องรีแฟกเตอร์ใหญ่ เพิ่มโครงสร้างจากรอยต่อ:
- แยกลูปที่ซับซ้อนเป็นฟังก์ชันชื่อชัด และนิยามสิ่งที่ยังคงเป็นจริงในแต่ละการวนรอบ
- แทนที่เงื่อนไข “มายากล” ด้วย predicate ตั้งชื่อดี (เช่น
isEligibleForRefund) - ห่อการเปลี่ยนสถานะยากไว้ด้านในฟังก์ชันเดียวเพื่อที่ส่วนอื่นจะไม่ใช้ผิด
การอัปเกรดเหล่านี้เป็นแบบขั้นบันได: ลดภาระความคิดสำหรับการเปลี่ยนถัดไป
คำถามรีวิวโค้ดที่ช่วยรักษาความตรงไปตรงมา
เวลารีวิวหรือเขียนการเปลี่ยน ถาม:
- “อะไรต้องเป็นจริงที่นี่?” (invariants, สมมติฐาน, สถานะที่ต้องการ)
- “อะไรเปลี่ยนได้อย่างปลอดภัย?” (ส่วนไหนที่อนุญาตให้เปลี่ยนโดยไม่ทำให้ caller พัง)
ถ้าคนรีวิวตอบไม่ได้อย่างรวดเร็ว โค้ดกำลังส่งสัญญาณว่ามีการพึ่งพาซ่อนอยู่
จดเหตุผล ไม่ใช่แค่ขั้นตอน
คอมเมนต์ที่ทำซ้ำโค้ดมักล้าสมัย เขียน ทำไม โค้ดนี้ถูกต้อง: สมมติฐานสำคัญ กรณีขอบที่คุณป้องกัน และสิ่งที่จะพังถ้าสมมติฐานเปลี่ยน โน้ตสั้นๆ เช่น “Invariant: total เท่ากับผลรวมของไอเท็มที่ประมวลผลแล้ว” มักมีค่ายิ่งกว่าพารากราฟบรรยาย
ถ้าต้องการที่เบาๆ ในการเก็บนิสัยเหล่านี้ รวบรวมเป็นเช็คลิสต์ร่วม (ดู /blog/practical-checklist-for-disciplined-code)
ที่ AI ช่วยสร้างเข้ามาได้อย่างไร (โดยไม่สูญเสียวินัย)
ทีมสมัยใหม่ใช้ AI เพื่อเร่งการส่งมอบ ความเสี่ยงคุ้นเคยคือความเร็ววันนี้กลายเป็นความสับสนวันหน้า หากโค้ดที่สร้างขึ้นอธิบายไม่ได้ วิธีที่เป็นมิตรกับ Dijkstra ในการใช้ AI คือถือมันเป็นตัวเร่งสำหรับ การคิดเชิงโครงสร้าง ไม่ใช่ตัวแทน ตัวอย่างเช่นเมื่อสร้างงานใน Koder.ai—แพลตฟอร์ม vibe-coding ที่สร้างเว็บ แบ็กเอนด์ และแอปมือถือผ่านการแชต—คุณสามารถรักษานิสัย “ให้เหตุผลก่อน” ได้โดยทำให้ prompt และขั้นตอนการรีวิวชัดเจน:
- ขอ สัญญาชัดเจน: “กำหนด preconditions, postconditions, และพฤติกรรมข้อผิดพลาดสำหรับ endpoint นี้”
- ขอ invariants ในฟลูว์มีสถานะ: “อะไรต้องเป็นจริงหลังแต่ละขั้นตอนของ state machine เช็คเอาต์นี้?”
- ใช้ planning mode เพื่อบังคับการแยกงานเป็นชิ้นเล็กๆ ที่ตรวจสอบได้ (โมดูล อินเทอร์เฟซ ความรับผิดชอบ) ก่อนสร้างรายละเอียดการ implement
- พึ่งพา snapshots และ rollback เพื่อให้การเปลี่ยนแปลงเล็กและย้อนกลับได้—สะท้อนวินัยของการแก้ไขเฉพาะจุดและทาง undo ที่ปลอดภัย
แม้จะส่งออกซอร์สโค้ดไปรันที่อื่น หลักการเดียวกันยังใช้: โค้ดที่สร้างโดย AI ควรเป็นโค้ดที่คุณอธิบายได้
เช็คลิสต์ปฏิบัติสำหรับโค้ดที่ถูกต้อง เรียบง่าย และมีวินัย
นี่คือเช็คลิสต์น้ำหนักเบาแบบ “เป็นมิตรกับ Dijkstra” ที่ใช้ระหว่างรีวิว รีแฟกเตอร์ หรือก่อน merge มันไม่เกี่ยวกับการพิสูจน์ทั้งวัน แต่คือการทำให้ความถูกต้องและความชัดเจนเป็นค่าเริ่มต้น
ตรวจสอบตนเองเร็วๆ (โค้ดใหม่และรีแฟกเตอร์)
- อธิบายโค้ดให้เพื่อนร่วมงานภายใน 60 วินาทีได้ไหม? ถ้าอธิบายต้องมีคำว่า “เชื่อใจฉัน” เยอะ ให้ทำให้เรียบง่ายขึ้น
- การไหลการควบคุมชัดเจนไหม? ชอบโค้ดเส้นตรง ลูปและเงื่อนไขเล็กๆ หลีกเลี่ยงการออกก่อนที่กระจัดกระจายและการซ้อนลึก
- มี preconditions และ postconditions อะไรบ้าง? เขียนมันในคอมเมนต์ docstring หรือชื่อฟังก์ชัน ถ้าพูดไม่ได้ ฟังก์ชันอาจทำมากเกินไป
- แต่ละฟังก์ชันทำงานเดียวและมีขอบเขตชัดไหม? อินพุตเข้า เอาต์พุตออก—พึ่ง global น้อยที่สุด
- อะไรเป็น invariant ที่ทำให้ลูปนี้ซื่อสัตย์? แม้โน้ตหนึ่งบรรทัดเช่น “
totalเท่ากับผลรวมของไอเท็มที่ประมวลผลแล้ว” ก็ป้องกันบั๊กซับซ้อนได้ - มีทริกฉลาดเกินจำเป็นไหม? ถ้าโค้ดต้องมีไกด์ทัวร์ แปลว่าคุณกำลังสะสมหนี้ความเฉลียวฉลาด
สิ่งที่วัดเชิงคุณภาพได้
- ง่ายต่อการอธิบาย: คนที่ไม่คุ้นกับโมดูลบอกได้ไหมว่ามันทำอะไรและทำไมถูกต้อง
- ง่ายต่อการทดสอบ: กรณีขอบทดสอบได้ง่ายไหม หรือจำเป็นต้องตั้งค่าซับซ้อนและ mocking เยอะ
- ความเสี่ยงจากการเปลี่ยน: เมื่อข้อกำหนดเปลี่ยน คุณคาดได้ไหมว่าจะพังตรงไหน? ถ้าทุกการเปลี่ยนทำให้กลัว แปลว่าขอบเขตรั่ว
ขั้นตอนถัดไปเชิงปฏิบัติ
เลือกโมดูลรกหนึ่งอันและ ปรับโครงสร้างการไหลก่อน:
- แยกฟังก์ชันเล็กๆ ที่มีชื่อชัด
- แทนที่สาขาซับซ้อนด้วยเคสที่ชัดเจนและง่ายต่อการอ่าน
- ย้ายกรณีพิเศษไปขอบ (การตรวจอินพุต การ return เร็ว)
แล้วเพิ่มการทดสอบรอบขอบเขตใหม่ไม่กี่ชิ้น ถ้าต้องการรูปแบบเพิ่มเติม ลองดูโพสต์ที่เกี่ยวข้องใน /blog.
คำถามที่พบบ่อย
ทำไม Dijkstra ยังมีความสำคัญกับทีมซอฟต์แวร์สมัยใหม่?
เพราะเมื่อโค้ดเบสโตขึ้น คอขวดหลักจะกลายเป็นการ เข้าใจ ไม่ใช่แค่การพิมพ์โค้ด ความเน้นของ Dijkstra ที่ให้ความสำคัญกับการควบคุมการไหลที่คาดเดาได้ สัญญา (contracts) ชัดเจน และความถูกต้อง ช่วยลดความเสี่ยงที่การเปลี่ยนแปลงเล็กๆ จะทำให้พฤติกรรมโดยรวมเปลี่ยนไป ซึ่งเป็นสาเหตุสำคัญที่ทำให้ทีมช้าลงเมื่อเวลาผ่านไป
ที่นี่คำว่า “scale” หมายถึงอะไร—ประสิทธิภาพหรืออย่างอื่น?
ที่นี่ “ขยายตัว” ไม่ได้หมายถึงประสิทธิภาพเพียงอย่างเดียว แต่หมายถึง ความยุ่งยากที่เพิ่มทวีคูณ ตัวอย่างแรงกดดัน:
- ฟีเจอร์และกรณีขอบที่มากขึ้น
- ผู้ร่วมพัฒนาที่มากขึ้นและการส่งมอบระหว่างคน
- การผสานรวมและรูปแบบความล้มเหลวที่หลากหลาย
- เวลาที่มากขึ้นและการตัดสินใจตกทอด
พลังเหล่านี้ทำให้การคิดและความคาดเดาได้มีค่ามากกว่าความเฉลียวฉลาดเฉพาะหน้า
การเขียนโปรแกรมเชิงโครงสร้างคืออะไร ในเชิงปฏิบัติ?
การเขียนโปรแกรมเชิงโครงสร้างเน้นชุดโครงสร้างควบคุมที่ชัดเจน:
- ลำดับ (ทำ A แล้ว B)
- การเลือก (
if/else,switch) - การทำซ้ำ (
for,while)
เป้าหมายไม่ใช่ความเข้มงวด แต่เป็นการทำให้เส้นทางการทำงานตามโปรแกรมอ่านได้ง่าย จนครอบคลุมพฤติกรรม ตรวจสอบ และดีบักได้โดยไม่ต้อง “เทเลพอร์ต” ไปรอบๆ โค้ด
ทำไมการไหลที่เป็นสปาเก็ตตี้ (เช่น `goto` แบบไม่จำกัด) ถึงเป็นปัญหาการบำรุงรักษา?
ปัญหาคือการกระโดดแบบ ไม่จำกัด ที่สร้างเส้นทางการทำงานที่คาดเดายากและสถานะที่ไม่ชัดเจน เมื่อการไหลของการควบคุมยุ่งเหยิง นักพัฒนาต้องเสียเวลาตอบคำถามพื้นฐานเช่น “เรามาถึงจุดนี้ได้ยังไง?” และ “ตัวแปรนี้อยู่ในสถานะไหน?”
รูปแบบร่วมสมัยที่คล้ายกันคือการมีเงื่อนไขซ้อนลึก การออกจากฟังก์ชันกระจัดกระจาย และการเปลี่ยนสถานะแบบเงียบๆ ที่ทำให้ติดตามพฤติกรรมได้ยาก
นิยามเชิงปฏิบัติของความถูกต้องสำหรับซอฟต์แวร์ผลิตภัณฑ์คืออะไร?
ความถูกต้องคือ “ฟีเจียสงบ” ที่ผู้ใช้พึ่งพา: ระบบทำในสิ่งที่สัญญาไว้สม่ำเสมอ และเมื่อผิดพลาดก็ล้มเหลวในทางที่อธิบายได้ มันคือความแตกต่างระหว่าง “มันใช้ได้ในตัวอย่างไม่กี่เคส” กับ “มันยังใช้ได้หลังการรีแฟกเตอร์ การผสาน รวมถึงกรณีขอบ”
ทำไมบักเล็กๆ ถึงมีต้นทุนสูงในระบบขนาดใหญ่?
เพราะการพึ่งพาซึ่งกันและกันขยายความผิดพลาด สถานะผิดเล็กน้อยหรือตัวเลขขอบเขตผิดหนึ่งค่า มักถูกคัดลอก แคช รีเทรีย หรือถูก “แก้ทาง” ในโมดูลอื่นๆ เมื่อเวลาผ่านไป ทีมเลิกถามว่า “อะไรเป็นความจริง?” และเริ่มถามว่า “ปกติแล้วเกิดอะไรขึ้น?” ซึ่งทำให้การตอบเหตุการณ์และการเปลี่ยนแปลงมีความยากขึ้น
ที่นี่ความเรียบง่ายหมายถึงอะไร (และไม่หมายถึงอะไร)?
ความเรียบง่ายคือการมี แนวคิดไม่กี่อย่างที่กำลังดำเนินอยู่พร้อมกัน: การไหลของข้อมูลชัดเจน การไหลของการควบคุมชัดเจน และความรับผิดชอบชัดเจน มันไม่ใช่การลดบรรทัดโค้ดโดยไม่คิด ไม่ใช่ลูกเล่นซ้อนหนา หรือการใช้ abstraction หนักๆ
การทดสอบที่ดี: ถ้าพฤติกรรมยังคาดเดาได้เมื่อความต้องการเปลี่ยน แปลว่าเรียบง่ายพอ ถ้าทุกกรณีใหม่เพิ่มคำว่า “ยกเว้น…” คุณกำลังสะสมความซับซ้อนโดยไม่ได้ตั้งใจ
แนวคิด invariants ช่วยในโค้ดประจำวันอย่างไร โดยไม่ต้องพิสูจน์อย่างเป็นทางการ?
อิล invariant คือข้อเท็จจริงที่ต้องคงอยู่ในขณะที่รันโค้ด โดยเฉพาะในลูป ตัวอย่างง่าย: ขณะรวมราคาสินค้า total ควรเท่ากับผลรวมของไอเท็มที่ประมวลผลแล้ว ถ้าข้อความนี้เป็นจริงทุกขั้นตอน ผลลัพธ์เมื่อจบลูปก็เชื่อถือได้
การใช้แบบน้ำหนักเบา:
- เขียนคอมเมนต์หนึ่งบรรทัดเหนือลูป (เช่น “
totalเท่ากับผลรวมของไอเท็มที่ประมวลผลแล้ว”) - แก้โค้ดจนสามารถรักษาข้อเท็จจริงนั้นได้ในแต่ละรอบ
- ใส่ assertion ถ้าทำได้และมีประโยชน์
วิธีนี้ทำให้การแก้ไขในอนาคตปลอดภัยขึ้นเพราะคนถัดไปรู้ว่าห้ามทำลายข้อเท็จจริงนี้
ทีมควรบาลานซ์การทดสอบกับการให้เหตุผลอย่างไร?
การทดสอบค้นหาบั๊กด้วยการลองตัวอย่าง ส่วนการให้เหตุผล (reasoning) ป้องกันชนิดของข้อบกพร่องโดยการทำตรรกะให้ชัดเจน
แนวทางปฏิบัติที่สมดุล:
- ทดสอบอย่างกว้างขวาง
- เพิ่ม assertion และสัญญา (pre/postconditions) ในจุดที่ความล้มเหลวมีต้นทุนสูง
สรุป: ทดสอบกว้าง + การให้เหตุผลเชิงลึกเฉพาะจุด ที่สำคัญ
มีวิธีแบบค่อยเป็นค่อยไปที่จะนำแนวคิดของ Dijkstra ไปใช้โดยไม่หัวรุนแรงไหม?
เริ่มจากการเปลี่ยนแปลงเล็กๆ ที่ลดภาระความคิด:
- แยกฟังก์ชันเล็กๆ และระบุอินพุต/เอาต์พุต
- เปลี่ยนเงื่อนไขลึกลับเป็น predicate ที่ตั้งชื่อชัด (เช่น
isEligibleForRefund) - ห่อการเปลี่ยนสถานะที่ซับซ้อนไว้หลังฟังก์ชันเดียว
- ใส่คอมเมนต์สั้นๆ ที่อธิบาย เหตุผลว่าทำไมโค้ดนี้ถูกต้อง (invariants, สมมติฐาน)
การอัปเกรดเหล่านี้ทีละน้อยจะช่วยให้การเปลี่ยนแปลงครั้งถัดไปถูกลงและปลอดภัยกว่า