| Topic 24 | โปรแกรมที่ตายแล้วไม่โกหก |
|---|
คุณเคยสังเกตไหมว่าบางครั้งคนอื่นสามารถสังเกตเห็นว่ามีบางอย่างผิดปกติกับคุณได้ก่อนที่คุณจะรู้ตัวเองเสียอีก? เรื่องนี้ก็เหมือนกับโค้ดของคนอื่น ถ้ามีบางอย่างเริ่มผิดเพี้ยนในโปรแกรมของเรา บางครั้งไลบรารีหรือรูทีนของ framework ก็เป็นตัวที่จับได้ก่อน บางทีเราอาจส่งค่า nil เข้าไป หรือส่ง list ที่ว่างเปล่า บางทีอาจมี key ที่หายไปในแฮชนั้น หรือค่าที่เราคิดว่าเป็นแฮชกลับกลายเป็น list แทน บางทีอาจเกิด network error หรือ filesystem error ที่เราไม่ได้ดักจับไว้ แล้วเราก็ได้ข้อมูลที่ว่างเปล่าหรือเสียหายมา ข้อผิดพลาดทาง logic เมื่อหลายล้านคำสั่งก่อนหน้านี้ทำให้ตัวเลือกของ case statement ไม่ใช่ 1, 2, หรือ 3 ตามที่คาดไว้อีกต่อไป เราจะไปเจอกับเคส default โดยไม่คาดคิด นั่นก็เป็นอีกเหตุผลหนึ่งว่าทำไม case/switch statement ทุกตัวจึงต้องมี default clause: เราอยากรู้เมื่อสิ่งที่ “เป็นไปไม่ได้” เกิดขึ้นจริงครับ
มันง่ายมากที่จะตกหลุมพราง “มันเกิดขึ้นไม่ได้หรอก” พวกเราส่วนใหญ่เคยเขียนโค้ดที่ไม่ได้ตรวจสอบว่าไฟล์ถูกปิดสำเร็จหรือไม่ หรือว่า trace statement ถูกเขียนออกไปตามที่คาดไว้หรือเปล่า และถ้าทุกอย่างเท่าเทียมกัน ก็มีแนวโน้มว่าเราไม่จำเป็นต้องตรวจสอบมันเลย—โค้ดที่ว่านี้คงไม่มีทางล้มเหลวภายใต้เงื่อนไขปกติ แต่เรากำลังเขียนโค้ดแบบตั้งรับ (defensive) เรากำลังทำให้แน่ใจว่าข้อมูลเป็นสิ่งที่เราคิดว่ามันเป็น ว่าโค้ดที่อยู่ใน production คือโค้ดที่เราคิดว่ามันเป็น เรากำลังตรวจสอบว่า dependency เวอร์ชันที่ถูกต้องถูกโหลดขึ้นมาจริงๆ ครับ
ข้อผิดพลาดทุกตัวให้ข้อมูลกับคุณเสมอ คุณอาจโน้มน้าวตัวเองว่า error นี้เกิดขึ้นไม่ได้หรอก แล้วเลือกที่จะเพิกเฉยต่อมัน แต่ Pragmatic Programmer จะบอกตัวเองว่าถ้ามี error เกิดขึ้น แสดงว่าต้องมีบางอย่างที่แย่มากๆ เกิดขึ้นแล้ว อย่าลืมอ่าน Error Message ให้ดีๆ (ดู Coder in a Strange Land (โค้ดเดอร์ในดินแดนแปลกหน้า) )
Catch and Release Is for Fish (จับแล้วปล่อยเหมาะกับการตกปลาเท่านั้น)
นักพัฒนาบางคนรู้สึกว่าเป็น style ที่ดีที่จะ catch หรือ rescue exception ทุกตัว แล้ว raise มันออกไปใหม่หลังจากเขียนข้อความบางอย่าง โค้ดของพวกเขาเต็มไปด้วยอะไรแบบนี้ (ซึ่ง statement raise เปล่าๆ จะ raise exception ปัจจุบันซ้ำอีกครั้ง):
| | try do |
|---|---|
| | add_score_to_board(score); |
| | rescue InvalidScore |
| | Logger.error( " Can't add invalid score. Exiting" ); |
| | raise |
| | rescue BoardServerDown |
| | Logger.error( " Can't add score: board is down. Exiting" ); |
| | raise |
| | rescue StaleTransaction |
| | Logger.error( " Can't add score: stale transaction. Exiting" ); |
| | raise |
| | end |
นี่คือวิธีที่ Pragmatic Programmer จะเขียนโค้ดชิ้นนี้:
| | add_score_to_board(score); |
|---|
เราชอบแบบนี้มากกว่าด้วยสองเหตุผล อย่างแรก โค้ดของแอปพลิเคชันไม่ถูกบดบังด้วยการจัดการ error อย่างที่สอง และอาจสำคัญกว่าด้วยซ้ำ คือโค้ดมี coupling น้อยลง ในตัวอย่างแบบยืดยาวข้างต้น เราต้องลิสต์ exception ทุกตัวที่เมธอด add_score_to_board อาจ raise ออกมา ถ้าคนที่เขียนเมธอดนั้นเพิ่ม exception ตัวใหม่เข้ามา โค้ดของเราก็จะล้าสมัยไปแบบเงียบๆ ทันที ในเวอร์ชันที่สองซึ่ง pragmatic กว่า exception ตัวใหม่จะถูกส่งต่อ (propagate) ขึ้นไปโดยอัตโนมัติครับ
| Tip 38 | Crash แต่เนิ่นๆ |
|---|
Crash, Don't Trash (จะพังก็พังไปเลย อย่าทำข้อมูลพัง)
หนึ่งในประโยชน์ของการตรวจจับปัญหาให้เร็วที่สุดเท่าที่จะทำได้ก็คือคุณสามารถ crash ได้เร็วขึ้น และการ crash ก็มักจะเป็นสิ่งที่ดีที่สุดที่คุณทำได้ ทางเลือกอื่นอาจเป็นการทำงานต่อไป เขียนข้อมูลที่เสียหายลงใน database สำคัญบางตัว หรือสั่งให้เครื่องซักผ้าเข้าสู่รอบปั่นครั้งที่ยี่สิบติดต่อกัน
ภาษา Erlang และ Elixir ยึดถือปรัชญานี้ Joe Armstrong ผู้คิดค้นภาษา Erlang และผู้เขียนหนังสือ Programming Erlang: Software for a Concurrent World [Arm07] มักถูกยกคำพูดที่ว่า “Defensive programming เป็นการเสียเวลาเปล่า ปล่อยให้มัน crash ไปเลย!” ในสภาพแวดล้อมแบบนี้ โปรแกรมถูกออกแบบมาให้ล้มเหลวได้ แต่ความล้มเหลวนั้นถูกจัดการด้วย supervisor supervisor คือตัวที่รับผิดชอบการรันโค้ด และรู้ว่าต้องทำอะไรในกรณีที่โค้ดล้มเหลว ซึ่งอาจรวมถึงการเก็บกวาดหลังจากนั้น การรีสตาร์ตมันใหม่ และอื่นๆ แล้วถ้า supervisor เองล้มเหลวล่ะจะเกิดอะไรขึ้น? supervisor ของมันเองก็จะจัดการเหตุการณ์นั้น นำไปสู่การออกแบบที่ประกอบด้วย supervisor tree เทคนิคนี้มีประสิทธิภาพมาก และช่วยอธิบายได้ว่าทำไมภาษาเหล่านี้ถึงถูกใช้ในระบบที่ต้องการความพร้อมใช้งานสูงและทนทานต่อความล้มเหลวครับ
ในสภาพแวดล้อมอื่น การออกจากโปรแกรมที่กำลังรันอยู่เฉยๆ อาจไม่เหมาะสม คุณอาจถือครอง resource ที่อาจไม่ถูกปล่อยคืน หรือคุณอาจต้องเขียน log message เก็บกวาด transaction ที่ยังเปิดค้างอยู่ หรือโต้ตอบกับ process อื่นๆ
อย่างไรก็ตาม หลักการพื้นฐานก็ยังคงเดิม—เมื่อโค้ดของคุณค้นพบว่ามีบางอย่างที่ควรจะเป็นไปไม่ได้เกิดขึ้นจริง โปรแกรมของคุณก็ไม่สามารถทำงานต่อไปได้อย่างน่าเชื่อถืออีกต่อไป ทุกอย่างที่มันทำต่อจากจุดนี้ล้วนน่าสงสัยทั้งสิ้น ดังนั้นให้ยุติมันโดยเร็วที่สุดครับ
โปรแกรมที่ตายไปแล้วมักสร้างความเสียหายน้อยกว่าโปรแกรมที่พิการแต่ยังทำงานต่อไปครับ
Related Sections Include (หัวข้อที่เกี่ยวข้อง)
- Topic 20, Debugging (การดีบัก)
- Topic 23, Design by Contract (การออกแบบด้วยสัญญา)
- Topic 25, Assertive Programming (การเขียนโปรแกรมแบบยืนยันเงื่อนไข)
- Topic 26, How to Balance Resources (จะจัดสรร Resource อย่างสมดุลได้อย่างไร)
- Topic 43, Stay Safe Out There (ปลอดภัยไว้ก่อนเมื่อออกไปข้างนอก)