The Whole and the Parts

"ข้าสามารถเรียกวิญญาณออกมาจากห้วงลึกอันกว้างใหญ่ได้" "ทำไมล่ะ ข้าก็ทำได้ หรือใครๆ ก็ทำได้ แต่พวกมันจะมาหาหรือเปล่าเมื่อเจ้าเรียกหาพวกมัน?" — เชกสเปียร์ (SHAKESPEARE), KING HENRY IV, PART I

© The Walt Disney Company

เวทมนตร์ในสมัยใหม่ก็เหมือนกับสมัยก่อน ที่มีผู้ฝึกฝนจอมโอ้อวด: "ข้าสามารถเขียนโปรแกรมที่ควบคุมการจราจรทางอากาศ สกัดกั้นขีปนาวุธ ปรับยอดบัญชีธนาคาร ควบคุมสายการผลิตได้" ซึ่งคำตอบที่ได้รับคือ "ข้าก็ทำได้ และใครๆ ก็ทำได้ แต่พวกมันทำงานได้จริงหรือเปล่าเมื่อเจ้าเขียนพวกมันขึ้นมา?"

เราจะสร้างโปรแกรมให้ทำงานได้จริงได้อย่างไร? เราจะทดสอบโปรแกรมได้อย่างไร? และเราจะรวมชุดโปรแกรมส่วนประกอบที่ผ่านการทดสอบแล้วให้กลายเป็นระบบที่ผ่านการทดสอบและวางใจได้ได้อย่างไร? เราได้แตะประเด็นเทคนิคเหล่านี้ไปบ้างแล้วในที่ต่างๆ ตอนนี้ให้เรามาพิจารณาพวกมันอย่างเป็นระบบมากขึ้นอีกสักนิด

Designing the Bugs Out

การป้องกันบั๊กในขั้นตอนการนิยาม (Bug-proofing the definition): บั๊กที่ร้ายกาจและลึกลับที่สุดคือบั๊กของระบบ (system bugs) ที่เกิดจากสมมติฐานที่ไม่ตรงกันระหว่างผู้เขียนส่วนประกอบต่างๆ แนวทางในการสร้างความสอดคล้องทางแนวคิด (conceptual integrity) ที่ได้อภิปรายไปในบทที่ 4, 5 และ 6 นั้นช่วยแก้ปัญหาเหล่านี้โดยตรง กล่าวโดยย่อคือ ความสอดคล้องทางแนวคิดของผลิตภัณฑ์ไม่เพียงแต่ทำให้ใช้งานง่ายขึ้น แต่ยังทำให้สร้างง่ายขึ้นและเกิดบั๊กน้อยลงด้วย เช่นเดียวกับความพยายามทางสถาปัตยกรรมที่ละเอียดและถี่ถ้วน

V. A. Vyssotsky จากโครงการ Safeguard ของ Bell Telephone Laboratories กล่าวว่า "งานที่สำคัญที่สุดคือการนิยามผลิตภัณฑ์ให้ชัดเจน ความล้มเหลวจำนวนมหาศาลเกี่ยวข้องกับแง่มุมที่ไม่ได้มีการระบุไว้อย่างชัดเจน" การนิยามหน้าที่การทำงานอย่างรอบคอบ การทำข้อกำหนดที่ละเอียด และการกำจัดหน้าที่ส่วนเกินหรือเทคนิคที่แพรวพราวเกินความจำเป็น ทั้งหมดนี้ช่วยลดจำนวนบั๊กของระบบที่ต้องตามหาในภายหลัง

การทดสอบข้อกำหนด (Testing the specification): นานก่อนที่จะมีโค้ดใดๆ เกิดขึ้น ข้อกำหนดจะต้องถูกส่งไปให้กลุ่มทดสอบภายนอกเพื่อตรวจสอบความครบถ้วนและความชัดเจน ดังที่ Vyssotsky กล่าวไว้ว่า นักพัฒนาเองไม่สามารถทำสิ่งนี้ได้: "พวกเขาจะไม่บอกคุณหรอกว่าเขาไม่เข้าใจ แต่มันจะหาทางแถไปตามช่องโหว่และความคลุมเครือเหล่านั้นด้วยความยินดี"

การออกแบบจากบนลงล่าง (Top-down design): ในบทความปี 1971 ที่ชัดเจนมาก Niklaus Wirth ได้ทำให้กระบวนการออกแบบที่เหล่านักเขียนโปรแกรมเก่งๆ ใช้กันมานานหลายปีเป็นรูปธรรมขึ้นมา นอกจากนี้ แนวคิดของเขาแม้จะกล่าวถึงการออกแบบโปรแกรม แต่ก็สามารถนำมาใช้ได้อย่างสมบูรณ์กับการออกแบบระบบโปรแกรมที่ซับซ้อน การแบ่งการสร้างระบบออกเป็นสถาปัตยกรรม การนำไปใช้งาน และการทำให้เป็นจริง เป็นรูปธรรมของแนวคิดเหล่านี้ ยิ่งไปกว่านั้น ทั้งสถาปัตยกรรม การนำไปใช้งาน และการทำให้เป็นจริง ต่างก็ทำได้ดีที่สุดด้วยวิธีการแบบบนลงล่าง

