| Topic 25 | การเขียนโปรแกรมแบบยืนยันเงื่อนไข |
|---|
ความสำนึกผิดต่อตัวเองมีความหรูหราอยู่ในตัว เมื่อเราโทษตัวเอง เรารู้สึกว่าไม่มีใครมีสิทธิ์มาโทษเราได้อีก
Oscar Wilde , The Picture of Dorian Gray
ดูเหมือนว่าจะมีคาถาบทหนึ่งที่โปรแกรมเมอร์ทุกคนต้องท่องจำให้ขึ้นใจตั้งแต่ช่วงต้นของอาชีพการงาน มันคือหลักการพื้นฐานของการคำนวณ ความเชื่อหลักที่เราเรียนรู้ที่จะนำไปใช้กับ requirement, การออกแบบ, โค้ด, คอมเมนต์ แทบทุกอย่างที่เราทำ มันมีอยู่ว่า
สิ่งนี้ไม่มีทางเกิดขึ้นได้เลย…
“แอปพลิเคชันนี้ไม่มีทางถูกใช้ในต่างประเทศหรอก แล้วจะไป internationalize มันทำไม?” “ count ไม่มีทางติดลบได้หรอก” “Logging ไม่มีทางล้มเหลวได้หรอก”
อย่ามาหลอกตัวเองแบบนี้กันเลยดีกว่า โดยเฉพาะตอนเขียนโค้ดครับ
| Tip 39 | ใช้ Assertion เพื่อป้องกันสิ่งที่เป็นไปไม่ได้ |
|---|
เมื่อไหร่ก็ตามที่คุณคิดว่า “แต่แน่นอนว่ามันเกิดขึ้นไม่ได้หรอก” ให้เพิ่มโค้ดเพื่อตรวจสอบมันซะ วิธีที่ง่ายที่สุดในการทำแบบนี้คือการใช้ assertion การใช้งานภาษาหลายๆ ภาษาจะมีบางรูปแบบของ assert ที่คอยตรวจสอบเงื่อนไขแบบ Boolean [31] การตรวจสอบเหล่านี้มีค่าอย่างยิ่ง ถ้าพารามิเตอร์หรือผลลัพธ์ไม่ควรเป็น null เลย ก็ให้ตรวจสอบมันอย่างชัดเจน:
| | assert (result != null ); |
|---|
ในการใช้งานภาษา Java คุณสามารถ (และควร) เพิ่มข้อความอธิบายเข้าไปด้วย:
| | assert result != null && result.size() > 0 : "Empty result from XYZ" ; |
|---|
Assertion ยังมีประโยชน์ในการตรวจสอบการทำงานของอัลกอริทึมด้วย สมมติว่าคุณเขียนอัลกอริทึม sort สุดล้ำขึ้นมาตัวหนึ่ง ชื่อว่า my_sort ลองตรวจสอบดูว่ามันทำงานถูกต้อง:
| | books = my_sort(find( "scifi" )) |
|---|---|
| | assert(is_sorted?(books)) |
อย่าใช้ assertion แทนการจัดการ error จริงๆ assertion ใช้ตรวจสอบสิ่งที่ไม่ควรเกิดขึ้นเลย คุณคงไม่อยากเขียนโค้ดแบบต่อไปนี้หรอก:
| | puts( "Enter 'Y' or 'N': " ) |
|---|---|
| | ans = gets[0] # Grab first character of response |
| | assert((ch == 'Y' ) || (ch == 'N' )) # Very bad idea! |
และแค่เพราะการใช้งาน assert ส่วนใหญ่จะยุติ process เมื่อ assertion ล้มเหลว ก็ไม่ได้หมายความว่าเวอร์ชันที่คุณเขียนขึ้นเองจะต้องทำแบบนั้นด้วย ถ้าคุณต้องปลดปล่อย resource ให้ catch exception ของ assertion หรือดัก exit แล้วรัน error handler ของคุณเอง แค่ต้องแน่ใจว่าโค้ดที่คุณรันในช่วงมิลลิวินาทีสุดท้ายก่อนตายนั้น ไม่พึ่งพาข้อมูลที่เป็นต้นเหตุให้ assertion ล้มเหลวตั้งแต่แรกครับ
Assertions and Side Effects (Assertion และผลข้างเคียง)
มันน่าอายไม่น้อยเวลาที่โค้ดที่เราเพิ่มเข้าไปเพื่อตรวจจับ error กลับกลายเป็นตัวสร้าง error ใหม่ขึ้นมาเอง เรื่องแบบนี้เกิดขึ้นได้กับ assertion ถ้าการประเมินเงื่อนไขมีผลข้างเคียง (side effect) ตัวอย่างเช่น การเขียนโค้ดแบบนี้ถือเป็นความคิดที่แย่มาก
| | while (iter.hasMoreElements()) { |
|---|---|
| | assert (iter.nextElement() != null ); |
| | Object obj = iter.nextElement(); |
| | // .... |
| | } |
การเรียก .nextElement() ใน assertion มีผลข้างเคียงคือทำให้ iterator เลื่อนผ่านตัว element ที่กำลังดึงไป ทำให้ loop ประมวลผลได้แค่ครึ่งเดียวของ element ทั้งหมดใน collection การเขียนแบบนี้จะดีกว่า
| | while (iter.hasMoreElements()) { |
|---|---|
| | Object obj = iter.nextElement(); |
| | assert (obj != null ); |
| | // .... |
| | } |
ปัญหานี้เป็น Heisenbug ชนิดหนึ่ง [32] —คือการดีบักที่ไปเปลี่ยนพฤติกรรมของระบบที่กำลังถูกดีบักอยู่นั่นเองครับ
(เราเองก็เชื่อด้วยว่าทุกวันนี้ เมื่อภาษาส่วนใหญ่รองรับฟังก์ชันสำหรับวนลูปบน collection ได้ดีอยู่แล้ว การเขียน loop แบบชัดเจนแบบนี้ก็ไม่จำเป็นและถือเป็นฟอร์มที่ไม่ดีอีกต่อไป)
Leave Assertions Turned On (เปิด Assertion ทิ้งไว้)
มีความเข้าใจผิดที่พบได้บ่อยเกี่ยวกับ assertion มันมีเนื้อหาประมาณนี้:
Assertion เพิ่ม overhead ให้กับโค้ดเล็กน้อย เพราะมันตรวจสอบสิ่งที่ไม่ควรเกิดขึ้นเลย มันจึงจะถูกทริกเกอร์ก็ต่อเมื่อมีบั๊กในโค้ดเท่านั้น เมื่อโค้ดถูกทดสอบและปล่อยออกไปแล้ว ก็ไม่จำเป็นต้องใช้มันอีกต่อไป และควรปิดมันเพื่อให้โค้ดรันได้เร็วขึ้น assertion เป็นแค่เครื่องมือช่วยดีบักเท่านั้น
ในนี้มีสมมติฐานที่ผิดอย่างชัดเจนอยู่สองข้อ ข้อแรก พวกเขาสมมติว่าการเทสต์จะจับบั๊กได้ทั้งหมด แต่ในความเป็นจริง สำหรับโปรแกรมที่ซับซ้อนใดๆ แทบเป็นไปไม่ได้เลยที่คุณจะเทสต์ได้แม้แต่เศษเสี้ยวเล็กๆ ของ permutation ที่โค้ดของคุณจะต้องเจอ ข้อที่สอง คนที่มองโลกในแง่ดีมักลืมไปว่าโปรแกรมของคุณรันอยู่ในโลกที่อันตราย ระหว่างการเทสต์ หนูคงไม่ได้มากัดสายสื่อสารขาด คนเล่นเกมคงไม่ได้ใช้หน่วยความจำจนหมด และไฟล์ log ก็คงไม่เต็มพาร์ทิชันจัดเก็บข้อมูล แต่สิ่งเหล่านี้อาจเกิดขึ้นได้เมื่อโปรแกรมของคุณรันอยู่ใน production แนวป้องกันด่านแรกของคุณคือการตรวจสอบ error ที่เป็นไปได้ทุกแบบ ส่วนแนวป้องกันด่านที่สองคือการใช้ assertion เพื่อพยายามตรวจจับสิ่งที่คุณพลาดไปครับ
การปิด assertion ตอนส่งมอบโปรแกรมไปยัง production ก็เหมือนกับการเดินลวดสูงโดยไม่มีตาข่ายรองรับ เพียงเพราะคุณเคยเดินผ่านมันได้สำเร็จตอนซ้อม มันดูดราม่าดี แต่คงหาประกันชีวิตยากหน่อยครับ
ถึงแม้คุณจะ มี ปัญหาเรื่อง performance จริงๆ ก็ให้ปิดเฉพาะ assertion ที่ส่งผลกระทบกับคุณจริงๆ เท่านั้น ตัวอย่างการ sort ข้างต้นอาจเป็นส่วนสำคัญของแอปพลิเคชันคุณ และอาจต้องการความเร็วสูง การเพิ่มการตรวจสอบเข้าไปหมายถึงการวนอ่านข้อมูลอีกรอบหนึ่ง ซึ่งอาจยอมรับไม่ได้ ให้ทำให้การตรวจสอบตัวนั้นเป็นแบบ optional ได้ แต่ให้เก็บที่เหลือไว้เหมือนเดิมครับ
ใช้ Assertion ใน Production แล้วอาจได้เงินก้อนโต
อดีตเพื่อนบ้านของแอนดี้เคยเป็นหัวหน้าบริษัทสตาร์ทอัพเล็กๆ แห่งหนึ่งที่ผลิตอุปกรณ์เครือข่าย หนึ่งในความลับสู่ความสำเร็จของพวกเขาคือการตัดสินใจเก็บ assertion ไว้ใน release ที่ใช้งานจริงใน production assertion เหล่านี้ถูกออกแบบมาอย่างดีเพื่อรายงานข้อมูลที่เกี่ยวข้องทั้งหมดที่นำไปสู่ความล้มเหลว และนำเสนอผ่าน UI ที่ดูดีให้กับผู้ใช้ปลายทาง ฟีดแบ็กระดับนี้ที่มาจากผู้ใช้จริงภายใต้สถานการณ์จริง ทำให้นักพัฒนาสามารถอุดช่องโหว่และแก้บั๊กที่ซ่อนเร้นและยากจะทำซ้ำเหล่านี้ได้ ส่งผลให้ได้ซอฟต์แวร์ที่มีความเสถียรอย่างน่าทึ่งและแข็งแกร่งราวกับกันกระสุนครับ
บริษัทเล็กๆ ที่ไม่มีใครรู้จักแห่งนี้มีผลิตภัณฑ์ที่แข็งแกร่งมากจนไม่นานก็ถูกซื้อกิจการไปด้วยมูลค่าหลายร้อยล้านดอลลาร์
ก็แค่บอกไว้เฉยๆ ครับ
Exercise 16 ( คำตอบที่เป็นไปได้ )
มาเช็กความจริงกันสักหน่อย สิ่งที่ “เป็นไปไม่ได้” เหล่านี้ข้อไหนเกิดขึ้นได้จริง?
- เดือนที่มีน้อยกว่า 28 วัน
- error code จากการเรียก system call ว่าไม่สามารถเข้าถึงไดเรกทอรีปัจจุบันได้
- ในภาษา C++: a = 2; b = 3; แต่ (a + b) กลับไม่เท่ากับ 5
- รูปสามเหลี่ยมที่มีผลรวมมุมภายใน ≠ 180°
- นาทีที่ไม่มี 60 วินาที
- (a + 1) <= a
Related Sections Include (หัวข้อที่เกี่ยวข้อง)
- Topic 23, Design by Contract (การออกแบบด้วยสัญญา)
- Topic 24, Dead Programs Tell No Lies (โปรแกรมที่ตายแล้วไม่โกหก)
- Topic 42, Property-Based Testing (การทดสอบแบบ Property-Based)
- Topic 43, Stay Safe Out There (ปลอดภัยไว้ก่อนเมื่อออกไปข้างนอก)