หนึ่งในเทคนิคที่สำคัญที่สุดในการจัดการความซับซ้อนของซอฟต์แวร์คือการออกแบบระบบเพื่อให้นักพัฒนาต้องเผชิญกับความซับซ้อนเพียงส่วนเล็กๆ ของภาพรวมในแต่ละช่วงเวลาเท่านั้น แนวทางนี้เรียกว่า modular design และบทนี้จะนำเสนอหลักการพื้นฐานของมัน
4.1 การออกแบบแบบ Modular
ในการออกแบบแบบ modular นั้น ระบบซอฟต์แวร์จะถูกแบ่งออกเป็นกลุ่มของ modules ที่ค่อนข้างเป็นอิสระต่อกัน Modules สามารถมีได้หลายรูปแบบ เช่น classes, subsystems, หรือ services ในโลกอุดมคติ แต่ละ module จะเป็นอิสระจากกันอย่างสมบูรณ์: นักพัฒนาสามารถทำงานใน module ใดก็ได้ โดยไม่จำเป็นต้องรู้เรื่องเกี่ยวกับ module อื่นเลย ในโลกนี้ ความซับซ้อนของระบบจะเท่ากับ ความซับซ้อนของ module ที่แย่ที่สุดเท่านั้น
อย่างไรก็ตาม อุดมคตินี้ไม่สามารถบรรลุได้ Modules ต้องทำงานร่วมกันโดยการเรียกใช้ functions หรือ methods ของกันและกัน ดังนั้น modules จึงจำเป็นต้องรู้ข้อมูลบางอย่างเกี่ยวกับกันและกัน จะเกิด dependencies ระหว่าง modules: ถ้า module หนึ่งเปลี่ยนแปลง modules อื่นอาจต้องเปลี่ยนแปลงตามไปด้วย ตัวอย่างเช่น arguments ของ method สร้าง dependency ระหว่าง method นั้นกับโค้ดที่เรียกใช้ method ถ้า arguments ที่ต้องการเปลี่ยนแปลง การเรียกใช้ method ทั้งหมดจะต้องถูกแก้ไขให้สอดคล้องกับ signature ใหม่ dependencies สามารถมีรูปแบบอื่นๆ อีกมากมาย และอาจซับซ้อนละเอียดอ่อนมาก เป้าหมายของการออกแบบแบบ modular คือการลด dependencies ระหว่าง modules ให้เหลือน้อยที่สุด
ในการจัดการ dependencies เราแบ่งแต่ละ module ออกเป็นสองส่วน: interface และ implementation Interface ประกอบด้วยทุกสิ่งที่นักพัฒนาที่ทำงานใน module อื่นต้องรู้เพื่อที่จะใช้ module นั้น โดยทั่วไป interface จะอธิบาย what (สิ่งที่ module ทำ) แต่ไม่ใช่ how (วิธีการที่มันทำ) Implementation ประกอบด้วยโค้ดที่ดำเนินการตามสัญญาที่ interface ให้ไว้ นักพัฒนาที่ทำงานใน module หนึ่งต้องเข้าใจ interface และ implementation ของ module นั้น รวมถึง interfaces ของ modules อื่นที่ module นั้นเรียกใช้ด้วย นักพัฒนาไม่จำเป็นต้องเข้าใจ implementations ของ modules อื่นนอกเหนือจากที่ตนกำลังทำงานอยู่
ลองพิจารณา module ที่ implement balanced trees module นี้อาจมีโค้ดที่ซับซ้อนในการทำให้แน่ใจว่า tree ยังคงสมดุลอยู่ อย่างไรก็ตาม ความซับซ้อนนี้ไม่ปรากฏให้ผู้ใช้ module เห็น ผู้ใช้จะเห็น interface ที่ค่อนข้างง่ายสำหรับการเรียกใช้ operations เพื่อ insert, remove, และ fetch nodes ใน tree ในการเรียกใช้ insert operation ผู้เรียกเพียงแค่ต้องส่ง key และ value สำหรับ node ใหม่เท่านั้น กลไกในการ traverse tree และ split nodes จะไม่ปรากฏใน interface
สำหรับวัตถุประสงค์ของหนังสือเล่มนี้ module คือหน่วยของโค้ดใดๆ ที่มี interface และ implementation แต่ละ class ในภาษาเชิงวัตถุ (object-oriented) คือ module หนึ่งๆ Methods ภายใน class หรือ functions ในภาษาที่ไม่ใช่เชิงวัตถุ ก็สามารถถือเป็น modules ได้เช่นกัน: แต่ละสิ่งเหล่านี้มี interface และ implementation และเทคนิคการออกแบบแบบ modular สามารถนำไปใช้กับมันได้ subsystems และ services ในระดับที่สูงขึ้นก็เป็น modules เช่นกัน interfaces ของพวกมันอาจมีรูปแบบที่แตกต่างออกไป เช่น kernel calls หรือ HTTP requests การพูดถึง modular design ในหนังสือเล่มนี้ส่วนใหญ่จะเน้นที่การออกแบบ classes แต่เทคนิคและแนวคิดต่างๆ ใช้ได้กับ modules ประเภทอื่นๆ ด้วย
Modules ที่ดีที่สุดคือ modules ที่มี interfaces เรียบง่ายกว่า implementations มาก modules ดังกล่าวมีข้อดีสองประการ ประการแรก interface ที่เรียบง่ายช่วยลดความซับซ้อนที่ module นั้น imposes กับส่วนอื่นๆ ของระบบ ประการที่สอง ถ้า module ถูกแก้ไขในลักษณะที่ไม่เปลี่ยน interface ของมัน ก็จะไม่มี module อื่นใดได้รับผลกระทบจากการแก้ไขนั้น ถ้า interface ของ module เรียบง่ายกว่า implementation มาก ก็จะมีหลายแง่มุมของ module ที่สามารถเปลี่ยนแปลงได้โดยไม่กระทบ modules อื่น
4.2 อะไรอยู่ใน Interface?
interface ของ module ประกอบด้วยข้อมูลสองประเภท: แบบทางการ (formal) และไม่เป็นทางการ (informal) ส่วนที่เป็นทางการของ interface ถูกระบุไว้อย่างชัดเจนในโค้ด และบางส่วนสามารถตรวจสอบความถูกต้องได้โดยภาษา ตัวอย่างเช่น interface ทางการของ method คือ signature ซึ่งประกอบด้วยชื่อและ types ของ parameters, type ของ return value, และข้อมูลเกี่ยวกับ exceptions ที่ถูก throw โดย method นั้น ภาษาโปรแกรมส่วนใหญ่รับประกันว่าการเรียกใช้ method แต่ละครั้งจะส่ง arguments จำนวนและ types ที่ถูกต้องตรงกับ signature ของมัน interface ทางการสำหรับ class ประกอบด้วย signatures ของ public methods ทั้งหมด รวมถึงชื่อและ types ของ public variables ใดๆ
แต่ละ interface ยังรวมถึงองค์ประกอบที่ไม่เป็นทางการ (informal elements) ด้วย สิ่งเหล่านี้ไม่ได้ถูกระบุในแบบที่ ภาษาสามารถเข้าใจหรือบังคับใช้ได้ ส่วนที่ไม่เป็นทางการของ interface รวมถึงพฤติกรรมระดับสูง เช่น ข้อเท็จจริงที่ว่า function ลบไฟล์ที่ถูกระบุชื่อโดย argument ตัวใดตัวหนึ่ง ถ้ามีข้อจำกัดเกี่ยวกับการใช้งาน class (เช่น method หนึ่งต้องถูกเรียกก่อนอีก method หนึ่ง) สิ่งเหล่านี้ก็เป็นส่วนหนึ่งของ interface ของ class เช่นกัน โดยทั่วไป ถ้านักพัฒนาจำเป็นต้องรู้ข้อมูลบางอย่างเพื่อที่จะใช้ module ข้อมูลนั้นก็เป็นส่วนหนึ่งของ interface ของ module แง่มุมที่ไม่เป็นทางการของ interface สามารถอธิบายได้ด้วย comments เท่านั้น และภาษาโปรแกรมไม่สามารถรับประกันได้ว่าคำอธิบายนั้นสมบูรณ์หรือถูกต้อง 1 สำหรับ interfaces ส่วนใหญ่แล้ว แง่มุมที่ไม่เป็นทางการมักจะมีขนาดใหญ่กว่าและซับซ้อนกว่าแง่มุมที่เป็นทางการ
ข้อดีอย่างหนึ่งของ interface ที่ถูกระบุอย่างชัดเจนคือมันบ่งชี้อย่างแม่นยำว่านักพัฒนาต้องรู้อะไรบ้างเพื่อที่จะใช้ module ที่เกี่ยวข้อง ซึ่งช่วยขจัดปัญหา "unknown unknowns" ที่อธิบายไว้ใน Section 2.2 .
4.3 Abstractions (นามธรรม)
คำว่า abstraction (นามธรรม) มีความเกี่ยวข้องอย่างใกล้ชิดกับแนวคิดของการออกแบบแบบ modular Abstraction คือมุมมองที่เรียบง่ายของสิ่งหนึ่ง ซึ่งละเว้นรายละเอียดที่ไม่สำคัญ Abstractions มีประโยชน์เพราะมันทำให้เราคิดและจัดการกับสิ่งที่ซับซ้อนได้ง่ายขึ้น
ในการเขียนโปรแกรมแบบ modular แต่ละ module จะให้ abstraction ในรูปแบบของ interface ของมัน Interface นำเสนอมุมมองที่เรียบง่ายของฟังก์ชันการทำงานของ module รายละเอียดของ implementation นั้นไม่สำคัญจากมุมมองของ abstraction ของ module ดังนั้นจึงถูกละเว้นจาก interface
ในนิยามของ abstraction คำว่า "unimportant" (ไม่สำคัญ) เป็นสิ่งสำคัญ ยิ่งละเว้นรายละเอียดที่ไม่สำคัญออกจาก abstraction ได้มากเท่าไหร่ ก็ยิ่งดีเท่านั้น อย่างไรก็ตาม รายละเอียดจะถูกละเว้นจาก abstraction ได้ก็ต่อเมื่อมันไม่สำคัญเท่านั้น Abstraction สามารถผิดพลาดได้สองทาง ทางแรกคือมันรวมรายละเอียดที่ไม่สำคัญจริงๆ เข้าไป เมื่อเกิดเหตุการณ์นี้ มันทำให้ abstraction ซับซ้อนเกินความจำเป็น ซึ่งเพิ่มภาระทางปัญญา (cognitive load) ให้กับ นักพัฒนาที่ใช้ abstraction นั้น ข้อผิดพลาดที่สองคือ abstraction ละเว้นรายละเอียดที่สำคัญจริงๆ สิ่งนี้ส่งผลให้เกิดความคลุมเครือ: นักพัฒนาที่ดูเพียง abstraction จะไม่มีข้อมูลทั้งหมดที่ต้องการ เพื่อใช้งาน abstraction อย่างถูกต้อง Abstraction ที่ละเว้นรายละเอียดสำคัญเรียกว่า false abstraction (นามธรรมเทียม): มันอาจดูเรียบง่าย แต่ในความเป็นจริงมันไม่ใช่ กุญแจสำคัญในการออกแบบ abstractions คือการเข้าใจว่าสิ่งใดสำคัญ และมองหา designs ที่ลดปริมาณข้อมูลที่สำคัญให้เหลือน้อยที่สุด
ยกตัวอย่างเช่น ระบบไฟล์ (file system) abstraction ที่ระบบไฟล์ให้ไว้จะละเว้นรายละเอียดหลายอย่าง เช่น กลไกในการเลือก blocks บน storage device เพื่อใช้เก็บข้อมูลในไฟล์ใดไฟล์หนึ่ง รายละเอียดเหล่านี้ไม่สำคัญสำหรับผู้ใช้ระบบไฟล์ (ตราบใดที่ระบบยังให้ประสิทธิภาพที่เพียงพอ) อย่างไรก็ตาม รายละเอียบบางอย่างของ implementation ของระบบไฟล์ก็สำคัญสำหรับผู้ใช้ ระบบไฟล์ส่วนใหญ่จะ cache ข้อมูลใน main memory และอาจชะลอการเขียนข้อมูลใหม่ลง storage device เพื่อปรับปรุงประสิทธิภาพ แอปพลิเคชันบางตัว เช่น database จำเป็นต้องรู้อย่างแม่นยำว่าข้อมูลถูกเขียนลง storage เมื่อใด เพื่อให้แน่ใจว่าข้อมูลจะคงอยู่หลังจากระบบล่ม ดังนั้น กฎสำหรับการ flush ข้อมูลไปยัง secondary storage ต้องปรากฏให้เห็นใน interface ของระบบไฟล์
เราพึ่งพา abstractions ในการจัดการความซับซ้อนไม่เพียงแค่ในการเขียนโปรแกรม แต่ยังรวมถึงในชีวิตประจำวันของเราอีกด้วย เตาไมโครเวฟมีอุปกรณ์อิเล็กทรอนิกส์ที่ซับซ้อนเพื่อแปลงกระแสไฟฟ้าสลับเป็นรังสีไมโครเวฟและกระจายรังสีนั้นไปทั่วช่องทำอาหาร โชคดีที่ผู้ใช้เห็น abstraction ที่ง่ายกว่ามาก ซึ่งประกอบด้วยปุ่มสองสามปุ่มสำหรับควบคุมเวลาและความแรงของไมโครเวฟ รถยนต์ให้ abstraction ที่เรียบง่ายที่ช่วยให้เราขับมันได้โดยไม่ต้องเข้าใจกลไกของมอเตอร์ไฟฟ้า การจัดการพลังงานแบตเตอรี่ ระบบเบรกป้องกันล้อล็อก (ABS) cruise control และอื่นๆ
4.4 Deep Modules (โมดูลที่ลึก)
Modules ที่ดีที่สุดคือ modules ที่ให้ฟังก์ชันการทำงานที่ทรงพลังแต่มี interfaces ที่เรียบง่าย ผมใช้คำว่า deep (ลึก) เพื่ออธิบาย modules ดังกล่าว เพื่อให้เห็นภาพของแนวคิดเรื่อง depth ลองนึกภาพว่าแต่ละ module ถูกแทนด้วย สี่เหลี่ยมผืนผ้า ดังที่แสดงใน Figure 4.1 . พื้นที่ของสี่เหลี่ยมผืนผ้าแต่ละอันเป็นสัดส่วนกับฟังก์ชันการทำงานที่ module นั้น implement ขอบบนของสี่เหลี่ยมผืนผ้า แทน interface ของ module; ความยาวของขอบนั้นบ่งบอกถึงความซับซ้อนของ interface Modules ที่ดีที่สุดคือ deep: พวกมันมีฟังก์ชันการทำงานมากมายที่ซ่อนอยู่เบื้องหลัง interface ที่เรียบง่าย Deep module คือ abstraction ที่ดี เพราะมีเพียงส่วนเล็กๆ ของความซับซ้อนภายในเท่านั้นที่ปรากฏต่อผู้ใช้
Figure 4.1: Deep และ shallow modules. Modules ที่ดีที่สุดคือ deep: พวกมันให้สามารถเข้าถึงฟังก์ชันการทำงานมากมายผ่าน interface ที่เรียบง่าย Shallow module คือ module ที่มี interface ค่อนข้างซับซ้อน แต่มีฟังก์ชันการทำงานไม่มาก: มันไม่ได้ซ่อนความซับซ้อนมากนัก
ความลึกของ module (module depth) เป็นวิธีคิดเกี่ยวกับต้นทุนเทียบกับประโยชน์ ประโยชน์ที่ module มอบให้คือฟังก์ชันการทำงานของมัน ต้นทุนของ module (ในแง่ของความซับซ้อนของระบบ) คือ interface ของมัน interface ของ module แสดงถึงความซับซ้อนที่ module สร้างขึ้นกับส่วนอื่นๆ ของระบบ: ยิ่ง interface เล็กลงและเรียบง่ายขึ้นเท่าไหร่ ความซับซ้อนที่มันนำเข้ามาก็ยิ่งน้อยลงเท่านั้น Modules ที่ดีที่สุดคือ modules ที่มีประโยชน์มากที่สุดและมีต้นทุนน้อยที่สุด Interfaces เป็นสิ่งที่ดี แต่ interfaces ที่มากขึ้น หรือใหญ่ขึ้น ไม่ได้ดีกว่าเสมอไป!
กลไกสำหรับ file I/O ที่ระบบปฏิบัติการ Unix และลูกหลาน เช่น Linux จัดหาให้ เป็นตัวอย่างที่สวยงามของ deep interface มี system calls พื้นฐานเพียงห้าตัวสำหรับ I/O พร้อม signatures ที่เรียบง่าย:
int open(const char* path, int flags, mode_t permissions);
ssize_t read(int fd, void* buffer, size_t count);
ssize_t write(int fd, const void* buffer, size_t count);
off_t lseek(int fd, off_t offset, int referencePosition);
int close(int fd);
System call open รับชื่อไฟล์แบบลำดับชั้น เช่น /a/b/c และคืนค่า integer file descriptor ซึ่งใช้เพื่ออ้างถึงไฟล์ที่เปิดอยู่ argument อื่นๆ สำหรับ open ให้ข้อมูลเสริม เช่น ว่าไฟล์ถูกเปิดเพื่อ การอ่านหรือเขียน ควรสร้างไฟล์ใหม่ถ้าไม่มีไฟล์อยู่หรือไม่ และสิทธิ์การเข้าถึงไฟล์ ถ้ามีการสร้างไฟล์ใหม่ System calls read และ write ถ่ายโอนข้อมูลระหว่าง buffer areas ในหน่วยความจำของแอปพลิเคชันกับไฟล์ close สิ้นสุดการเข้าถึงไฟล์ ไฟล์ส่วนใหญ่ถูกเข้าถึงแบบเรียงลำดับ (sequential) ดังนั้นจึงเป็นค่าเริ่มต้น อย่างไรก็ตาม การเข้าถึงแบบสุ่ม (random access) สามารถทำได้โดยการเรียก lseek system call เพื่อเปลี่ยนตำแหน่งการเข้าถึงปัจจุบัน
การ implement Unix I/O interface ในยุคปัจจุบันต้องใช้โค้ดหลายแสนบรรทัด ซึ่งจัดการกับปัญหาที่ซับซ้อน เช่น:
- ไฟล์ถูกแสดงบน disk อย่างไรเพื่อให้เข้าถึงได้อย่างมีประสิทธิภาพ?
- ไดเรกทอรีถูกจัดเก็บอย่างไร และชื่อพาธแบบลำดับชั้นถูกประมวลผลเพื่อค้นหาไฟล์ที่อ้างถึงได้อย่างไร?
- สิทธิ์การเข้าถึงถูกบังคับใช้อย่างไร เพื่อให้ผู้ใช้หนึ่งไม่สามารถแก้ไขหรือลบไฟล์ของผู้ใช้อื่น?
- การเข้าถึงไฟล์ถูก implement อย่างไร? ตัวอย่างเช่น ฟังก์ชันการทำงานถูกแบ่งระหว่าง interrupt handlers และ background code อย่างไร และสองสิ่งนี้สื่อสารกันอย่างปลอดภัยได้อย่างไร?
- นโยบายการจัดตารางเวลา (scheduling policies) แบบใดที่ใช้เมื่อมีการเข้าถึงหลายไฟล์พร้อมกัน?
- ข้อมูลไฟล์ที่ถูกเข้าถึงล่าสุดสามารถถูก cache ในหน่วยความจำเพื่อลดจำนวนการเข้าถึง disk ได้อย่างไร?
- อุปกรณ์จัดเก็บข้อมูลรอง (secondary storage) หลายประเภทที่แตกต่างกัน เช่น disks และ flash drives สามารถถูกรวมเข้าเป็นระบบไฟล์เดียวได้อย่างไร?
ปัญหาเหล่านี้และอีกมากมาย ถูกจัดการโดย implementation ของ Unix file system พวกมันไม่ปรากฏให้ เห็นแก่ programmers ที่เรียกใช้ system calls การ implement Unix I/O interface มีการเปลี่ยนแปลงอย่างมาก ตลอดหลายปีที่ผ่านมา แต่ system calls พื้นฐานทั้งห้าตัวนั้นไม่เคยเปลี่ยน
อีกตัวอย่างหนึ่งของ deep module คือ garbage collector ในภาษาเช่น Go หรือ Java module นี้ไม่มี interface เลย; มันทำงานอย่างไม่ปรากฏให้เห็นเบื้องหลังเพื่อเรียกคืนหน่วยความจำที่ไม่ได้ใช้ การเพิ่ม garbage collection เข้าไปในระบบจริงๆ แล้วทำให้ interface โดยรวมเล็กลง เพราะมันกำจัด interface สำหรับ freeing objects การ implement garbage collector ค่อนข้างซับซ้อน แต่ความซับซ้อนนั้นถูกซ่อนจาก programmers ที่ใช้ภาษา
Deep modules อย่างเช่น Unix I/O และ garbage collectors ให้ abstractions ที่ทรงพลังเพราะมันใช้งานง่าย ในขณะที่ซ่อนความซับซ้อนของ implementation ไว้อย่างมีนัยสำคัญ
4.5 Shallow Modules (โมดูลที่ตื้น)
ในทางกลับกัน shallow module คือ module ที่มี interface ค่อนข้างซับซ้อนเมื่อเทียบกับ ฟังก์ชันการทำงานที่มันให้ ตัวอย่างเช่น class ที่ implement linked lists เป็น shallow ต้องใช้โค้ดไม่มากในการจัดการ linked list (การ insert หรือลบ element ใช้เพียงไม่กี่บรรทัด) ดังนั้น abstraction ของ linked list จึงไม่ได้ซ่อนรายละเอียดมากมาย ความซับซ้อนของ interface ของ linked list เกือบจะเท่ากับ ความซับซ้อนของ implementation ของมัน Shallow classes บางครั้งก็หลีกเลี่ยงไม่ได้ แต่มันไม่ได้ช่วยมากในการจัดการความซับซ้อน
นี่คือตัวอย่างที่รุนแรงของ shallow method ที่นำมาจาก project ในวิชา software design:
private void addNullValueForAttribute(String attribute) {
data.put(attribute, null);
}
จากมุมมองของการจัดการความซับซ้อน method นี้ทำให้แย่ลง ไม่ได้ดีขึ้น method นี้ไม่ได้ให้ abstraction เลย เนื่องจากฟังก์ชันการทำงานทั้งหมดของมันปรากฏผ่าน interface ตัวอย่างเช่น ผู้เรียกอาจต้อง รู้ว่า attribute จะถูกเก็บในตัวแปร data การคิดถึง interface ก็ไม่ได้ง่ายไปกว่าการคิดถึง implementation ทั้งหมด ถ้า method มีการ document อย่างถูกต้อง เอกสารจะยาวกว่าโค้ดของ method เสียอีก มันต้องใช้การกดแป้นพิมพ์มากกว่าในการเรียกใช้ method เมื่อเทียบกับการที่ผู้เรียกจะจัดการตัวแปร data โดยตรง method นี้เพิ่มความซับซ้อน (ในรูปแบบของ interface ใหม่ที่นักพัฒนาต้องเรียนรู้) แต่ไม่ได้ให้ประโยชน์ที่ชดเชยได้
Red Flag: Shallow Module (ธงแดง: โมดูลที่ตื้น)
Shallow module คือ module ที่มี interface ซับซ้อนเมื่อเทียบกับฟังก์ชันการทำงานที่มันให้ Shallow modules ไม่ได้ช่วยมากในการต่อสู้กับความซับซ้อน เพราะประโยชน์ที่มันให้ (ไม่ต้องเรียนรู้ว่ามันทำงานภายในอย่างไร) ถูกหักล้างด้วยต้นทุนของการเรียนรู้และการใช้ interfaces ของมัน Modules ขนาดเล็กมักจะตื้น (shallow)
4.6 Classitis (โรคคลาส)
น่าเสียดายที่คุณค่าของ deep classes ยังไม่เป็นที่ยอมรับอย่างแพร่หลายในปัจจุบัน ภูมิปัญญาดั้งเดิมในการเขียนโปรแกรมคือ classes ควร เล็ก (small) ไม่ใช่ลึก (deep) นักศึกษามักถูกสอนว่าสิ่งสำคัญที่สุดในการออกแบบ class คือการแบ่ง class ใหญ่ๆ ออกเป็น class เล็กๆ คำแนะนำเดียวกันนี้มักถูกให้กับ methods: "method ใดๆ ที่ยาวกว่า N บรรทัดควรถูกแบ่ง ออกเป็นหลาย methods" (N อาจต่ำถึง 10) วิธีการนี้ส่งผลให้เกิด shallow classes และ methods จำนวนมาก ซึ่งเพิ่มความซับซ้อนโดยรวมของระบบ
สุดขั้วของแนวทาง "classes ควรเล็ก" คือกลุ่มอาการที่ผมเรียกว่า classitis (โรคคลาส) ซึ่งเกิดจากความเชื่อที่ผิดว่า "classes เป็นสิ่งดี ดังนั้นยิ่งมี classes มากยิ่งดี" ในระบบที่ประสบกับ classitis นักพัฒนาจะถูกสนับสนุนให้ลดปริมาณฟังก์ชันการทำงานในแต่ละ class ใหม่: ถ้าต้องการฟังก์ชันการทำงานมากขึ้น ก็เพิ่ม classes เข้าไปอีก Classitis อาจทำให้ classes แต่ละตัวดูเรียบง่าย แต่มันเพิ่มความซับซ้อนของระบบโดยรวม Classes ขนาดเล็กไม่ได้มีส่วนช่วยในฟังก์ชันการทำงานมากนัก ดังนั้นจึงต้องมีจำนวนมาก แต่ละตัวก็มี interface ของตัวเอง interfaces เหล่านี้สะสมกันจนเกิดความซับซ้อนมหาศาลในระดับระบบ Classes ขนาดเล็กยังส่งผลให้เกิดรูปแบบการเขียนโปรแกรมที่ยืดเยื้อ (verbose) เนื่องจาก boilerplate ที่จำเป็นสำหรับแต่ละ class
4.7 Examples: Java and Unix I/O (ตัวอย่าง: Java และ Unix I/O)
หนึ่งในตัวอย่างที่ชัดเจนที่สุดของ classitis ในปัจจุบันคือ Java class library ภาษา Java ไม่ได้ต้องการ class ขนาดเล็กจำนวนมาก แต่วัฒนธรรมของ classitis ดูเหมือนจะฝังรากลึกในชุมชนนักเขียนโปรแกรม Java ตัวอย่างเช่น การเปิดไฟล์เพื่ออ่าน serialized objects จากไฟล์นั้น คุณต้องสร้าง objects ที่แตกต่างกันสามตัว:
FileInputStream fileStream =
new FileInputStream(fileName);
BufferedInputStream bufferedStream =
new BufferedInputStream(fileStream);
ObjectInputStream objectStream =
new ObjectInputStream(bufferedStream);
Object FileInputStream ให้ I/O แค่ขั้นพื้นฐานเท่านั้น: มันไม่สามารถทำ buffered I/O ได้ และไม่สามารถอ่านหรือเขียน serialized objects ได้ Object BufferedInputStream เพิ่ม buffering ให้กับ FileInputStream และ ObjectInputStream เพิ่มความสามารถในการอ่านและเขียน serialized objects สอง object แรกในโค้ดด้านบน fileStream และ bufferedStream ไม่เคยถูกใช้หลังจากเปิดไฟล์แล้ว การดำเนินการทั้งหมดในภายหลังจะใช้ objectStream
เป็นสิ่งที่น่ารำคาญเป็นพิเศษ (และเสี่ยงต่อข้อผิดพลาด) ที่ buffering ต้องถูกขออย่างชัดเจนโดยการสร้าง object BufferedInputStream แยกต่างหาก ถ้านักพัฒนาลืมสร้าง object นี้ จะไม่มี buffering และ I/O จะช้า บางทีนักพัฒนา Java อาจแย้งว่าไม่ใช่ทุกคนที่ต้องการใช้ buffering สำหรับ file I/O ดังนั้นมันไม่ควรถูกสร้างไว้ในกลไกพื้นฐาน พวกเขาอาจแย้งว่าควรแยก buffering ออกไป เพื่อให้ผู้คนเลือกได้ว่าจะใช้หรือไม่ การให้ทางเลือกเป็นสิ่งที่ดี แต่ interfaces ควรถูกออกแบบให้กรณีทั่วไป (common case) ง่ายที่สุดเท่าที่จะเป็นไปได้ (ดู formula ในหน้า 6) ผู้ใช้ file I/O เกือบทุกคนต้องการ buffering ดังนั้นมันควรถูกจัดให้เป็นค่าเริ่มต้น สำหรับสถานการณ์ไม่กี่อย่างที่ไม่ต้องการ buffering library ก็สามารถให้กลไกในการปิดมันได้ กลไกใดๆ สำหรับการปิด buffering ควรถูกแยกอย่างชัดเจนใน interface (ตัวอย่างเช่น โดยการให้ constructor ที่แตกต่างสำหรับ FileInputStream หรือผ่าน method ที่ปิดหรือแทนที่กลไกการทำ buffering) เพื่อที่นักพัฒนาส่วนใหญ่ไม่จำเป็นต้องรู้ว่ามันมีอยู่ด้วยซ้ำ
ในทางตรงกันข้าม นักออกแบบ Unix system calls ทำให้กรณีทั่วไปง่าย ตัวอย่างเช่น พวกเขารู้ว่า sequential I/O เป็นเรื่องที่พบบ่อยที่สุด ดังนั้นพวกเขาจึงทำให้มันเป็นพฤติกรรมค่าเริ่มต้น การเข้าถึงแบบสุ่ม (random access) ก็ยังทำได้ค่อนข้างง่าย โดยใช้ lseek system call แต่นักพัฒนาที่ทำแค่ sequential access ไม่จำเป็นต้องรู้เกี่ยวกับกลไกนั้น ถ้า interface มีคุณสมบัติมากมาย แต่นักพัฒนาส่วนใหญ่จำเป็นต้องรู้แค่ไม่กี่อย่าง ความซับซ้อนที่แท้จริงของ interface นั้นก็คือแค่ความซับซ้อนของคุณสมบัติที่ถูกใช้บ่อยเท่านั้น
4.8 Conclusion (บทสรุป)
โดยการแยก interface ของ module ออกจาก implementation ของมัน เราสามารถซ่อนความซับซ้อนของ implementation จากส่วนอื่นๆ ของระบบได้ ผู้ใช้ module จำเป็นต้องเข้าใจเพียงแค่ abstraction ที่ interface ของมันให้ไว้ ประเด็นที่สำคัญที่สุดในการออกแบบ classes และ modules อื่นๆ คือการทำให้พวกมัน deep เพื่อให้มี interfaces ที่เรียบง่ายสำหรับกรณีการใช้งานทั่วไป ในขณะที่ยังคงให้ฟังก์ชันการทำงานที่มีนัยสำคัญ สิ่งนี้จะเพิ่มปริมาณความซับซ้อนที่ถูกซ่อนไว้ให้มากที่สุด
1 มีภาษาบางภาษา ซึ่งส่วนใหญ่อยู่ในชุมชนวิจัย ที่พฤติกรรมโดยรวมของ method หรือ function สามารถถูกอธิบาย อย่างเป็นทางการได้โดยใช้ specification language specification สามารถถูกตรวจสอบโดยอัตโนมัติเพื่อให้แน่ใจว่า มันตรงกับ implementation คำถามที่น่าสนใจคือ specification ที่เป็นทางการเช่นนั้นสามารถแทนที่ ส่วนที่ไม่เป็นทางการของ interface ได้หรือไม่ ความคิดเห็นปัจจุบันของผมคือ interface ที่อธิบายด้วยภาษาอังกฤษ น่าจะเข้าใจง่ายและเป็นธรรมชาติสำหรับนักพัฒนามากกว่า interface ที่เขียนด้วย formal specification language