โดยสังเขป กระบวนการของ Wirth คือการระบุว่าการออกแบบเป็นลำดับขั้นตอนของการขัดเกลา (refinement steps) เราเริ่มจากการร่างนิยามของงานแบบคร่าวๆ และวิธีแก้ปัญหาคร่าวๆ ที่ให้ผลลัพธ์หลักที่ต้องการ จากนั้นเราจึงตรวจสอบนิยามนั้นให้ละเอียดขึ้นเพื่อดูว่าผลลัพธ์ที่ได้แตกต่างจากสิ่งที่ต้องการอย่างไร และเราจึง...

นำขั้นตอนใหญ่ๆ ของวิธีแก้ปัญหามาแบ่งย่อยเป็นขั้นตอนที่เล็กลง การขัดเกลาแต่ละครั้งในนิยามของงานจะกลายเป็นการขัดเกลาในอัลกอริทึมสำหรับวิธีแก้ปัญหา และแต่ละครั้งอาจมาพร้อมกับการขัดเกลาในการแสดงแทนข้อมูล จากกระบวนการนี้ เราจะระบุโมดูลของวิธีแก้ปัญหาหรือของข้อมูลซึ่งการขัดเกลาต่อไปสามารถดำเนินไปได้อย่างเป็นอิสระจากงานอื่น ระดับของความเป็นโมดูลนี้จะเป็นตัวกำหนดความสามารถในการปรับตัวและความสามารถในการเปลี่ยนแปลงของโปรแกรม Wirth สนับสนุนให้ใช้สัญลักษณ์ในระดับที่สูงที่สุดเท่าที่จะเป็นไปได้ในแต่ละขั้นตอน เพื่อแสดงแนวคิดและซ่อนรายละเอียดไว้จนกว่าการขัดเกลาเพิ่มเติมจะจำเป็น

การออกแบบจากบนลงล่างที่ดีช่วยหลีกเลี่ยงบั๊กได้ในหลายทาง: ประการแรก ความชัดเจนของโครงสร้างและการแสดงแทนข้อมูลทำให้การระบุข้อกำหนดและหน้าที่การทำงานของโมดูลทำได้ง่ายขึ้น ประการที่สอง การแบ่งส่วนและความเป็นอิสระของโมดูลช่วยหลีกเลี่ยงบั๊กของระบบ ประการที่สาม การซ่อนรายละเอียดทำให้มองเห็นข้อบกพร่องในโครงสร้างได้ชัดเจนขึ้น ประการที่สี่ งานออกแบบสามารถทดสอบได้ในแต่ละขั้นตอนของการขัดเกลา ดังนั้นการทดสอบจึงเริ่มได้เร็วขึ้นและมุ่งเน้นไปที่ระดับรายละเอียดที่เหมาะสมในแต่ละขั้นตอน

กระบวนการขัดเกลาทีละขั้นตอน (step-wise refinement) ไม่ได้หมายความว่าเราไม่ต้องย้อนกลับไปยกเลิกระดับบนสุดและเริ่มใหม่ทั้งหมดเลย เมื่อเราเจอรายละเอียดที่ซับซ้อนอย่างคาดไม่ถึง อันที่จริง เรื่องนี้เกิดขึ้นบ่อยครั้ง แต่มันง่ายกว่ามากที่จะเห็นว่าเมื่อใดและทำไมเราควรทิ้งงานออกแบบภาพรวมและเริ่มใหม่ ระบบที่แย่หลายระบบเกิดจากความพยายามที่จะกู้งานออกแบบพื้นฐานที่แย่และปะผุด้วยการแก้ไขเพียงผิวเผินทุกรูปแบบ การออกแบบจากบนลงล่างช่วยลดสิ่งยั่วยวนนี้ ผมเชื่อมั่นว่าการออกแบบจากบนลงล่างคือรูปแบบการเขียนโปรแกรมใหม่ที่สำคัญที่สุดในทศวรรษนี้

