การจัดการ Exception ถือเป็นหนึ่งในแหล่งที่มาของความซับซ้อนที่แย่ที่สุดในระบบซอฟต์แวร์ โค้ดที่ต้องจัดการกับเงื่อนไขพิเศษนั้นเขียนได้ยากกว่าโค้ดที่จัดการกับกรณีปกติโดยธรรมชาติ และนักพัฒนามักจะกำหนด Exception ขึ้นมาโดยไม่คิดว่ามันจะถูกจัดการอย่างไร บทนี้จะอธิบายว่าทำไม Exception จึงสร้างความซับซ้อนอย่างไม่สมส่วน จากนั้นจะแสดงวิธีทำให้การจัดการ Exception ง่ายขึ้น บทเรียนสำคัญโดยรวมจากบทนี้คือการลดจำนวนจุดที่ต้องจัดการ Exception ในหลายกรณี เราสามารถปรับเปลี่ยนความหมายของการทำงานเพื่อให้พฤติกรรมปกติครอบคลุมทุกสถานการณ์และไม่มีเงื่อนไขพิเศษใด ๆ ให้ต้องรายงาน (ซึ่งเป็นที่มาของชื่อบทนี้)
10.1 ทำไม Exception ถึงเพิ่มความซับซ้อน
ผมใช้คำว่า exception เพื่อหมายถึงเงื่อนไขที่ผิดปกตใด ๆ ที่เปลี่ยนแปลงโฟลว์การทำงานปกติของโปรแกรม ภาษาโปรแกรมมิ่งหลายภาษามีกลไก Exception แบบเป็นทางการที่ให้ Exception ถูก throw จากโค้ดระดับล่างและถูก catch โดยโค้ดที่ครอบอยู่ อย่างไรก็ตาม Exception สามารถเกิดขึ้นได้แม้จะไม่ใช้กลไกการรายงาน Exception แบบเป็นทางการ เช่น เมื่อ method คืนค่าพิเศษที่บ่งบอกว่ามันทำงานปกติไม่สำเร็จ Exception ในทุกรูปแบบเหล่านี้ล้วนสร้างความซับซ้อน
โค้ดชิ้นหนึ่ง ๆ อาจพบ Exception ได้หลายทางที่แตกต่างกัน:
- ผู้เรียกอาจส่ง argument หรือข้อมูลการตั้งค่าที่ไม่ถูกต้องมาให้
- method ที่ถูกเรียกอาจไม่สามารถดำเนินการตามที่ร้องขอได้สำเร็จ เช่น การดำเนินการ I/O อาจล้มเหลว หรือทรัพยากรที่ต้องการอาจไม่พร้อมใช้งาน
- ในระบบแบบกระจาย (distributed system) แพ็กเก็ตเครือข่ายอาจสูญหายหรือล่าช้า เซิร์ฟเวอร์อาจไม่ตอบสนองภายในเวลาที่กำหนด หรือ peers อาจสื่อสารในแบบที่ไม่คาดคิด
- โค้ดอาจตรวจพบบั๊ก ความไม่สอดคล้องภายใน หรือสถานการณ์ที่มันไม่พร้อมจะจัดการ
ระบบขนาดใหญ่ต้องจัดการกับเงื่อนไขพิเศษมากมาย โดยเฉพาะอย่างยิ่งถ้าเป็นระบบแบบกระจายหรือต้องทนทานต่อความผิดพลาด (fault-tolerant) การจัดการ Exception อาจกินสัดส่วนที่สำคัญของโค้ดทั้งหมดในระบบ
โค้ดจัดการ Exception นั้นเขียนยากกว่าโค้ดกรณีปกติโดยธรรมชาติ Exception จะขัดขวางโฟลว์ปกติของโค้ด โดยปกติแล้วมันหมายความว่าบางอย่างทำงานไม่เป็นดังที่คาดหวัง เมื่อ Exception เกิดขึ้น โปรแกรมเมอร์สามารถจัดการได้สองวิธี ซึ่งแต่ละวิธีก็ซับซ้อนได้ไม่แพ้กัน วิธีแรกคือเดินหน้าต่อไปและทำงานที่กำลังทำอยู่ให้เสร็จทั้งที่เกิด Exception เช่น ถ้าแพ็กเก็ตเครือข่ายสูญหาย ก็สามารถส่งใหม่ได้ ถ้าข้อมูลเสียหาย ก็อาจกู้คืนจากสำเนาสำรองได้ วิธีที่สองคือยกเลิกการทำงานที่กำลังทำอยู่และรายงาน Exception ขึ้นไปข้างบน อย่างไรก็ตาม การยกเลิกอาจซับซ้อนเพราะ Exception อาจเกิดขึ้น ณ จุดที่สถานะของระบบไม่สอดคล้องกัน (data structure อาจถูก initialize ไปเพียงบางส่วน) โค้ดจัดการ Exception ต้องทำให้สถานะกลับมาสอดคล้องกัน เช่น โดยการยกเลิกการเปลี่ยนแปลงใด ๆ ที่เกิดขึ้นก่อนจะเกิด Exception
ยิ่งไปกว่านั้น โค้ดจัดการ Exception ยังสร้างโอกาสให้เกิด Exception เพิ่มขึ้นอีก ลองพิจารณากรณีการส่งแพ็กเก็ตเครือข่ายที่สูญหายซ้ำ บางทีแพ็กเก็ตอาจไม่ได้สูญหายจริง ๆ แต่แค่ล่าช้า ในกรณีนี้ การส่งแพ็กเก็ตซ้ำจะทำให้มีแพ็กเก็ตซ้ำซ้อนถึงผู้รับ ซึ่งนำไปสู่เงื่อนไขพิเศษใหม่ที่ผู้รับต้องจัดการ หรือพิจารณากรณีการกู้คืนข้อมูลที่สูญหายจากสำเนาสำรอง: ถ้าสำเนาสำรองก็สูญหายไปด้วยล่ะ Exception ทุติยภูมิที่เกิดขึ้นระหว่างการกู้คืนมักจะละเอียดอ่อนและซับซ้อนกว่า Exception หลัก ถ้า Exception ถูกจัดการโดยการยกเลิกการทำงานที่กำลังทำอยู่ ก็ต้องรายงานให้ผู้เรียกทราบเป็น Exception อีกตัว เพื่อป้องกันการเกิด Exception เป็นลูกโซ่ไม่รู้จบ นักพัฒนาต้องหาทางจัดการ Exception ในที่สุดโดยไม่สร้าง Exception เพิ่ม
การสนับสนุน Exception ในภาษาโปรแกรมมิ่งมักจะ verbose และเทอะทะ ซึ่งทำให้โค้ดจัดการ Exception อ่านยาก ตัวอย่างเช่น พิจารณาโค้ดต่อไปนี้ที่อ่านชุดของ tweets จากไฟล์โดยใช้ความสามารถของ Java ในการทำ object serialization และ deserialization:
try (
FileInputStream fileStream =
new FileInputStream(fileName);
BufferedInputStream bufferedStream =
new BufferedInputStream(fileStream);
ObjectInputStream objectStream =
new ObjectInputStream(bufferedStream);
) {
for (int i = 0; i < tweetsPerFile; i++) {
tweets.add((Tweet) objectStream.readObject());
}
}
catch (FileNotFoundException e) {
...
}
catch (ClassNotFoundException e) {
...
}
catch (EOFException e) {
// Not a problem: not all tweet files have full
// set of tweets.
}
catch (IOException e) {
...
}
catch (ClassCastException e) {
...
}
แค่ boilerplate try-catch พื้นฐานก็กินจำนวนบรรทัดโค้ดมากกว่าโค้ดสำหรับการทำงานปกติแล้ว โดยยังไม่ต้องนับโค้ดที่จัดการ Exception จริง ๆ ด้วยซ้ำ การเชื่อมโยงโค้ดจัดการ Exception กับโค้ดกรณีปกตินั้นทำได้ยาก เช่น มันไม่ชัดเจนว่า Exception แต่ละตัวถูกสร้างขึ้นจากจุดไหน ทางเลือกหนึ่งคือการแบ่งโค้ดออกเป็นหลาย ๆ try block ในกรณีสุดโต่ง อาจมี try หนึ่งอันสำหรับโค้ดทุกบรรทัดที่สร้าง Exception ได้ ซึ่งจะทำให้เห็นชัดว่า Exception เกิดจากตรงไหน แต่ try block ต่าง ๆ เองก็ทำให้โฟลว์ของโค้ดขาดตอนและอ่านยากขึ้น ยิ่งไปกว่านั้น โค้ดจัดการ Exception บางส่วนอาจถูก duplicated ไปในหลาย try block
การมั่นใจว่าโค้ดจัดการ Exception ทำงานได้จริงนั้นเป็นเรื่องยาก Exception บางประเภท เช่น I/O error ไม่สามารถจำลองได้ง่ายในสภาพแวดล้อมทดสอบ ดังนั้นการทดสอบโค้ดที่จัดการมันจึงทำได้ยาก Exception ไม่ได้เกิดขึ้นบ่อยในระบบที่ทำงานจริง ดังนั้นโค้ดจัดการ Exception จึงแทบไม่ถูกเรียกใช้งาน บั๊กสามารถซ่อนตัวโดยไม่ถูกตรวจจับเป็นเวลานาน และเมื่อถึงเวลาที่ต้องใช้โค้ดจัดการ Exception จริง ๆ ก็มีโอกาสสูงที่มันจะทำงานไม่ได้ (หนึ่งในคำพูดที่ผมชอบ: "โค้ดที่ไม่เคยถูก execute คือโค้ดที่ใช้ไม่ได้") งานวิจัยล่าสุดพบว่ามากกว่า 90% ของความล้มเหลวร้ายแรงในระบบ distributed data-intensive เกิดจากการจัดการข้อผิดพลาดที่ไม่ถูกต้อง 1 เมื่อโค้ดจัดการ Exception ล้มเหลว การดีบั๊กปัญหาก็ทำได้ยาก เพราะมันเกิดขึ้นน้อยมาก
10.2 Exception ที่มากเกินไป
โปรแกรมเมอร์ยิ่งทำให้ปัญหาเรื่องการจัดการ Exception แย่ลงด้วยการกำหนด Exception ที่ไม่จำเป็น โปรแกรมเมอร์ส่วนใหญ่ถูกสอนว่า การตรวจจับและรายงานข้อผิดพลาดเป็นสิ่งสำคัญ พวกเขามักตีความว่านั่นหมายถึง "ยิ่งตรวจจับข้อผิดพลาดได้มากเท่าไหร่ยิ่งดี" ซึ่งนำไปสู่สไตล์การป้องกันเกินเหตุที่ทุกอย่างที่ดูน่าสงสัยแม้เพียงเล็กน้อยจะถูกปฏิเสธด้วย Exception ส่งผลให้มี Exception ที่ไม่จำเป็นมากมายจนเพิ่มความซับซ้อนของระบบ
ผมเคยทำผิดพลาดแบบนี้เองในการออกแบบภาษา Tcl scripting Tcl มีคำสั่ง unset ที่ใช้ลบตัวแปร ผมกำหนดให้ unset throw error ถ้าตัวแปรนั้นไม่มีอยู่ ตอนนั้นผมคิดว่ามันต้องเป็นบั๊กแน่ ๆ ถ้ามีคนพยายามลบตัวแปรที่ไม่มีอยู่ ดังนั้น Tcl ควรจะรายงานมันออกมา อย่างไรก็ตาม หนึ่งในการใช้งาน unset ที่พบบ่อยที่สุดคือการทำความสะอาดสถานะชั่วคราวที่ถูกสร้างขึ้นจากการดำเนินการก่อนหน้านี้ บ่อยครั้งที่เราคาดเดาได้ยากว่าสถานะอะไรถูกสร้างขึ้นไปบ้าง โดยเฉพาะถ้าการดำเนินการนั้นถูกยกเลิกระหว่างทาง ดังนั้นสิ่งที่ง่ายที่สุดคือการลบตัวแปรทั้งหมดที่อาจถูกสร้างขึ้นมา นิยามของ unset ทำให้เรื่องนี้กลายเป็นเรื่องยุ่งยาก: นักพัฒนาต้องครอบการเรียก unset ด้วย catch statement เพื่อดักจับและละเลย error ที่ unset throw ออกมา เมื่อมองย้อนกลับไป นิยามของคำสั่ง unset ถือเป็นหนึ่งในความผิดพลาดที่ใหญ่ที่สุดของผมในการออกแบบ Tcl
มันยั่วยวนให้ใช้ Exception เป็นทางหนีจากการจัดการสถานการณ์ยาก ๆ แทนที่จะหาวิธีจัดการที่สะอาด ก็แค่ throw Exception แล้วโยนปัญหาให้ผู้เรียก บางคนอาจแย้งว่าวิธีนี้เป็นการเพิ่มอำนาจให้ผู้เรียก เพราะช่วยให้ผู้เรียกแต่ละรายจัดการ Exception ได้ในแบบที่แตกต่างกัน อย่างไรก็ตาม ถ้าคุณเองยังหาทางออกไม่ตกสำหรับสถานการณ์นั้น ก็มีโอกาสสูงที่ผู้เรียกก็ไม่รู้จะจัดการยังไงเหมือนกัน การสร้าง Exception ในสถานการณ์แบบนี้ก็แค่ส่งต่อปัญหาให้คนอื่น และเพิ่มความซับซ้อนให้ระบบ
Exception ที่ถูก throw โดย class ถือเป็นส่วนหนึ่งของ interface ของมัน class ที่มี Exception มากมายจะมี interface ที่ซับซ้อน และมันตื้น (shallow) กว่า class ที่มี Exception น้อยกว่า Exception เป็นองค์ประกอบของ interface ที่ซับซ้อนเป็นพิเศษ มันสามารถ แพร่กระจายขึ้นไปหลายระดับ stack ก่อนจะถูก catch ดังนั้นมันจึงไม่เพียงส่งผลต่อผู้เรียก method เท่านั้น แต่อาจส่งผลต่อผู้เรียกในระดับที่สูงขึ้นไปอีก (รวมถึง interface ของพวกเขาด้วย)
การ throw Exception นั้นง่าย การจัดการมันต่างหากที่ยาก ดังนั้น ความซับซ้อนของ Exception จึงมาจากโค้ดจัดการ Exception วิธีที่ดีที่สุดในการลดความเสียหายด้านความซับซ้อนที่เกิดจากการจัดการ Exception คือ การลดจำนวนจุดที่ต้องจัดการ Exception ที่เหลือของบทนี้จะพูดถึงเทคนิคสี่ประการในการลดจำนวนตัวจัดการ Exception
10.3 ทำให้ Error หมดไปด้วยการออกแบบ
วิธีที่ดีที่สุดในการกำจัดความซับซ้อนของการจัดการ Exception คือการออกแบบ API ของคุณให้ไม่มี Exception ให้ต้องจัดการเลย: ทำให้ Error หมดไปด้วยการออกแบบ (define errors out of existence) นี่อาจฟังดูเหมือนการดูหมิ่นศาสนา แต่มันได้ผลอย่างมากในทางปฏิบัติ ลองพิจารณาคำสั่ง Tcl unset ที่พูดถึงข้างต้น แทนที่จะ throw error เมื่อ unset ถูกขอให้ลบตัวแปรที่ไม่รู้จัก มันควรจะแค่ return โดยไม่ทำอะไรเลย ผมน่าจะเปลี่ยนนิยามของ unset เล็กน้อย: แทนที่จะลบตัวแปร unset ควรรับประกันว่าตัวแปรนั้นไม่มีอยู่แล้ว ด้วยนิยามแรก unset ไม่สามารถทำงานของมันได้ถ้าตัวแปรไม่มีอยู่ ดังนั้นการสร้าง Exception จึงสมเหตุสมผล แต่ด้วยนิยามที่สอง มันเป็นเรื่องธรรมชาติโดยสมบูรณ์ที่ unset จะถูกเรียกด้วยชื่อตัวแปรที่ไม่มีอยู่ ในกรณีนี้ งานของมันเสร็จเรียบร้อยแล้ว ดังนั้นมันก็แค่ return ไม่มีกรณี error ให้ต้องรายงานอีกต่อไป
10.4 ตัวอย่าง: การลบไฟล์ใน Windows
การลบไฟล์เป็นอีกตัวอย่างหนึ่งของวิธีที่ error สามารถถูกทำให้หมดไปได้ ระบบปฏิบัติการ Windows ไม่อนุญาตให้ลบไฟล์ถ้ามันถูกเปิดใช้งานอยู่ใน process ใด ๆ นี่เป็นแหล่งของความหงุดหงิดไม่รู้จบสำหรับนักพัฒนาและผู้ใช้ เพื่อที่จะลบไฟล์ที่กำลังถูกใช้งาน ผู้ใช้ต้องค้นหาทั่วทั้งระบบเพื่อหา process ที่เปิดไฟล์นั้นอยู่ แล้ว kill process นั้น บางครั้งผู้ใช้ยอมแพ้และรีบูตเครื่องเพียงเพื่อจะได้ลบไฟล์สักไฟล์
ระบบปฏิบัติการ Unix กำหนดการลบไฟล์อย่าง elegantly กว่า ใน Unix ถ้าไฟล์กำลังถูกเปิดอยู่ตอนที่ถูกลบ Unix จะไม่ลบไฟล์ทันที แต่จะทำเครื่องหมายไฟล์ว่าถูกกำหนดให้ลบ แล้วการลบก็จะ return success ชื่อไฟล์จะถูกลบออกจาก directory แล้ว ดังนั้นไม่มี process อื่นสามารถเปิดไฟล์เก่าได้อีก และสามารถสร้างไฟล์ใหม่ที่มีชื่อเดียวกันได้ แต่ข้อมูลไฟล์เดิมยังคงอยู่ Process ที่เปิดไฟล์ไว้แล้วยังสามารถอ่านและเขียนมันได้ตามปกติ เมื่อไฟล์ถูกปิดโดย process ทั้งหมดที่เข้าถึงมันอยู่ ข้อมูลของมันก็จะถูกปลดปล่อย
วิธีการของ Unix ทำให้ error สองประเภทหมดไป อย่างแรก การลบจะไม่คืน error อีกต่อไปถ้าไฟล์กำลังถูกใช้งาน การลบสำเร็จ และไฟล์จะถูกลบในที่สุด อย่างที่สอง การลบไฟล์ที่กำลังถูกใช้อยู่ไม่ได้สร้าง Exception ให้กับ process ที่ใช้ไฟล์นั้น ทางเลือกหนึ่งสำหรับปัญหานี้คือการลบไฟล์ทันทีและทำเครื่องหมาย opens ทั้งหมดของไฟล์ให้ใช้งานไม่ได้ ความพยายามใด ๆ โดย process อื่นที่จะอ่านหรือเขียนไฟล์ที่ถูกลบจะล้มเหลว อย่างไรก็ตาม วิธีการนี้จะสร้าง error ใหม่ให้ process เหล่านั้นต้องจัดการ แต่ Unix เลือกที่จะให้พวกเขาเข้าถึงไฟล์ได้ตามปกติ การหน่วงการลบไฟล์ทำให้ error หมดไป
มันอาจดูแปลกที่ Unix อนุญาตให้ process อ่านและเขียนไฟล์ที่ถึงคราวต้องถูกลบ แต่ผมไม่เคยเจอสถานการณ์ที่สิ่งนี้ก่อให้เกิดปัญหาสำคัญเลย นิยามการลบไฟล์ของ Unix ใช้ง่ายกว่ามาก ทั้งสำหรับนักพัฒนาและผู้ใช้ เมื่อเทียบกับนิยามของ Windows
10.5 ตัวอย่าง: เมธอด substring ของ Java
เป็นตัวอย่างสุดท้าย ลองพิจารณาคลาส String ของ Java และเมธอด substring ของมัน เมื่อกำหนด index สองตัวเข้าไปใน string substring จะคืน substring ที่เริ่มจากอักขระตำแหน่ง index แรกและสิ้นสุดที่อักขระก่อน index ที่สอง อย่างไรก็ตาม ถ้า index ใด index หนึ่งอยู่นอกช่วงของ string substring จะ throw IndexOutOfBoundsException Exception นี้ไม่จำเป็นและทำให้การใช้เมธอดนี้ยุ่งยากขึ้น ผมมักพบว่าตัวเองอยู่ในสถานการณ์ที่ index ตัวใดตัวหนึ่งหรือทั้งสองตัวอาจอยู่นอกช่วงของ string และผมอยากจะ extract อักขระทั้งหมดใน string ที่ซ้อนทับกับช่วงที่กำหนดไว้ น่าเสียดายที่สิ่งนี้บังคับให้ผมต้องตรวจสอบ index แต่ละตัวและปรับให้เป็นศูนย์หรือจบที่ปลาย string การเรียกเมธอดหนึ่งบรรทัดกลายเป็นโค้ด 5–10 บรรทัด
เมธอด substring ของ Java จะใช้งานง่ายกว่าถ้ามันปรับค่าต่าง ๆ ให้อัตโนมัติ โดยใช้ API ต่อไปนี้: "คืนค่าอักขระของ string (ถ้ามี) ที่มี index มากกว่าหรือเท่ากับ beginIndex และน้อยกว่า endIndex " นี่คือ API ที่เรียบง่ายและเป็นธรรมชาติ และมันทำให้ Exception IndexOutOfBoundsException หมดไป พฤติกรรมของเมธอดจะถูกนิยามไว้ชัดเจนแม้ว่า index ตัวใดตัวหนึ่งหรือทั้งสองตัวจะติดลบ หรือ beginIndex มากกว่า endIndex วิธีการนี้ทำให้ API ง่ายขึ้นในขณะที่เพิ่มฟังก์ชันการทำงาน ดังนั้นจึงทำให้เมธอดลึก (deeper) ขึ้น ภาษาอื่น ๆ อีกมากมายได้ใช้แนวทาง ที่ไม่มี error นี้ เช่น Python คืนค่า result ว่างสำหรับการ slice list ที่อยู่นอกช่วง
เมื่อผมโต้แย้งเรื่องการทำให้ Error หมดไป บางคนก็แย้งว่าการ throw error จะช่วยจับบั๊กได้ ถ้าเราทำให้ Error หมดไป มันจะไม่ทำให้ซอฟต์แวร์มีบั๊กมากขึ้นหรือ? บางทีนี่อาจเป็นเหตุผลที่นักพัฒนา Java ตัดสินใจว่า substring ควร throw exception วิธีการที่มี error อาจจับบั๊กบางอย่างได้ แต่มันก็เพิ่มความซับซ้อนซึ่งนำไปสู่บั๊กอื่น ๆ ด้วย ในทางที่มี error นักพัฒนาต้องเขียนโค้ดเพิ่มเติมเพื่อหลีกเลี่ยงหรือละเลย error ซึ่งเพิ่มโอกาสเกิดบั๊ก หรือไม่ก็ลืมเขียนโค้ดเพิ่มเติมนั้น ซึ่งในกรณีนั้น error ที่ไม่คาดคิดอาจถูก throw ตอน runtime ในทางตรงกันข้าม การทำให้ Error หมดไปจะทำให้ API ง่ายขึ้นและลดปริมาณโค้ดที่ต้องเขียน
โดยรวมแล้ว วิธีที่ดีที่สุดในการลดบั๊กคือการทำให้ซอฟต์แวร์ง่ายขึ้น
10.6 การซ่อน Exception (Exception masking)
เทคนิคที่สองในการลดจำนวนจุดที่ต้องจัดการ Exception คือ การซ่อน Exception (exception masking) ด้วยวิธีนี้ เงื่อนไขพิเศษจะถูกตรวจจับและจัดการในระดับต่ำของระบบ เพื่อให้ซอฟต์แวร์ระดับสูงไม่ต้องรู้ถึงเงื่อนไขนั้น การซ่อน Exception พบได้ทั่วไปโดยเฉพาะในระบบแบบกระจาย ตัวอย่างเช่น ใน network transport protocol อย่าง TCP แพ็กเก็ตสามารถถูกทิ้งได้จากหลายสาเหตุ เช่น การเสียหายและความแออัด TCP ซ่อนการสูญเสียแพ็กเก็ตโดยการส่งแพ็กเก็ตที่สูญหายซ้ำภายใน implementation ของมัน ดังนั้นข้อมูลทั้งหมดจะถึงปลายทางในที่สุดและ client จะไม่รู้ถึงแพ็กเก็ตที่ถูกทิ้ง
ตัวอย่างที่ถกเถียงกันมากกว่าเรื่องการซ่อน Exception คือใน NFS network file system ถ้า NFS file server ล่มหรือไม่ตอบสนองไม่ว่าด้วยเหตุผลใด client จะส่ง request ไปยัง server ซ้ำแล้วซ้ำเล่าจนกว่าปัญหาจะได้รับการแก้ไขในที่สุด โค้ด file system ระดับต่ำบน client จะไม่รายงาน Exception ใด ๆ ไปยังแอปพลิเคชันที่เรียกใช้ การทำงานที่กำลังดำเนินอยู่ (และด้วยเหตุนี้แอปพลิเคชัน) ก็จะค้างจนกว่าการทำงานจะสำเร็จ ถ้าการค้างกินเวลานานกว่าเล็กน้อย NFS client จะพิมพ์ข้อความบน console ของผู้ใช้ในรูปแบบ "NFS server xyzzy not responding still trying."
ผู้ใช้ NFS มักบ่นว่าแอปพลิเคชันของพวกเขาค้างระหว่างรอให้ NFS server กลับมาทำงานปกติ หลายคนแนะนำว่า NFS ควรยกเลิกการทำงานด้วย Exception แทนที่จะปล่อยให้ค้าง อย่างไรก็ตาม การรายงาน Exception จะทำให้แย่ลง ไม่ใช่ดีขึ้น แอปพลิเคชันไม่สามารถทำอะไรได้มากถ้ามันเสียการเข้าถึงไฟล์ของมัน ทางเลือกหนึ่งคือให้แอปพลิเคชันลอง ดำเนินการไฟล์ใหม่ แต่ก็ยังทำให้แอปพลิเคชันค้างอยู่ดี และการทำ retry ในที่เดียวที่ชั้น NFS ก็ง่ายกว่าการทำที่การเรียก file system ทุกครั้งในทุกแอปพลิเคชัน (compiler ไม่ควรต้องมากังวลเรื่องนี้!) ทางเลือกอื่นคือให้แอปพลิเคชันยกเลิกและคืน error ไปยังผู้เรียกของมัน ก็ไม่น่าที่ผู้เรียกจะรู้วิธีจัดการเช่นกัน ดังนั้นพวกเขาก็จะยกเลิกต่อ ส่งผลให้สภาพแวดล้อมการทำงานของผู้ใช้พังทลาย ผู้ใช้ก็ยังไม่สามารถทำงานได้ในขณะที่ file server down อยู่ดี และพวกเขาจะต้องรีสตาร์ทแอปพลิเคชันทั้งหมดเมื่อ file server กลับมาทำงาน
ดังนั้น ทางเลือกที่ดีที่สุดคือให้ NFS ซ่อน error และปล่อยให้แอปพลิเคชันค้าง ด้วยวิธีนี้ แอปพลิเคชันไม่ต้องมีโค้ดใด ๆ เพื่อจัดการปัญหา server และสามารถทำงานต่อได้อย่างราบรื่นเมื่อ server กลับมา ถ้าผู้ใช้เบื่อที่จะรอ พวกเขาก็สามารถยกเลิกแอปพลิเคชันด้วยตนเองได้เสมอ
การซ่อน Exception ใช้ไม่ได้ในทุกสถานการณ์ แต่มันเป็นเครื่องมือที่ทรงพลังในสถานการณ์ที่มันใช้ได้ มันส่งผลให้ class ลึกขึ้น (deeper) เพราะช่วยลด interface ของ class (Exception ที่ผู้ใช้ต้องรู้ตัวน้อยลง) และเพิ่มฟังก์ชันการทำงานในรูปแบบของโค้ดที่ซ่อน Exception การซ่อน Exception เป็นตัวอย่างของการดึงความซับซ้อนลงไปข้างล่าง (pulling complexity downward)
10.7 การรวม Exception (Exception aggregation)
เทคนิคที่สามในการลดความซับซ้อนที่เกี่ยวข้องกับ Exception คือ การรวม Exception (exception aggregation) แนวคิดเบื้องหลังการรวม Exception คือการจัดการ Exception หลาย ๆ ตัวด้วยโค้ดชิ้นเดียว แทนที่จะเขียนตัวจัดการแยกสำหรับ Exception แต่ละตัว ให้จัดการทั้งหมดในที่เดียวด้วยตัวจัดการเดียว
ลองพิจารณาวิธีการจัดการ parameter ที่หายไปใน Web server Web server จะ implement collection ของ URLs เมื่อ server ได้รับ URL ที่เข้ามา มันจะ dispatch ไปยัง service method ที่เฉพาะกับ URL นั้นเพื่อประมวลผล URL และสร้าง response URL ประกอบด้วย parameter ต่าง ๆ ที่ใช้ในการสร้าง response แต่ละ service method จะเรียก method ระดับต่ำกว่า (ขอเรียกมันว่า getParameter ) เพื่อ extract parameter ที่ต้องการจาก URL ถ้า URL ไม่มี parameter ที่ต้องการ getParameter จะ throw exception
เมื่อนักศึกษาในวิชาออกแบบซอฟต์แวร์ implement server แบบนี้ หลายคนครอบการเรียก getParameter แต่ละครั้งด้วย exception handler ที่แยกจากกันเพื่อ catch Exception NoSuchParameter ดังใน Figure 10.1 ส่งผลให้มี handler จำนวนมาก ซึ่งทั้งหมดทำสิ่งเดียวกันโดยพื้นฐาน (สร้าง error response)
Figure 10.1: โค้ดด้านบน dispatch ไปยัง method ใด method หนึ่งใน Web server ซึ่งแต่ละ method จัดการ URL ที่เฉพาะเจาะจง แต่ละ method เหล่านั้น (ด้านล่าง) ใช้ parameter จาก HTTP request ที่เข้ามา ในรูปนี้ มี exception handler แยกสำหรับการเรียก getParameter แต่ละครั้ง ซึ่งส่งผลให้มีโค้ดที่ซ้ำกัน
วิธีที่ดีกว่าคือการรวม Exception แทนที่จะ catch Exception ในแต่ละ service method ปล่อยให้มัน propagate ขึ้นไปยัง top-level dispatch method ของ Web server ดังใน Figure 10.2 ตัวจัดการเดียวใน method นี้สามารถ catch Exception ทั้งหมดและสร้าง error response ที่เหมาะสมสำหรับ parameter ที่หายไป
วิธีการรวม Exception สามารถขยายไปได้อีกในตัวอย่าง Web มี error อื่น ๆ อีกมากมายนอกจาก parameter ที่หายไปที่สามารถเกิดขึ้นได้ขณะประมวลผลหน้าเว็บ เช่น parameter อาจมี syntax ไม่ถูกต้อง (service method คาดหวัง integer แต่ค่าที่ได้คือ "xyz") หรือผู้ใช้อาจไม่มีสิทธิ์สำหรับการดำเนินการที่ร้องขอ ในแต่ละกรณี error ควรส่งผลให้เกิด error response error ต่างกันเพียงข้อความ error ที่จะใส่ใน response ("parameter 'quantity' not present in URL" หรือ "bad value 'xyz' for 'quantity' parameter; must be positive integer") ดังนั้น ทุกเงื่อนไขที่ส่งผลให้เกิด error response สามารถจัดการด้วย exception handler ตัวเดียวที่ระดับบนสุด ข้อความ error สามารถสร้างขึ้นตอนที่ throw Exception และรวมเป็นตัวแปรใน exception record เช่น getParameter จะสร้างข้อความ "parameter 'quantity' not present in URL" handler ระดับบนสุดจะ extract ข้อความจาก Exception และนำไปรวมใน error response
Figure 10.2: โค้ดนี้ทำงานเทียบเท่ากับ Figure 10.1 แต่การจัดการ Exception ถูกรวมเข้าด้วยกัน: exception handler ตัวเดียวใน dispatcher จับ Exception NoSuchParameter ทั้งหมดจากทุก method ที่เฉพาะกับ URL
การรวม Exception ที่อธิบายในย่อหน้าก่อนหน้ามีคุณสมบัติที่ดีในแง่ของการ encapsulate และการซ่อนข้อมูล (information hiding) ตัวจัดการ Exception ระดับบนสุด encapsulate ความรู้เกี่ยวกับวิธีสร้าง error response แต่มันไม่รู้อะไรเกี่ยวกับ error เฉพาะเจาะจง มันแค่ใช้ข้อความ error ที่ให้มาใน Exception method getParameter encapsulate ความรู้เกี่ยวกับวิธี extract parameter จาก URL และยังรู้วิธีอธิบายข้อผิดพลาดการ extract ในรูปแบบที่มนุษย์อ่านได้ ข้อมูลสองส่วนนี้เกี่ยวข้องกันอย่างใกล้ชิด ดังนั้นการให้พวกมันอยู่ที่เดียวกันจึงสมเหตุสมผล อย่างไรก็ตาม getParameter ไม่รู้อะไรเกี่ยวกับ syntax ของ HTTP error response เมื่อมีการเพิ่มฟังก์ชันการทำงานใหม่เข้าไปใน Web server method ใหม่เช่น getParameter อาจถูกสร้างขึ้นพร้อมกับ error ของตัวเอง ถ้า method ใหม่ throw Exception ในแบบเดียวกับ getParameter (โดยสร้าง Exception ที่สืบทอดจาก superclass เดียวกันและรวมข้อความ error ในแต่ละ Exception) พวกมันก็สามารถเชื่อมต่อกับระบบที่มีอยู่ได้โดยไม่ต้องเปลี่ยนแปลงอื่นใด: handler ระดับบนสุดจะสร้าง error response ให้พวกมันโดยอัตโนมัติ
ตัวอย่างนี้แสดงให้เห็น design pattern ที่มีประโยชน์โดยทั่วไปสำหรับการจัดการ Exception ถ้าระบบประมวลผลชุดของ request การ define Exception ที่ยกเลิก request ปัจจุบัน ทำความสะอาดสถานะของระบบ และดำเนินการต่อกับ request ถัดไปก็มีประโยชน์ Exception จะถูก catch ในที่เดียวใกล้กับส่วนบนสุดของ request-handling loop ของระบบ Exception นี้สามารถถูก throw ได้ ณ จุดใดก็ได้ในการประมวลผล request เพื่อยกเลิก request นั้น สามารถ define subclass ต่าง ๆ ของ Exception สำหรับเงื่อนไขที่แตกต่างกัน Exception ประเภทนี้ควรแยกให้ชัดเจนจาก Exception ที่เป็นอันตรายถึงแก่ระบบทั้งหมด
การรวม Exception ทำงานได้ดีที่สุดถ้า Exception แพร่กระจายขึ้นไปหลายระดับ stack ก่อนที่จะถูกจัดการ สิ่งนี้ทำให้ Exception จาก method มากขึ้นสามารถถูกจัดการในที่เดียวกัน ซึ่งตรงกันข้ามกับการซ่อน Exception: การซ่อนมักจะทำงานได้ดีที่สุดถ้า Exception ถูกจัดการใน method ระดับต่ำ สำหรับการซ่อน method ระดับต่ำมักจะเป็น library method ที่ถูกใช้โดย method อื่น ๆ อีกมากมาย ดังนั้นการปล่อยให้ Exception แพร่กระจายจะเพิ่มจำนวนจุดที่มันถูกจัดการ การซ่อนและการรวม Exception คล้ายกันตรงที่ทั้งสองวิธีวางตัวจัดการ Exception ไว้ในจุดที่สามารถจับ Exception ได้มากที่สุด ทำให้ไม่ต้องสร้าง handler มากมายที่มิฉะนั้นจะต้องถูกสร้างขึ้น
อีกตัวอย่างหนึ่งของการรวม Exception เกิดขึ้นในระบบจัดเก็บข้อมูล RAMCloud สำหรับการกู้คืนจากความล้มเหลว ระบบ RAMCloud ประกอบด้วยชุดของ storage server ที่เก็บสำเนาหลายชุดของแต่ละ object ดังนั้นระบบสามารถกู้คืนจากความล้มเหลวได้หลากหลาย ตัวอย่างเช่น ถ้า server ล่มและสูญเสียข้อมูลทั้งหมด RAMCloud จะสร้างข้อมูลที่สูญหายขึ้นมาใหม่โดยใช้สำเนาที่เก็บอยู่ใน server อื่น Error ยังสามารถเกิดขึ้นในระดับเล็กกว่าได้ เช่น server อาจพบว่า object แต่ละอันเสียหาย
RAMCloud ไม่มีกลไกการกู้คืนแยกสำหรับ error แต่ละประเภท แต่ RAMCloud "เลื่อนขั้น" (promote) error เล็ก ๆ จำนวนมากให้กลายเป็น error ที่ใหญ่ขึ้น ในหลักการ RAMCloud สามารถจัดการ object ที่เสียหายโดยการกู้คืน object นั้นจากสำเนาสำรอง แต่มันไม่ทำอย่างนั้น แต่ถ้ามันพบ object ที่เสียหาย มันจะ crash server ที่มี object นั้น RAMCloud ใช้วิธีนี้เพราะการกู้คืนจาก crash ค่อนข้างซับซ้อน และวิธีนี้ลดจำนวนกลไกการกู้คืนที่แตกต่างกันที่ต้องสร้างขึ้น การสร้างกลไกการกู้คืนสำหรับ server ที่ล่มนั้นหลีกเลี่ยงไม่ได้ ดังนั้น RAMCloud จึงใช้กลไกเดียวกันสำหรับการกู้คืนประเภทอื่น ๆ ด้วย สิ่งนี้ลดปริมาณโค้ดที่ต้องเขียน และยังหมายถึงการกู้คืนจาก server crash ถูกเรียกใช้งานบ่อยขึ้น ส่งผลให้บั๊กในการกู้คืนมีโอกาสถูกค้นพบและแก้ไขมากขึ้น
ข้อเสียอย่างหนึ่งของการเลื่อนขั้น object ที่เสียหายให้กลายเป็น server crash คือมันเพิ่มต้นทุนการกู้คืนอย่างมาก นี่ไม่ใช่ปัญหาใน RAMCloud เพราะ object เสียหายเกิดขึ้นน้อยมาก อย่างไรก็ตาม การเลื่อนขั้น error อาจไม่สมเหตุสมผลสำหรับ error ที่เกิดขึ้นบ่อยครั้ง ตัวอย่างหนึ่งคือ การ crash server ทุกครั้งที่แพ็กเก็ตเครือข่ายสูญหายนั้นไม่สามารถทำได้ในทางปฏิบัติ
วิธีหนึ่งในการคิดเกี่ยวกับการรวม Exception คือมันแทนที่กลไกเฉพาะกิจ (special-purpose) หลายตัว ซึ่งแต่ละตัวถูกปรับแต่งสำหรับสถานการณ์เฉพาะ ด้วยกลไกทั่วไปตัวเดียวที่สามารถจัดการหลายสถานการณ์ได้ นี่เป็นอีกตัวอย่างหนึ่งของประโยชน์ของกลไกทั่วไป (general-purpose mechanisms)
10.8 แค่ crash ไปเลย?
เทคนิคที่สี่ในการลดความซับซ้อนที่เกี่ยวข้องกับการจัดการ Exception คือการ crash แอปพลิเคชัน ในแอปพลิเคชันส่วนใหญ่ จะมี error บางอย่างที่ไม่คุ้มค่าที่จะพยายามจัดการ โดยทั่วไป error เหล่านี้จัดการยากหรือเป็นไปไม่ได้ และไม่ได้เกิดขึ้นบ่อยนัก สิ่งที่ง่ายที่สุดในการตอบสนองต่อ error เหล่านี้คือการพิมพ์ข้อมูล diagnostic แล้ว abort แอปพลิเคชัน
ตัวอย่างหนึ่งคือ error "out of memory" ที่เกิดขึ้นระหว่างการจัดสรรหน่วยความจำ พิจารณาฟังก์ชัน malloc ใน C ซึ่งคืนค่า NULL ถ้ามันไม่สามารถจัดสรร block หน่วยความจำที่ต้องการได้ นี่เป็นพฤติกรรมที่โชคร้าย เพราะมันสมมติว่าผู้เรียก malloc ทุกคนจะตรวจสอบค่าที่คืนมาและดำเนินการที่เหมาะสมถ้าไม่มีหน่วยความจำ แอปพลิเคชันมีการเรียก malloc มากมาย ดังนั้นการตรวจสอบผลลัพธ์หลังการเรียกแต่ละครั้งจะเพิ่มความซับซ้อนอย่างมาก ถ้าโปรแกรมเมอร์ลืมตรวจสอบ (ซึ่งค่อนข้างเป็นไปได้) แล้วแอปพลิเคชันจะ dereference null pointer เมื่อหน่วยความจำหมด ส่งผลให้เกิด crash ที่กลบเกลื่อนปัญหาที่แท้จริง
ยิ่งไปกว่านั้น แอปพลิเคชันไม่สามารถทำอะไรได้มากเมื่อพบว่าหน่วยความจำหมด ในหลักการ แอปพลิเคชันสามารถมองหาหน่วยความจำที่ไม่จำเป็นเพื่อคืน แต่ถ้าแอปพลิเคชันมีหน่วยความจำที่ไม่จำเป็น มันก็สามารถคืนได้ตั้งแต่แรก ซึ่งจะป้องกัน error out-of-memory ไม่ให้เกิดขึ้นตั้งแต่ต้น ระบบในปัจจุบันมีหน่วยความจำมากมายจนแทบไม่มีวันหมด ถ้าหมด ก็มักจะบ่งบอกถึงบั๊กในแอปพลิเคชัน ดังนั้น การพยายามจัดการ error out-of-memory จึงแทบไม่เคยสมเหตุสมผล มันสร้างความซับซ้อนมากเกินไปโดยได้ประโยชน์น้อยเกินไป
วิธีที่ดีกว่าคือการ defining method ใหม่ ckalloc ซึ่งเรียก malloc ตรวจสอบผลลัพธ์ และ abort แอปพลิเคชันพร้อมข้อความ error ถ้าหน่วยความจำหมด แอปพลิเคชันไม่ต้องเรียก malloc โดยตรง มันเรียก ckalloc เสมอ
ในภาษาใหม่เช่น C++ และ Java โอเปอเรเตอร์ new จะ throw exception ถ้าหน่วยความจำหมด การ catch Exception นี้ก็ไม่ค่อยมีประโยชน์ เพราะมีโอกาสสูงที่ exception handler ก็จะพยายามจัดสรรหน่วยความจำเช่นกัน ซึ่งก็จะล้มเหลวอีก หน่วยความจำที่จัดสรรแบบ dynamic นั้นเป็นองค์ประกอบพื้นฐานของแอปพลิเคชันสมัยใหม่ใด ๆ จนการให้แอปพลิเคชันทำงานต่อเมื่อหน่วยความจำหมดนั้นไม่สมเหตุสมผล จะดีกว่าถ้า crash ทันทีที่ตรวจพบ error
ยังมีตัวอย่างอื่น ๆ อีกมากมายของ error ที่การ crash แอปพลิเคชันนั้นสมเหตุสมผล สำหรับโปรแกรมส่วนใหญ่ ถ้า I/O error เกิดขึ้นขณะอ่านหรือเขียนไฟล์ที่เปิดอยู่ (เช่น disk hard error) หรือถ้า network socket ไม่สามารถเปิดได้ แอปพลิเคชันก็ไม่สามารถทำอะไรเพื่อกู้คืนได้มาก ดังนั้นการ abort ด้วยข้อความ error ที่ชัดเจนจึงเป็นวิธีที่สมเหตุสมผล error เหล่านี้เกิดขึ้นไม่บ่อย ดังนั้นมันจึงไม่น่าส่งผลต่อการใช้งานโดยรวมของแอปพลิเคชัน การ abort ด้วยข้อความ error ก็เหมาะสมเช่นกันถ้าแอปพลิเคชันพบ error ภายในเช่น data structure ที่ไม่สอดคล้องกัน เงื่อนไขแบบนี้มักบ่งบอกถึงบั๊กในโปรแกรม
การจะ crash หรือไม่เมื่อเจอ error ใด error หนึ่งนั้นขึ้นอยู่กับแอปพลิเคชัน สำหรับระบบจัดเก็บข้อมูลแบบ replicated การ abort เมื่อเกิด I/O error นั้นไม่เหมาะสม ในทางกลับกัน ระบบต้องใช้ข้อมูลแบบ replicated เพื่อกู้คืนข้อมูลใด ๆ ที่สูญหาย กลไกการกู้คืนจะเพิ่มความซับซ้อนอย่างมากให้กับโปรแกรม แต่การกู้คืนข้อมูลที่สูญหายเป็นส่วนสำคัญของคุณค่าที่ระบบมอบให้กับผู้ใช้
10.9 ออกแบบให้กรณีพิเศษหมดไป
ด้วยเหตุผลเดียวกันที่ทำให้การทำให้ Error หมดไปนั้นสมเหตุสมผล การทำให้กรณีพิเศษอื่น ๆ หมดไปก็สมเหตุสมผลเช่นกัน กรณีพิเศษสามารถส่งผลให้โค้ดเต็มไปด้วย if statement ซึ่งทำให้โค้ดเข้าใจยากและนำไปสู่บั๊ก ดังนั้น ควรกำจัดกรณีพิเศษทุกครั้งที่ทำได้ วิธีที่ดีที่สุดคือการออกแบบกรณีปกติในแบบที่จัดการกรณีพิเศษโดยอัตโนมัติโดยไม่ต้องใช้โค้ดเพิ่มเติม
ในโปรเจกต์ text editor ที่อธิบายใน Chapter 6 นักศึกษาต้อง implement กลไกสำหรับการเลือกข้อความและคัดลอกหรือลบส่วนที่เลือก นักศึกษาส่วนใหญ่ใช้ตัวแปรสถานะใน implementation การเลือกของตนเพื่อบ่งชี้ว่ามีการเลือกอยู่หรือไม่ พวกเขาอาจเลือกวิธีนี้เพราะมีบางครั้งที่ไม่มีการเลือกใดปรากฏบนหน้าจอ ดังนั้นมันจึงดูเป็นธรรมชาติที่จะแสดงแนวคิดนี้ใน implementation อย่างไรก็ตาม วิธีนี้ส่งผลให้มีการตรวจสอบมากมายเพื่อตรวจจับเงื่อนไข "ไม่มีการเลือก" และจัดการมันเป็นกรณีพิเศษ
โค้ดจัดการการเลือกสามารถทำให้ง่ายขึ้นโดยการกำจัดกรณีพิเศษ "ไม่มีการเลือก" เพื่อให้การเลือกมีอยู่เสมอ เมื่อไม่มีการเลือกปรากฏบนหน้าจอ มันสามารถแสดงภายในด้วยการเลือกที่ว่างเปล่า (empty selection) ซึ่งตำแหน่งเริ่มต้นและสิ้นสุดเป็นตำแหน่งเดียวกัน ด้วยวิธีนี้ โค้ดจัดการการเลือกสามารถเขียนได้โดยไม่ต้องตรวจสอบ "ไม่มีการเลือก" เมื่อคัดลอกการเลือก ถ้าการเลือกว่างเปล่า ก็จะแทรก 0 ไบต์ที่ตำแหน่งใหม่ (ถ้า implement อย่างถูกต้อง ก็ไม่ต้องตรวจสอบ 0 ไบต์เป็นกรณีพิเศษ) ในทำนองเดียวกัน ควรออกแบบโค้ดสำหรับการลบการเลือกให้กรณีว่างเปล่าถูกจัดการโดยไม่ต้องตรวจสอบเป็นกรณีพิเศษ พิจารณาการเลือกที่อยู่บรรทัดเดียวทั้งหมด ในการลบการเลือก ให้ extract ส่วนของบรรทัดก่อนการเลือกและต่อกับส่วนของบรรทัดหลังการเลือกเพื่อสร้างบรรทัดใหม่ ถ้าการเลือกว่างเปล่า วิธีนี้จะสร้างบรรทัดเดิมขึ้นมาใหม่
ตัวอย่างนี้ยังแสดงให้เห็นแนวคิด "different layer, different abstraction" จาก Chapter 7 แนวคิดเรื่อง "ไม่มีการเลือก" สมเหตุสมผลในแง่ของผู้ใช้ที่คิดเกี่ยวกับ interface ของแอปพลิเคชัน แต่นั่นไม่ได้หมายความว่ามันต้องถูกแสดงอย่างชัดแจ้งภายในแอปพลิเคชัน การมีการเลือกที่มีอยู่เสมอ แต่บางครั้งก็ว่างเปล่าและมองไม่เห็น ส่งผลให้ implementation ง่ายขึ้น
10.10 การทำมากเกินไป
การทำให้ Exception หายไป หรือซ่อนมันไว้ภายใน module จะสมเหตุสมผลก็ต่อเมื่อข้อมูล Exception ไม่จำเป็นต้องใช้ภายนอก module ซึ่งเป็นจริงสำหรับตัวอย่างในบทนี้ เช่น คำสั่ง Tcl unset และเมธอด Java substring ในสถานการณ์ที่หายากที่ผู้เรียกสนใจเกี่ยวกับกรณีพิเศษที่ถูกตรวจจับโดย Exception ก็ยังมีวิธีอื่นที่ผู้เรียกจะได้รับข้อมูลนี้
อย่างไรก็ตาม การทำแนวคิดนี้มากเกินไปก็เป็นไปได้ ใน module สำหรับการสื่อสารเครือข่าย ทีมนักศึกษาซ่อน Exception ด้านเครือข่ายทั้งหมด: ถ้า network error เกิดขึ้น module จะจับมัน ทิ้งมัน และดำเนินการต่อเหมือนไม่มีปัญหา ซึ่งหมายความว่าแอปพลิเคชันที่ใช้ module ไม่มีทางรู้ว่าข้อมูลสูญหายหรือ peer server ล้มเหลว หากไม่มีข้อมูลนี้ ก็เป็นไปไม่ได้ที่จะสร้างแอปพลิเคชันที่ robust ในกรณีนี้ จำเป็นต้อง expose Exception ออกมา แม้ว่ามันจะเพิ่มความซับซ้อนให้กับ interface ของ module
ในเรื่อง Exception เช่นเดียวกับหลาย ๆ ด้านในการออกแบบซอฟต์แวร์ คุณต้องพิจารณาว่าอะไรสำคัญและอะไรไม่สำคัญ สิ่งที่ไม่สำคัญควรถูก ซ่อนไว้ และยิ่งซ่อนได้มากเท่าไหร่ยิ่งดี แต่เมื่ออะไรบางอย่างสำคัญ มันจะต้องถูกเปิดเผย
10.11 สรุป
กรณีพิเศษในทุกรูปแบบทำให้โค้ดเข้าใจยากขึ้นและเพิ่มโอกาสเกิดบั๊ก บทนี้เน้นที่ Exception ซึ่งเป็นหนึ่งในแหล่งที่มาสำคัญที่สุดของโค้ดกรณีพิเศษ และได้พูดถึงวิธีลดจำนวนจุดที่ต้องจัดการ Exception วิธีที่ดีที่สุดคือการกำหนดความหมายใหม่เพื่อกำจัดเงื่อนไข error สำหรับ Exception ที่ไม่สามารถทำให้หมดไปได้ คุณควรมองหาโอกาสที่จะซ่อนมันในระดับต่ำเพื่อจำกัดผลกระทบของมัน หรือรวมตัวจัดการกรณีพิเศษหลายตัวเข้าด้วยกันเป็นตัวจัดการทั่วไปตัวเดียว เมื่อรวมกันแล้ว เทคนิคเหล่านี้สามารถส่งผลกระทบอย่างมีนัยสำคัญต่อความซับซ้อนโดยรวมของระบบ
1 Ding Yuan et. al., "Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems," 2014 USENIX Conference on Operating System Design and Implementation.