| Topic 38 | การเขียนโปรแกรมด้วยความบังเอิญ |
|---|
คุณเคยดูหนังสงครามขาวดำเก่า ๆ ไหมครับ ทหารที่เหนื่อยล้าคนหนึ่งค่อย ๆ คลานออกมาจากพุ่มไม้อย่างระมัดระวัง ข้างหน้าเป็นพื้นที่โล่ง: มีทุ่นระเบิดอยู่หรือเปล่า หรือว่าข้ามไปได้อย่างปลอดภัย ไม่มีสัญญาณบ่งบอกว่าเป็นทุ่งทุ่นระเบิดเลย ไม่มีป้ายเตือน ไม่มีลวดหนาม ไม่มีหลุมระเบิด ทหารคนนั้นใช้ดาบปลายปืนจิ้มพื้นข้างหน้าและยักไหล่ด้วยความคาดว่าจะเกิดระเบิดขึ้น แต่ก็ไม่มีอะไรเกิดขึ้น เขาจึงค่อย ๆ เดินลุยไปในทุ่งนั้นสักพัก พลางจิ้มและสำรวจไปเรื่อย ๆ ในที่สุด เมื่อมั่นใจว่าพื้นที่นี้ปลอดภัยแล้ว เขาก็ยืดตัวขึ้นและเดินอย่างภาคภูมิใจไปข้างหน้า ก่อนจะถูกระเบิดจนแหลกละเอียด
การสำรวจหาทุ่นระเบิดครั้งแรกของทหารคนนั้นไม่พบอะไรเลย แต่นั่นเป็นเพียงเรื่องบังเอิญเท่านั้น เขาถูกนำไปสู่ข้อสรุปที่ผิด—และผลลัพธ์ก็เลวร้ายครับ
ในฐานะนักพัฒนา เราก็ทำงานอยู่ในทุ่งทุ่นระเบิดเช่นกันครับ มีกับดักนับร้อยที่รอจับเราอยู่ทุกวัน เมื่อนึกถึงเรื่องราวของทหารคนนั้น เราควรระวังการด่วนสรุปที่ผิดพลาด เราควรหลีกเลี่ยงการเขียนโปรแกรมด้วยความบังเอิญ—การพึ่งพาโชคและความสำเร็จที่เกิดขึ้นโดยไม่ตั้งใจ—แล้วหันมา เขียนโปรแกรมอย่างตั้งใจ แทน
How to Program by Coincidence (จะเขียนโปรแกรมด้วยความบังเอิญได้อย่างไร)
สมมติว่า Fred ได้รับมอบหมายงานเขียนโปรแกรมชิ้นหนึ่ง Fred พิมพ์โค้ดบางส่วน ลองรัน แล้วดูเหมือนจะทำงานได้ Fred พิมพ์โค้ดเพิ่มอีก ลองรัน ก็ยังดูเหมือนจะทำงานได้ หลังจากเขียนโค้ดแบบนี้ไปหลายสัปดาห์ โปรแกรมก็หยุดทำงานกะทันหัน และหลังจากพยายามแก้อยู่หลายชั่วโมง เขาก็ยังไม่รู้ว่าทำไม Fred อาจต้องเสียเวลาไล่ตามหาโค้ดส่วนนี้ไปเรื่อย ๆ โดยไม่มีทางแก้ได้เลย ไม่ว่าเขาจะทำอะไรก็ตาม มันก็ดูเหมือนจะไม่มีวันทำงานถูกต้องเสียที
Fred ไม่รู้ว่าทำไมโค้ดถึงล้มเหลว เพราะ เขาไม่รู้ตั้งแต่แรกอยู่แล้วว่าทำไมมันถึงทำงานได้ มันดูเหมือนจะทำงานได้ จากการ “ทดสอบ” อันจำกัดที่ Fred ทำ แต่นั่นก็แค่เรื่องบังเอิญเท่านั้น ด้วยความมั่นใจที่ผิด ๆ Fred จึงบุกหน้าต่อไปสู่หายนะ คนฉลาดส่วนใหญ่คงรู้จักใครสักคนที่เป็นแบบ Fred แต่ เรา รู้ดีกว่านั้น เราไม่พึ่งพาความบังเอิญ—ใช่ไหมครับ
บางครั้งเราก็อาจจะพึ่งพามันอยู่เหมือนกันครับ บางทีมันก็ง่ายมากที่จะสับสนระหว่างความบังเอิญอันน่ายินดีกับแผนการที่ตั้งใจไว้ ลองมาดูตัวอย่างสักสองสามข้อกันครับ
Accidents of Implementation (อุบัติเหตุจากการ Implement)
อุบัติเหตุจากการ implement คือสิ่งที่เกิดขึ้นเพียงเพราะโค้ดตอนนี้ถูกเขียนไว้แบบนั้น สุดท้ายคุณก็ไปพึ่งพา error หรือ boundary condition ที่ไม่มีการบันทึกไว้เป็นเอกสาร
สมมติว่าคุณเรียก routine หนึ่งด้วยข้อมูลที่ผิดพลาด routine นั้นตอบสนองออกมาในรูปแบบหนึ่ง และคุณก็เขียนโค้ดโดยอิงจากการตอบสนองนั้น แต่ผู้เขียนไม่ได้ตั้งใจให้ routine ทำงานแบบนั้นเลย—ไม่เคยถูกพิจารณาด้วยซ้ำ พอ routine นั้นถูก “แก้” โค้ดของคุณก็อาจพังได้ ในกรณีที่สุดโต่งที่สุด routine ที่คุณเรียกอาจไม่ได้ถูกออกแบบมาให้ทำสิ่งที่คุณต้องการเลยด้วยซ้ำ แต่มัน ดูเหมือน จะทำงานได้โอเค การเรียกสิ่งต่าง ๆ ผิดลำดับ หรือผิดบริบท ก็เป็นปัญหาในลักษณะเดียวกันครับ
ดูเหมือนตรงนี้ Fred กำลังพยายามอย่างสุดชีวิตเพื่อให้อะไรบางอย่างแสดงขึ้นมาบนหน้าจอ โดยใช้ GUI rendering framework ตัวหนึ่ง:
| | paint(); |
|---|---|
| | invalidate(); |
| | validate(); |
| | revalidate(); |
| | repaint(); |
| | paintImmediately(); |
แต่ routine เหล่านี้ไม่เคยถูกออกแบบมาให้เรียกแบบนี้เลย แม้มันจะดูเหมือนทำงานได้ แต่จริง ๆ แล้วก็แค่เรื่องบังเอิญเท่านั้น
ซ้ำร้ายไปกว่านั้น เมื่อฉากสุดท้ายถูกวาดออกมาได้สำเร็จ Fred ก็จะไม่ย้อนกลับไปเอาการเรียกที่ไม่จำเป็นออก “ตอนนี้มันทำงานได้แล้ว อย่าไปยุ่งกับมันดีกว่า….”
มันง่ายมากที่จะถูกความคิดแบบนี้หลอก ทำไมคุณถึงต้องเสี่ยงไปยุ่งกับสิ่งที่ทำงานได้อยู่แล้วด้วยล่ะ ที่จริงเราคิดเหตุผลได้หลายข้อเลยครับ:
-
มันอาจไม่ได้ทำงานได้จริง ๆ—มันอาจแค่ดูเหมือนทำงานได้เท่านั้น
-
boundary condition ที่คุณพึ่งพาอยู่อาจเป็นแค่เรื่องบังเอิญ ในสถานการณ์ที่ต่างออกไป (ความละเอียดหน้าจอที่ต่างกัน จำนวน CPU core ที่มากขึ้น) มันอาจทำงานต่างออกไปก็ได้
-
พฤติกรรมที่ไม่มีการบันทึกไว้อาจเปลี่ยนไปเมื่อ library ออกเวอร์ชันถัดไป
-
การเรียกที่เพิ่มเข้ามาโดยไม่จำเป็นทำให้โค้ดของคุณทำงานช้าลง
-
การเรียกที่เพิ่มเข้ามาก็เพิ่มความเสี่ยงที่จะสร้างบั๊กใหม่ ๆ ขึ้นมาเองด้วย
สำหรับโค้ดที่คุณเขียนให้คนอื่นเรียกใช้ หลักการพื้นฐานของการทำ modularization ที่ดี และการซ่อน implementation ไว้หลัง interface ที่เล็กและมีเอกสารกำกับอย่างดี ล้วนช่วยได้ครับ contract ที่ระบุไว้อย่างชัดเจน (ดู Topic 23, Design by Contract (การออกแบบด้วยสัญญา) ) ก็ช่วยลดความเข้าใจผิดได้เช่นกัน
สำหรับ routine ที่คุณเรียกใช้ จงพึ่งพาเฉพาะพฤติกรรมที่มีเอกสารระบุไว้เท่านั้น ถ้าทำแบบนั้นไม่ได้ไม่ว่าจะด้วยเหตุผลใดก็ตาม ก็จงบันทึกข้อสันนิษฐานของคุณไว้ให้ชัดเจน
Close Enough Isn't (ใกล้เคียงไม่ได้แปลว่าใช่)
ครั้งหนึ่งเราเคยทำงานในโปรเจกต์ขนาดใหญ่ที่รายงานข้อมูลซึ่งส่งมาจากอุปกรณ์เก็บข้อมูลฮาร์ดแวร์จำนวนมากในพื้นที่จริง อุปกรณ์เหล่านี้กระจายอยู่ในหลายรัฐและหลาย time zone และด้วยเหตุผลด้าน logistics และประวัติศาสตร์หลายอย่าง แต่ละอุปกรณ์จึงถูกตั้งค่าให้ใช้เวลาท้องถิ่น [50] ผลจากการตีความ time zone ที่ขัดแย้งกันและความไม่สอดคล้องกันของนโยบาย Daylight Savings Time ทำให้ผลลัพธ์แทบจะผิดอยู่ตลอด แต่ผิดไปแค่หนึ่งหน่วยเท่านั้น นักพัฒนาในโปรเจกต์นี้จึงติดนิสัยแค่บวกหนึ่งหรือลบหนึ่งเพื่อให้ได้คำตอบที่ถูกต้อง โดยให้เหตุผลว่ามันผิดไป แค่ หนึ่งหน่วยในสถานการณ์นี้เท่านั้น แล้ว function ถัดไปก็จะเห็นค่านั้นผิดไปหนึ่งหน่วยในทิศทางตรงกันข้าม แล้วก็เปลี่ยนมันกลับอีกที
แต่ความจริงที่ว่ามันผิดไป “แค่” หนึ่งหน่วยในบางครั้งนั้นเป็นเรื่องบังเอิญ ซึ่งบดบังข้อบกพร่องพื้นฐานที่ลึกกว่านั้นเอาไว้ เพราะไม่มีโมเดลจัดการเรื่องเวลาที่ถูกต้อง code base ขนาดใหญ่ทั้งหมดจึงค่อย ๆ เสื่อมลงเมื่อเวลาผ่านไป จนกลายเป็นกองคำสั่ง +1 และ -1 ที่ยุ่งเหยิงจนแก้ไขไม่ไหว สุดท้ายแล้วไม่มีส่วนไหนถูกต้องเลย และโปรเจกต์นี้ก็ถูกยกเลิกไปครับ
Phantom Patterns (รูปแบบลวงตา)
มนุษย์ถูกออกแบบมาให้มองเห็นรูปแบบและสาเหตุ แม้ในเวลาที่มันเป็นแค่เรื่องบังเอิญก็ตาม ตัวอย่างเช่น ผู้นำรัสเซียมักสลับกันระหว่างหัวล้านและผมดกอยู่เสมอ: ผู้นำรัสเซียที่หัวล้าน (หรือกำลังจะล้านอย่างเห็นได้ชัด) มักขึ้นดำรงตำแหน่งต่อจากคนที่ไม่หัวล้าน (“ผมดก”) และสลับกันไปมาแบบนี้มานานเกือบ 200 ปีแล้วครับ [51]
แต่ในขณะที่คุณคงไม่เขียนโค้ดที่ขึ้นอยู่กับว่าผู้นำรัสเซียคนถัดไปจะหัวล้านหรือผมดก ในบางสาขาเราก็คิดแบบนั้นอยู่ตลอดเวลา นักพนันมักจินตนาการเห็นรูปแบบในตัวเลขลอตเตอรี เกมลูกเต๋า หรือรูเล็ตต์ ทั้งที่จริง ๆ แล้วเหตุการณ์เหล่านี้เป็นอิสระต่อกันทางสถิติ ในโลกการเงิน การเทรดหุ้นและพันธบัตรก็เต็มไปด้วยความบังเอิญในลักษณะเดียวกัน แทนที่จะเป็นรูปแบบที่จับต้องได้จริง
log file ที่แสดง error เป็นระยะทุก ๆ 1,000 requests อาจเป็น race condition ที่วินิจฉัยได้ยาก หรืออาจเป็นแค่บั๊กธรรมดา ๆ ก็ได้ test ที่ดูเหมือนจะผ่านบนเครื่องของคุณแต่ไม่ผ่านบน server อาจบ่งชี้ถึงความแตกต่างระหว่างสองสภาพแวดล้อม หรือบางทีก็อาจแค่เป็นเรื่องบังเอิญเท่านั้น
อย่าสันนิษฐานเอาเอง จงพิสูจน์มันครับ
Accidents of Context (อุบัติเหตุจาก Context)
“อุบัติเหตุจาก context” ก็เกิดขึ้นได้เช่นกันครับ สมมติว่าคุณกำลังเขียน utility module อยู่ แค่เพราะตอนนี้คุณกำลังเขียนโค้ดสำหรับสภาพแวดล้อมแบบ GUI จำเป็นหรือที่ module นี้จะต้องพึ่งพาการมีอยู่ของ GUI คุณกำลังพึ่งพาผู้ใช้ที่พูดภาษาอังกฤษอยู่หรือเปล่า พึ่งพาผู้ใช้ที่อ่านออกเขียนได้หรือเปล่า แล้วยังมีอะไรอีกที่คุณกำลังพึ่งพาอยู่ ทั้งที่ไม่มีอะไรรับประกันได้เลย
คุณกำลังพึ่งพาว่า current directory เขียนได้อยู่หรือเปล่า พึ่งพาว่ามี environment variable หรือ configuration file บางตัวอยู่หรือเปล่า พึ่งพาว่าเวลาบน server ถูกต้องแม่นยำ—ในระดับความคลาดเคลื่อนเท่าไร คุณกำลังพึ่งพาความพร้อมใช้งานและความเร็วของเครือข่ายอยู่หรือเปล่าครับ
เมื่อคุณคัดลอกโค้ดมาจากคำตอบแรกที่เจอบนอินเทอร์เน็ต คุณแน่ใจหรือเปล่าว่า context ของคุณเหมือนกัน หรือว่าคุณกำลังสร้างโค้ดแบบ “cargo cult” ที่เลียนแบบแค่รูปแบบโดยไม่มีเนื้อหาจริง ๆ อยู่ข้างใน? [52]
การเจอคำตอบที่บังเอิญเข้ากันได้พอดี ไม่เหมือนกับการเจอคำตอบที่ถูกต้องนะครับ
| Tip 62 | อย่าเขียนโปรแกรมด้วยความบังเอิญ |
|---|
Implicit Assumptions (ข้อสันนิษฐานโดยนัย)
ความบังเอิญสามารถชักนำให้หลงทางได้ในทุกระดับ—ตั้งแต่การสร้าง requirement ไปจนถึงการ test การ test นั้นเสี่ยงต่อความสัมพันธ์เชิงเหตุผลที่ผิดและผลลัพธ์ที่เกิดจากความบังเอิญเป็นพิเศษ มันง่ายมากที่จะสันนิษฐานว่า X เป็นสาเหตุของ Y แต่อย่างที่เราพูดไว้ใน Topic 20, Debugging (การดีบัก) : อย่าสันนิษฐานเอาเอง จงพิสูจน์มัน
ในทุกระดับ ผู้คนทำงานโดยมีข้อสันนิษฐานมากมายอยู่ในใจ—แต่ข้อสันนิษฐานเหล่านี้แทบไม่เคยถูกบันทึกไว้เป็นเอกสาร และมักขัดแย้งกันระหว่างนักพัฒนาแต่ละคน ข้อสันนิษฐานที่ไม่ได้อิงจากข้อเท็จจริงที่ยืนยันได้แน่ชัด คือต้นตอของหายนะในทุกโปรเจกต์
How to Program Deliberately (จะเขียนโปรแกรมอย่างตั้งใจได้อย่างไร)
เราอยากใช้เวลาผลิตโค้ดให้น้อยลง ดักจับและแก้ error ให้เร็วที่สุดเท่าที่จะทำได้ในวงจรการพัฒนา และสร้าง error ให้น้อยลงตั้งแต่แรก มันจะช่วยได้มากถ้าเราสามารถเขียนโปรแกรมอย่างตั้งใจ:
-
รู้ตัวอยู่เสมอว่าคุณกำลังทำอะไรอยู่ Fred ปล่อยให้สถานการณ์ค่อย ๆ เลยเถิดไปจนควบคุมไม่ได้ จนสุดท้ายเขาก็จบเหมือนกบที่ถูกต้ม ในตัวอย่างนี้ .
-
คุณสามารถอธิบายโค้ดอย่างละเอียดให้โปรแกรมเมอร์รุ่นน้องฟังได้ไหม ถ้าทำไม่ได้ คุณอาจกำลังพึ่งพาความบังเอิญอยู่
-
อย่าเขียนโค้ดแบบมืดบอด การสร้างแอปพลิเคชันที่คุณไม่เข้าใจอย่างถ่องแท้ หรือใช้เทคโนโลยีที่คุณไม่เข้าใจ มีแนวโน้มสูงที่คุณจะโดนความบังเอิญกัดเอา ถ้าคุณไม่แน่ใจว่าทำไมมันถึงทำงานได้ คุณก็จะไม่รู้ว่าทำไมมันถึงล้มเหลวเช่นกัน
-
เดินหน้าจากแผนที่วางไว้ ไม่ว่าแผนนั้นจะอยู่ในหัวของคุณ หลังกระดาษเช็ดปากในบาร์ หรือบนไวท์บอร์ดก็ตาม
-
พึ่งพาเฉพาะสิ่งที่เชื่อถือได้เท่านั้น อย่าพึ่งพาข้อสันนิษฐาน ถ้าคุณบอกไม่ได้ว่าสิ่งนั้นเชื่อถือได้หรือไม่ ให้สันนิษฐานในกรณีที่แย่ที่สุดไว้ก่อน
-
บันทึกข้อสันนิษฐานของคุณไว้เป็นเอกสาร Topic 23, Design by Contract (การออกแบบด้วยสัญญา) , ช่วยทำให้ข้อสันนิษฐานในหัวของคุณชัดเจนขึ้น รวมถึงช่วยสื่อสารมันไปยังคนอื่นได้ด้วย
-
อย่า test แค่โค้ดของคุณ แต่ให้ test ข้อสันนิษฐานของคุณด้วย อย่าเดา จงลองทำจริง ๆ เขียน assertion เพื่อ test ข้อสันนิษฐานของคุณ (ดู Topic 25, Assertive Programming (การเขียนโปรแกรมเชิงยืนยัน) ) ถ้า assertion ของคุณถูกต้อง แสดงว่าคุณได้ปรับปรุงเอกสารในโค้ดของคุณให้ดีขึ้นแล้ว แต่ถ้าคุณพบว่าข้อสันนิษฐานของคุณผิด ก็ให้ถือว่าคุณโชคดีไปเลยครับ
-
จัดลำดับความสำคัญของความพยายามของคุณ ใช้เวลากับส่วนที่สำคัญ ซึ่งส่วนใหญ่แล้วมักเป็นส่วนที่ยากที่สุด ถ้าพื้นฐานหรือ infrastructure ของคุณไม่ถูกต้อง ฟีเจอร์เสริมสวยหรูแค่ไหนก็ไม่มีความหมายอะไรเลย
-
อย่าตกเป็นทาสของประวัติศาสตร์ อย่าปล่อยให้โค้ดที่มีอยู่กำหนดโค้ดในอนาคต โค้ดทุกส่วนสามารถถูกแทนที่ได้ถ้ามันไม่เหมาะสมอีกต่อไป แม้แต่ในโปรแกรมเดียวกัน ก็อย่าปล่อยให้สิ่งที่คุณทำไปแล้วมาจำกัดสิ่งที่คุณจะทำต่อไป—พร้อมที่จะ refactor อยู่เสมอ (ดู Topic 40, Refactoring (การปรับโครงสร้างโค้ด) ) การตัดสินใจนี้อาจส่งผลต่อ schedule ของโปรเจกต์ ข้อสันนิษฐานคือผลกระทบนั้นจะน้อยกว่าต้นทุนของการ ไม่ เปลี่ยนแปลงครับ [53]
ดังนั้นครั้งต่อไปที่มีอะไรดูเหมือนจะทำงานได้ แต่คุณไม่รู้ว่าทำไม จงตรวจสอบให้แน่ใจว่ามันไม่ใช่แค่เรื่องบังเอิญครับ
Related Sections Include (หัวข้อที่เกี่ยวข้อง)
- Topic 4, Stone Soup and Boiled Frogs (ซุปหินกับกบต้ม)
- Topic 9, DRY—The Evils of Duplication (DRY—ความชั่วร้ายของการซ้ำซ้อน)
- Topic 23, Design by Contract (การออกแบบด้วยสัญญา)
- Topic 34, Shared State Is Incorrect State (สถานะที่ใช้ร่วมกันคือสถานะที่ผิดพลาด)
- Topic 43, Stay Safe Out There (ระวังตัวไว้ให้ดี)
Exercises (แบบฝึกหัด)
แบบฝึกหัด 25 ( คำตอบที่เป็นไปได้ )
แหล่งข้อมูลจาก vendor ส่ง array ของ tuple ที่แทนคู่ key-value มาให้คุณ โดย key ของ DepositAccount จะเก็บ string หมายเลขบัญชีไว้ใน value ที่คู่กัน:
| | [ |
|---|---|
| | ... |
| | { :DepositAccount , "564-904-143-00" } |
| | ... |
| | ] |
มันทำงานได้อย่างสมบูรณ์แบบตอน test บนโน้ตบุ๊กนักพัฒนาที่มี 4-core และบน build machine ที่มี 12-core แต่บน production server ที่รันอยู่ใน container คุณกลับได้หมายเลขบัญชีที่ผิดอยู่เรื่อย ๆ เกิดอะไรขึ้นครับ
แบบฝึกหัด 26 ( คำตอบที่เป็นไปได้ )
คุณกำลังเขียน autodialer สำหรับแจ้งเตือนด้วยเสียง และต้องจัดการฐานข้อมูลข้อมูลติดต่อ มาตรฐาน ITU ระบุว่าหมายเลขโทรศัพท์ต้องมีความยาวไม่เกิน 15 หลัก คุณจึงเก็บหมายเลขโทรศัพท์ของผู้ติดต่อไว้ใน field ตัวเลขที่รับประกันว่าเก็บได้อย่างน้อย 15 หลัก คุณ test อย่างละเอียดทั่วอเมริกาเหนือแล้วทุกอย่างก็ดูเหมือนจะเรียบร้อยดี แต่จู่ ๆ คุณก็ได้รับข้อร้องเรียนถล่มทลายจากส่วนอื่น ๆ ของโลก ทำไมล่ะครับ
แบบฝึกหัด 27 ( คำตอบที่เป็นไปได้ )
คุณเขียนแอปที่ช่วยขยายสูตรอาหารทั่วไปให้เหมาะกับห้องอาหารบนเรือสำราญที่นั่งได้ 5,000 คน แต่คุณกลับได้รับข้อร้องเรียนว่าการแปลงหน่วยไม่แม่นยำ คุณตรวจสอบดูแล้วพบว่าโค้ดใช้สูตรแปลงหน่วย 16 ถ้วยต่อ 1 แกลลอน นั่นถูกต้องแล้วใช่ไหมครับ