การเขียนโปรแกรมแบบโครงสร้าง (Structured programming): อีกหนึ่งชุดความคิดที่สำคัญในการออกแบบเพื่อป้องกันบั๊กในโปรแกรมมาจาก Dijkstra และถูกสร้างขึ้นบนโครงสร้างทางทฤษฎีโดย Bohm และ Jacopini โดยพื้นฐานแล้ว แนวทางนี้คือการออกแบบโปรแกรมที่โครงสร้างการควบคุมประกอบด้วยวงจรปิด (loops) ที่นิยามโดยคำสั่งเช่น DO WHILE และส่วนที่เป็นเงื่อนไขซึ่งแบ่งกลุ่มคำสั่งด้วยวงเล็บและกำหนดเงื่อนไขด้วย IF ... THEN ... ELSE Bohm และ Jacopini แสดงให้เห็นว่าโครงสร้างเหล่านี้เพียงพอในทางทฤษฎี ส่วน Dijkstra แย้งว่าทางเลือกอื่นอย่างการแตกแขนงที่ไม่มีขีดจำกัดผ่าน GO TO จะสร้างโครงสร้างที่นำไปสู่ข้อผิดพลาดทางตรรกะได้ง่าย

แนวคิดพื้นฐานนั้นถูกต้องอย่างแน่นอน แม้จะมีการวิจารณ์เกิดขึ้นมากมาย และโครงสร้างการควบคุมเพิ่มเติม เช่น การแตกแขนงแบบหลายทาง (ที่เรียกว่าคำสั่ง CASE) สำหรับการแยกแยะกรณีต่างๆ ที่อาจเกิดขึ้น และการจัดการเมื่อเกิดข้อผิดพลาดรุนแรง (GO TO ABNORMAL END) ก็มีความสะดวกมาก ยิ่งไปกว่านั้น บางคนยังเคร่งครัดมากเกี่ยวกับการหลีกเลี่ยง GO TO ทั้งหมด ซึ่งนั่นดูจะเกินไปหน่อย

ประเด็นที่สำคัญและเป็นหัวใจสำคัญในการสร้างโปรแกรมที่ปราศจากบั๊ก คือเราควรจะมองโครงสร้างการควบคุมของระบบในฐานะที่เป็น "โครงสร้างการควบคุม" ไม่ใช่เป็นเพียง "คำสั่งกระโดดแยกส่วน" วิธีคิดแบบนี้คือความก้าวหน้าก้าวสำคัญ

Component Debugging

ขั้นตอนในการแก้ไขข้อผิดพลาดของโปรแกรมได้ผ่านวงจรครั้งใหญ่ในช่วงยี่สิบปีที่ผ่านมา และในบางแง่มุมมันก็ได้ย้อนกลับไปสู่จุดเริ่มต้น วงจรนี้ผ่านไปสี่ขั้นตอน และเป็นเรื่องสนุกที่จะติดตามดูแรงจูงใจเบื้องหลังแต่ละขั้นตอน

การแก้ไขข้อผิดพลาดบนเครื่อง (On-machine debugging): เครื่องรุ่นแรกๆ มีอุปกรณ์อินพุต-เอาต์พุตที่ค่อนข้างแย่ และมีความล่าช้าในการอ่านเขียนข้อมูลสูง โดยปกติเครื่องจะอ่านและเขียนเทปกระดาษหรือเทปแม่เหล็ก และใช้เครื่องมือแยกต่างหาก (off-line facilities) ในการเตรียมเทปและพิมพ์ข้อมูล สิ่งนี้ทำให้การใช้อินพุต-เอาต์พุตผ่านเทปเป็นเรื่องที่ยุ่งยากจนทนไม่ไหวสำหรับการแก้ไขข้อผิดพลาด จึงมีการใช้คอนโซล (console) แทน ดังนั้นการแก้ไขข้อผิดพลาดจึงถูกออกแบบมาเพื่อให้สามารถทดลองได้หลายครั้งที่สุดต่อการใช้งานเครื่องหนึ่งรอบ นักเขียนโปรแกรมจะออกแบบขั้นตอนการแก้ไขข้อผิดพลาดอย่างระมัดระวัง—วางแผนว่าจะหยุดตรงไหน ตำแหน่งหน่วยความจำใดที่ต้องตรวจสอบ ต้องหาอะไรในนั้น และจะทำอย่างไรหากไม่พบสิ่งที่ต้องการ การวางโปรแกรมให้ตัวเองทำงานเหมือนเครื่องมือแก้ไขข้อผิดพลาดที่พิถีพิถันนี้อาจใช้เวลาถึงครึ่งหนึ่งของการเขียนโปรแกรมคอมพิวเตอร์จริงๆ บาปที่ร้ายแรงที่สุดคือการกดปุ่ม START อย่างบ้าบิ่นโดยไม่ได้แบ่งส่วนโปรแกรมออกเป็นส่วนการทดสอบพร้อมจุดหยุดที่วางแผนไว้

