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 (ปลอดภัยไว้ก่อนเมื่อออกไปข้างนอก) ​