การดัมพ์หน่วยความจำ (Memory dumps): การแก้ไขข้อผิดพลาดบนเครื่องนั้นมีประสิทธิภาพมาก ในเซสชันสองชั่วโมง เราอาจทดสอบได้ถึงสิบกว่าครั้ง แต่คอมพิวเตอร์นั้นหายากมากและมีราคาแพงมาก และความคิดที่ว่าเวลาของเครื่องจักรทั้งหมดนั้นถูกใช้ไปอย่างสูญเปล่าเป็นเรื่องที่น่าตกใจ ดังนั้นเมื่อมีเครื่องพิมพ์ความเร็วสูงมาเชื่อมต่อแบบออนไลน์ เทคนิคจึงเปลี่ยนไป เราจะรันโปรแกรมจนกระทั่งการตรวจสอบล้มเหลว จากนั้นจึงดัมพ์หน่วยความจำทั้งหมดออกมา แล้วจึงเริ่มงานบนโต๊ะที่แสนเหน็ดเหนื่อยในการไล่เลียงเนื้อหาในแต่ละตำแหน่งของหน่วยความจำ เวลาที่ใช้บนโต๊ะไม่ได้แตกต่างจากการแก้ไขข้อผิดพลาดบนเครื่องมากนัก แต่มันเกิดขึ้นหลังจากรันการทดสอบเพื่อ "ถอดรหัส" แทนที่จะเกิดขึ้นก่อนหน้าเพื่อ "วางแผน" การแก้ไขข้อผิดพลาดสำหรับผู้ใช้แต่ละคนใช้เวลานานขึ้นมาก เนื่องจากการทดสอบขึ้นอยู่กับเวลาดำเนินการแบบแบตช์ อย่างไรก็ตาม ขั้นตอนทั้งหมดนี้ถูกออกแบบมาเพื่อลดการใช้เวลาของคอมพิวเตอร์ให้น้อยที่สุด และเพื่อรองรับนักเขียนโปรแกรมให้ได้จำนวนมากที่สุดเท่าที่จะเป็นไปได้

สแนปชอต (Snapshots): เครื่องคอมพิวเตอร์ที่เทคนิคการดัมพ์หน่วยความจำถูกพัฒนาขึ้นนั้นมีขนาด 2,000-4,000 เวิร์ด หรือ 8K ถึง 16K ไบต์ แต่ขนาดของหน่วยความจำเติบโตขึ้นอย่างรวดเร็วมาก จนการดัมพ์หน่วยความจำทั้งหมดกลายเป็นเรื่องที่เป็นไปไม่ได้ ดังนั้นผู้คนจึงพัฒนาเทคนิคการดัมพ์แบบเลือกเฉพาะจุด (selective dumping) การติดตามผลแบบเลือกเฉพาะจุด (selective tracing) และการแทรกสแนปชอตเข้าไปในโปรแกรม TESTRAN ใน OS/360 คือจุดสิ้นสุดของทิศทางนี้ โดยยอมให้เราแทรกสแนปชอตเข้าไปในโปรแกรมได้โดยไม่ต้องทำการแอสเซมบลีหรือคอมไพล์ใหม่

การแก้ไขข้อผิดพลาดแบบโต้ตอบ (Interactive debugging): ในปี 1959 Codd และเพื่อนร่วมงาน และ Strachey ต่างรายงานผลงานที่มุ่งเป้าไปที่การแก้ไขข้อผิดพลาดแบบแบ่งเวลา (time-shared debugging) ซึ่งเป็นวิธีการที่จะได้ทั้งเวลาดำเนินการที่รวดเร็วทันใจเหมือนการแก้ไขบนเครื่อง และการใช้เครื่องจักรอย่างมีประสิทธิภาพเหมือนการแก้ไขแบบแบตช์ คอมพิวเตอร์จะมีโปรแกรมหลายโปรแกรมอยู่ในหน่วยความจำพร้อมสำหรับการรัน เทอร์มินัลที่ควบคุมด้วยโปรแกรมจะถูกเชื่อมโยงกับแต่ละโปรแกรมที่กำลังถูกแก้ไขข้อผิดพลาด การแก้ไขจะอยู่ภายใต้การควบคุมของโปรแกรมตัวควบคุม (supervisory program) เมื่อนักเขียนโปรแกรมที่เทอร์มินัลหยุดโปรแกรมของเขาเพื่อตรวจสอบความคืบหน้าหรือทำการเปลี่ยนแปลง ตัวควบคุมจะรันโปรแกรมอื่นแทน ทำให้เครื่องจักรทำงานยุ่งอยู่ตลอดเวลา

ระบบการทำงานหลายโปรแกรมพร้อมกัน (multiprogramming) ของ Codd ได้รับการพัฒนาขึ้น แต่เน้นที่การเพิ่มปริมาณงานโดยการใช้ประโยชน์จากอินพุต-เอาต์พุตอย่างมีประสิทธิภาพ และไม่ได้มีการนำการแก้ไขข้อผิดพลาดแบบโต้ตอบมาใช้งาน ส่วนไอเดียของ Strachey ได้รับการปรับปรุงและนำไปใช้งานในปี 1963 ในระบบทดลองสำหรับเครื่อง 7090 โดยคอร์บาโตและเพื่อนร่วมงานที่ MIT การพัฒนานี้ได้นำไปสู่ MULTICS, TSS และระบบแบ่งเวลาอื่นๆ ในปัจจุบัน

ความแตกต่างหลักที่ผู้ใช้สัมผัสได้ระหว่างการแก้ไขข้อผิดพลาดบนเครื่องในยุคแรกกับการแก้ไขแบบโต้ตอบในปัจจุบัน คือสิ่งอำนวยความสะดวกที่เกิดขึ้นได้จากการมีอยู่ของโปรแกรมตัวควบคุมและตัวแปลภาษาที่เกี่ยวข้อง เราสามารถเขียนโปรแกรมและแก้ไขข้อผิดพลาดได้ในภาษาระดับสูง เครื่องมือแก้ไขที่มีประสิทธิภาพช่วยให้การเปลี่ยนแปลงและการทำสแนปชอตทำได้ง่าย การกลับมาสู่ความสามารถในการได้รับผลทันใจแบบการแก้ไขบนเครื่อง ยังไม่ได้นำพาเรากลับไปสู่การ "วางแผนล่วงหน้า" สำหรับรอบการแก้ไขข้อผิดพลาด ในแง่หนึ่งการวางแผนล่วงหน้าดังกล่าวอาจดูไม่จำเป็นเท่าเมื่อก่อน เพราะเวลาของเครื่องจักรไม่ได้สูญเสียไปในขณะที่เรานั่งคิด อย่างไรก็ตาม ผลการทดลองที่น่าสนใจของ Gold แสดงให้เห็นว่า ความก้าวหน้าในการแก้ไขข้อผิดพลาดแบบโต้ตอบใน "การปฏิสัมพันธ์ครั้งแรก" ของแต่ละรอบการทำงานนั้นมีมากกว่าการปฏิสัมพันธ์ครั้งต่อๆ ไปถึงสามเท่า สิ่งนี้ชี้ให้เห็นอย่างชัดเจนว่าเรายังไม่ได้ใช้ศักยภาพของการปฏิสัมพันธ์อย่างเต็มที่เนื่องจากขาด...

การวางแผนรอบการทำงาน ถึงเวลาแล้วที่จะปัดฝุ่นเทคนิคการแก้ไขบนเครื่องแบบเก่ามาใช้ ผมพบว่าการใช้งานระบบเทอร์มินัลที่ดีอย่างเหมาะสมต้องการเวลาที่โต๊ะทำงานสองชั่วโมงสำหรับทุกๆ สองชั่วโมงที่ใช้บนเทอร์มินัล ครึ่งหนึ่งของเวลานี้ใช้ไปกับการ "เก็บกวาด" หลังจากรอบที่แล้ว: เช่น การอัปเดตบันทึกการแก้ไขข้อผิดพลาด จัดเก็บรายการโปรแกรมที่อัปเดตแล้วลงในสมุดบันทึกระบบ อธิบายปรากฏการณ์ประหลาดๆ ที่เกิดขึ้น ส่วนอีกครึ่งหนึ่งใช้ไปกับการเตรียมตัว: วางแผนการเปลี่ยนแปลงและการปรับปรุง และออกแบบการทดสอบที่ละเอียดสำหรับครั้งต่อไป หากไม่มีการวางแผนเช่นนี้ มันยากที่จะรักษาผลิตภาพไว้ได้ตลอดสองชั่วโมง และหากไม่มีการเก็บกวาดหลังจบรอบ มันยากที่จะทำให้รอบการใช้งานเทอร์มินัลที่ต่อเนื่องกันนั้นเป็นระบบและก้าวไปข้างหน้าได้

กรณีทดสอบ (Test cases): สำหรับการออกแบบขั้นตอนการแก้ไขข้อผิดพลาดและกรณีทดสอบจริงๆ Gruenberger มีคำแนะนำที่ยอดเยี่ยมเป็นพิเศษ และยังมีคำแนะนำสั้นๆ ในตำรามาตรฐานอื่นๆ อีกด้วย

System Debugging

ส่วนที่ยากอย่างคาดไม่ถึงของการสร้างระบบการเขียนโปรแกรมคือ การทดสอบระบบ (system test) ผมได้พูดถึงเหตุผลบางประการสำหรับทั้งความยากและความคาดไม่ถึงของมันไปแล้ว จากทั้งหมดนั้น เราควรจะมั่นใจในสองสิ่ง: การแก้ไขข้อผิดพลาดในระบบจะใช้เวลานานกว่าที่เราคาดไว้ และความยากของมันทำให้ต้องใช้แนวทางที่เป็นระบบและมีการวางแผนอย่างถี่ถ้วน ให้เรามาดูกันว่าแนวทางดังกล่าวประกอบด้วยอะไรบ้าง

ใช้ส่วนประกอบที่ผ่านการแก้ไขข้อผิดพลาดแล้ว (Use debugged components): สามัญสำนึก (แม้จะไม่ใช่การปฏิบัติทั่วไป) บอกเราว่าเราควรเริ่มการแก้ไขข้อผิดพลาดในระบบหลังจากที่แต่ละชิ้นส่วนดูเหมือนจะทำงานได้แล้วเท่านั้น

การปฏิบัติทั่วไปมักจะแหกกฎนี้ในสองทาง ประการแรกคือแนวทาง "ประกอบเข้าด้วยกันแล้วลองดู" (bolt-it-together-and-try approach) ซึ่งดูเหมือนจะตั้งอยู่บนแนวคิดที่ว่ามันจะมีบั๊กของระบบ (เช่น อินเทอร์เฟซ) นอกเหนือไปจากบั๊กของส่วนประกอบ ยิ่งรวมชิ้นส่วนเข้าด้วยกันเร็วเท่าไหร่ บั๊กของระบบก็จะปรากฏออกมาเร็วเท่านั้น ส่วนแนวคิดที่ซับซ้อนน้อยกว่าเล็กน้อยคือการใช้ชิ้นส่วนเพื่อทดสอบกันเอง ซึ่งจะช่วยลดความจำเป็นในการทำนั่งร้านเพื่อการทดสอบ (test scaffolding) ลงได้มาก ทั้งสองอย่างนี้เป็นจริงอย่างเห็นได้ชัด แต่ประสบการณ์แสดงให้เห็นว่ามันไม่ใช่ความจริงทั้งหมด—การใช้ส่วนประกอบที่สะอาดและผ่านการแก้ไขข้อผิดพลาดมาแล้วช่วยประหยัดเวลาในการทดสอบระบบได้มากกว่าเวลาที่ใช้ไปกับนั่งร้านและการทดสอบส่วนประกอบที่ถี่ถ้วนเสียอีก

แนวทางที่ลึกลับขึ้นมาอีกนิดคือแนวทาง "บั๊กที่มีการบันทึกไว้" (documented bug approach) ซึ่งกล่าวว่าส่วนประกอบหนึ่งพร้อมจะเข้าสู่การทดสอบระบบเมื่อพบบั๊กทั้งหมดแล้ว แม้จะยังแก้ไขไม่เสร็จทั้งหมดก็ตาม จากนั้นในการทดสอบระบบ (ตามทฤษฎีนี้) เราจะทราบผลกระทบที่คาดหวังของบั๊กเหล่านี้และสามารถเพิกเฉยต่อผลกระทบเหล่านั้นได้ เพื่อไปมุ่งเน้นที่ปรากฏการณ์ใหม่ๆ ทั้งหมดนี้เป็นเพียงการคิดเข้าข้างตัวเอง ซึ่งคิดค้นขึ้นมาเพื่อหาเหตุผลมาลบล้างความเจ็บปวดจากกำหนดการที่ล่าช้า เราไม่สามารถรู้ผลกระทบทั้งหมดที่คาดหวังจากบั๊กที่รู้จักได้หรอก หากเรื่องต่างๆ มันตรงไปตรงมาขนาดนั้น การทดสอบระบบก็คงไม่ยาก ยิ่งไปกว่านั้น การแก้ไขบั๊กของส่วนประกอบที่มีการบันทึกไว้เหล่านั้นจะนำพาบั๊กที่ไม่รู้จักเข้ามาอย่างแน่นอน และการทดสอบระบบก็จะเกิดความสับสน

สร้างนั่งร้านไว้ให้มาก (Build plenty of scaffolding): โดย "นั่งร้าน" ผมหมายถึงโปรแกรมและข้อมูลทั้งหมดที่สร้างขึ้นเพื่อจุดประสงค์ในการแก้ไขข้อผิดพลาด แต่ไม่เคยตั้งใจจะให้รวมอยู่ในผลิตภัณฑ์สุดท้าย...

ผลิตภัณฑ์ ไม่ใช่เรื่องแปลกที่จะมีปริมาณโค้ดในส่วนของนั่งร้านมากถึงครึ่งหนึ่งของปริมาณโค้ดในผลิตภัณฑ์จริง รูปแบบหนึ่งของนั่งร้านคือ ส่วนประกอบจำลอง (dummy component) ซึ่งประกอบด้วยเพียงอินเทอร์เฟซและอาจรวมถึงข้อมูลจำลองหรือกรณีทดสอบขนาดเล็ก ตัวอย่างเช่น ระบบอาจประกอบด้วยโปรแกรมจัดเรียงข้อมูลที่ยังทำไม่เสร็จ ส่วนประกอบข้างเคียงสามารถทดสอบได้โดยใช้โปรแกรมจำลองที่เพียงแค่อ่านและทดสอบรูปแบบของอินพุต และส่งข้อมูลที่ไม่มีความหมายแต่เรียงลำดับและอยู่ในรูปแบบที่ถูกต้องออกมา อีกรูปแบบหนึ่งคือ ไฟล์ขนาดเล็ก (miniature file) บั๊กของระบบในรูปแบบที่พบบ่อยมากคือความเข้าใจผิดเกี่ยวกับรูปแบบของไฟล์ในเทปและดิสก์ ดังนั้นจึงคุ้มค่าที่จะสร้างไฟล์เล็กๆ ที่มีบันทึกข้อมูลตัวอย่างเพียงไม่กี่ชุด แต่มีคำอธิบาย ตัวชี้ (pointers) และอื่นๆ ครบถ้วน กรณีที่จำกัดที่สุดของไฟล์ขนาดเล็กคือไฟล์สมมติ (dummy file) ซึ่งไม่มีข้อมูลอยู่จริงเลย ภาษาควบคุมงาน (Job Control Language) ของ OS/360 มีสิ่งอำนวยความสะดวกดังกล่าว และมันมีประโยชน์อย่างยิ่งสำหรับการแก้ไขข้อผิดพลาดในส่วนประกอบ นั่งร้านอีกรูปแบบหนึ่งคือโปรแกรมเสริม ตัวสร้างข้อมูลทดสอบ โปรแกรมพิมพ์ผลวิเคราะห์พิเศษ โปรแกรมวิเคราะห์ตารางอ้างอิงข้าม ทั้งหมดนี้คือตัวอย่างของอุปกรณ์จับยึดพิเศษที่เราอาจต้องการสร้างขึ้น

ควบคุมการเปลี่ยนแปลง (Control changes): การควบคุมอย่างเข้มงวดในระหว่างการทดสอบเป็นหนึ่งในเทคนิคที่น่าประทับใจของการแก้ไขข้อผิดพลาดในฮาร์ดแวร์ และมันก็สามารถนำมาใช้ได้ดีกับระบบซอฟต์แวร์เช่นกัน ประการแรก จะต้องมีผู้รับผิดชอบ เขาและเขาเพียงผู้เดียวเท่านั้นที่จะเป็นผู้อนุมัติการเปลี่ยนแปลงส่วนประกอบหรือการแทนที่เวอร์ชันหนึ่งด้วยอีกเวอร์ชันหนึ่ง จากนั้น ดังที่ได้พูดคุยกันไปแล้ว จะต้องมีสำเนาของระบบที่ถูกควบคุม: สำเนาของเวอร์ชันล่าสุดที่ถูกล็อกไว้สำหรับใช้ในการทดสอบส่วนประกอบ สำเนาที่อยู่ระหว่างการทดสอบซึ่งมีการติดตั้งการแก้ไข และสำเนา "สนามเด็กเล่น" (playpen) ที่แต่ละคนสามารถทำงานกับส่วนประกอบของตนเองได้ ทั้งการแก้ไขและการต่อเติม

ในโมเดลทางวิศวกรรมของ System/360 เราจะเห็นสายไฟสีม่วงปรากฏขึ้นเป็นครั้งคราวท่ามกลางสายไฟสีเหลืองปกติ เมื่อพบบั๊ก จะมีการทำสองสิ่ง คือ การแก้ไขเฉพาะหน้าถูกคิดค้นและติดตั้งลงในระบบเพื่อให้การทดสอบดำเนินต่อไปได้ การเปลี่ยนแปลงนี้จะใช้สายไฟสีม่วง เพื่อให้มันดูโดดเด่นสะดุดตา และมันจะถูกบันทึกลงในปูม ในขณะเดียวกัน เอกสารการเปลี่ยนแปลงอย่างเป็นทางการจะถูกจัดเตรียมและเข้าสู่กระบวนการออกแบบอัตโนมัติ ในที่สุดสิ่งนี้จะส่งผลให้ได้แบบแปลนและรายการสายไฟที่อัปเดตแล้ว และได้แผงหลังใหม่ที่มีการนำการเปลี่ยนแปลงมาใช้ในรูปแบบวงจรพิมพ์หรือสายไฟสีเหลือง คราวนี้โมเดลทางกายภาพและเอกสารก็กลับมาตรงกันอีกครั้ง และสายไฟสีม่วงก็หายไป

การเขียนโปรแกรมต้องการเทคนิคแบบ "สายไฟสีม่วง" และต้องการการควบคุมอย่างเข้มงวดและความเคารพอย่างลึกซึ้งต่อ "กระดาษ" (เอกสาร) ซึ่งในท้ายที่สุดคือตัวผลิตภัณฑ์จริง ส่วนประกอบสำคัญของเทคนิคดังกล่าวคือการบันทึกการเปลี่ยนแปลงทั้งหมดลงในวารสาร และการแยกแยะที่แสดงให้เห็นอย่างเด่นชัดในซอร์สโค้ด ระหว่างการแก้ไขเฉพาะหน้าแบบรวดเร็ว (quick patches) กับการแก้ไขที่ผ่านการขบคิด ทดสอบ และมีเอกสารประกอบอย่างดี

เพิ่มส่วนประกอบทีละชิ้น (Add one component at a time): หลักการนี้ก็เป็นเรื่องที่เห็นได้ชัดเช่นกัน แต่ความมองโลกในแง่ดีและความขี้เกียจมักเย้ายวนให้เราละเมิดมัน การทำเช่นนี้ต้องใช้ตัวจำลอง (dummies) และนั่งร้านอื่นๆ ซึ่งต้องใช้แรงงาน และสุดท้ายแล้ว แรงงานทั้งหมดนั้นอาจไม่จำเป็นก็ได้มั้ง? บางทีอาจจะไม่มีบั๊กเลยก็ได้? ไม่! จงต้านทานสิ่งยั่วยวนนั้น! นั่นคือหัวใจสำคัญของการทดสอบระบบอย่างเป็นระบบ เราต้องสมมติว่ามันจะมีบั๊กมากมาย และวางแผนขั้นตอนที่เป็นระเบียบเพื่อไล่จับพวกมันออกมา สังเกตว่าเราต้องมีกรณีทดสอบที่ถี่ถ้วน ทดสอบระบบส่วนย่อยหลังจากเพิ่มชิ้นส่วนใหม่แต่ละชิ้น และกรณีทดสอบเก่าที่เคยรันผ่านในส่วนรวมก่อนหน้านี้ จะต้องถูกนำมารันซ้ำในส่วนรวมใหม่เพื่อทดสอบการถดถอยของระบบ (system regression)

การแบ่งช่วงการอัปเดต (Quantize updates): เมื่อระบบเริ่มเป็นรูปเป็นร่างขึ้น ผู้สร้างส่วนประกอบจะปรากฏตัวขึ้นเป็นระยะๆ พร้อมกับเวอร์ชันใหม่ล่าสุดของชิ้นงานของตน—ซึ่งอาจจะเร็วขึ้น เล็กขึ้น ครบถ้วนขึ้น หรือคาดว่าจะมีบั๊กน้อยลง การแทนที่ส่วนประกอบที่ทำงานได้อยู่แล้วด้วยเวอร์ชันใหม่ต้องใช้ขั้นตอนการทดสอบที่เป็นระบบแบบเดียวกับการเพิ่มส่วนประกอบใหม่ แม้ว่าจะใช้เวลาน้อยกว่า เนื่องจากปกติจะมีกรณีทดสอบที่ครบถ้วนและมีประสิทธิภาพมากกว่าพร้อมใช้งานอยู่แล้ว

แต่ละทีมที่สร้างส่วนประกอบอื่นๆ จะใช้เวอร์ชันล่าสุดที่ผ่านการทดสอบแล้วของระบบที่รวมเข้าด้วยกัน เป็นฐานทดสอบ (test bed) สำหรับการแก้ไขข้อผิดพลาดในชิ้นงานของตน งานของพวกเขาจะถดถอยลงหากฐานทดสอบนั้นเปลี่ยนไปต่อหน้าต่อตา แน่นอนว่ามันต้องเปลี่ยน แต่การเปลี่ยนแปลงจำเป็นต้องทำเป็นรอบๆ (quantized) เพื่อให้ผู้ใช้แต่ละคนมีช่วงเวลาของความเสถียรที่สามารถทำงานได้อย่างมีผลิตภาพ สลับด้วยช่วงสั้นๆ ของการเปลี่ยนฐานทดสอบ วิธีนี้ดูเหมือนจะรบกวนการทำงานน้อยกว่าการปล่อยให้มีการเปลี่ยนแปลงที่กระเพื่อมและสั่นไหวอยู่ตลอดเวลา Lehman และ Belady ให้หลักฐานว่ารอบการเปลี่ยนแปลงควรจะใหญ่มากและห่างกันมาก หรือไม่ก็เล็กมากและถี่มาก กลยุทธ์หลังมีความเสี่ยงต่อความไม่เสถียรมากกว่าตามแบบจำลองของพวกเขา ประสบการณ์ของผมยืนยันเช่นนั้น: ผมจะไม่เสี่ยงใช้กลยุทธ์นั้นในทางปฏิบัติเลย การเปลี่ยนแปลงที่เป็นรอบๆ สอดรับกับเทคนิคสายไฟสีม่วงได้อย่างลงตัว การแก้ไขเฉพาะหน้าจะคงอยู่จนกว่าจะถึงการออกเวอร์ชันปกติครั้งถัดไปของส่วนประกอบนั้น ซึ่งควรจะรวมเอาการแก้ไขที่ผ่านการทดสอบและมีเอกสารประกอบแล้วไว้ด้วย