หนึ่งในการตัดสินใจที่พบบ่อยที่สุดในการออกแบบโมดูลใหม่ คือการเลือกว่าจะ implement ให้เป็นแบบ general-purpose หรือ special-purpose บางคนอาจโต้แย้งว่าควรใช้แนวทาง general-purpose ซึ่งคุณจะ implement mechanism ที่สามารถใช้แก้ปัญหาได้หลากหลาย ไม่ใช่แค่ปัญหาที่สำคัญในปัจจุบันเท่านั้น ในกรณีนี้ mechanism ใหม่อาจถูกนำไปใช้ในแบบที่ไม่คาดคิดในอนาคต ซึ่งช่วยประหยัดเวลาได้ แนวทาง general-purpose ดูสอดคล้องกับ mindset การลงทุนที่กล่าวถึงใน Chapter 3 ซึ่งคุณใช้เวลามากขึ้นในช่วงแรกเพื่อประหยัดเวลาในภายหลัง
ในทางกลับกัน เรารู้ว่าการคาดเดาความต้องการในอนาคตของระบบซอฟต์แวร์เป็นเรื่องยาก ดังนั้น solution แบบ general-purpose อาจมีฟังก์ชันที่ไม่มีใครใช้งานจริง นอกจากนี้ ถ้าคุณ implement อะไรที่ general-purpose เกินไป มันอาจทำงานได้ไม่ดีนักในการแก้ปัญหาเฉพาะที่คุณมีอยู่ในปัจจุบัน ผลที่ตามมาคือ บางคนอาจโต้แย้งว่าควรโฟกัสที่ความต้องการในปัจจุบัน สร้างเฉพาะสิ่งที่คุณรู้ว่าต้องการ และปรับให้เหมาะสมกับวิธีที่คุณวางแผนจะใช้ในปัจจุบัน ถ้าคุณใช้แนวทาง special-purpose แล้วค้นพบการใช้งานเพิ่มเติมในภายหลัง คุณก็สามารถ refactor ให้เป็น general-purpose ได้เสมอ แนวทาง special-purpose ดูสอดคล้องกับแนวทางการพัฒนาแบบ incremental
6.1 Make classes somewhat general-purpose (ทำให้คลาสเป็นแบบ somewhat general-purpose)
จากประสบการณ์ของผม จุดที่ลงตัวที่สุดคือการ implement โมดูลใหม่ในลักษณะ somewhat general-purpose คำว่า "somewhat general-purpose" หมายความว่าฟังก์ชันการทำงานของโมดูลควรสะท้อนความต้องการในปัจจุบันของคุณ แต่อินเทอร์เฟซของมันไม่ควรเป็นเช่นนั้น อินเทอร์เฟซควรจะกว้างพอที่จะรองรับการใช้งานได้หลากหลาย อินเทอร์เฟซควรใช้งานง่ายสำหรับความต้องการในปัจจุบันโดยไม่ถูกผูกมัดกับมันมากเกินไป คำว่า "somewhat" มีความสำคัญ: อย่าใจร้อนจนสร้างอะไรที่ general-purpose มากเกินไปจนยากที่จะใช้กับความต้องการปัจจุบันของคุณ
ประโยชน์ที่สำคัญที่สุด (และอาจจะน่าประหลาดใจ) ของแนวทาง general-purpose คือมันให้อินเทอร์เฟซที่เรียบง่ายและลึกกว่าแนวทาง special-purpose แนวทาง general-purpose ยังช่วยประหยัดเวลาในอนาคตได้ด้วย ถ้าคุณนำคลาสไปใช้กับวัตถุประสงค์อื่น อย่างไรก็ตาม ถึงแม้โมดูลจะถูกใช้เฉพาะกับวัตถุประสงค์ดั้งเดิมของมันเท่านั้น แนวทาง general-purpose ก็ยังดีกว่าเพราะความเรียบง่ายของมัน
6.2 Example: storing text for an editor (ตัวอย่าง: การจัดเก็บข้อความสำหรับ editor)
มาดูตัวอย่างจากคลาสออกแบบซอฟต์แวร์ที่นักศึกษาถูกขอให้สร้าง GUI text editors แบบง่ายๆ กัน editor ต้องแสดงไฟล์และให้ผู้ใช้สามารถชี้ คลิก และพิมพ์เพื่อแก้ไขไฟล์ editor ต้องรองรับการแสดงผลไฟล์เดียวกันพร้อมกันหลายมุมมองในหน้าต่างต่างๆ และต้องรองรับการ undo และ redo แบบหลายระดับสำหรับการแก้ไขไฟล์
โปรเจกต์ของนักศึกษาแต่ละคนมีคลาสที่จัดการข้อความพื้นฐานของไฟล์ คลาส text เหล่านี้มักจะมี methods สำหรับโหลดไฟล์เข้าสู่หน่วยความจำ อ่านและแก้ไขข้อความในไฟล์ และเขียนข้อความที่แก้ไขแล้วกลับไปยังไฟล์
ทีมนักศึกษาหลายทีม implement APIs แบบ special-purpose สำหรับคลาส text พวกเขารู้ว่าคลาสนี้จะถูกใช้ใน interactive editor ดังนั้นพวกเขาจึงคิดถึงฟีเจอร์ที่ editor ต้องมีและปรับแต่ง API ของคลาส text ให้เข้ากับฟีเจอร์เฉพาะเหล่านั้น ตัวอย่างเช่น ถ้าผู้ใช้ editor กดปุ่ม backspace editor จะลบอักขระที่อยู่ทางซ้ายของ cursor ทันที ถ้าผู้ใช้กดปุ่ม delete editor จะลบอักขระที่อยู่ทางขวาของ cursor ทันที เมื่อรู้แบบนี้ บางทีมจึงสร้าง method หนึ่งในคลาส text เพื่อรองรับฟีเจอร์เฉพาะแต่ละอย่าง:
void backspace(Cursor cursor);
void delete(Cursor cursor);
แต่ละ method เหล่านี้รับตำแหน่ง cursor เป็น argument โดย type พิเศษที่ชื่อ Cursor แทนตำแหน่งนี้ editor ยังต้องรองรับ selection ที่สามารถ copy หรือลบได้ นักศึกษาจัดการเรื่องนี้โดยกำหนดคลาส Selection และส่ง object ของคลาสนี้ไปยังคลาส text ระหว่างการลบ:
void deleteSelection(Selection selection);
นักศึกษาคงคิดว่าการ implement user interface จะง่ายขึ้นถ้า methods ของคลาส text สอดคล้องกับฟีเจอร์ที่ผู้ใช้มองเห็น แต่ในความเป็นจริงแล้ว การทำ specialization นี้ให้ประโยชน์กับโค้ด user interface เล็กน้อย และสร้าง cognitive load สูงสำหรับนักพัฒนาที่ทำงานบน user interface หรือคลาส text คลาส text ลงเอยด้วย shallow methods จำนวนมาก ซึ่งแต่ละ method เหมาะกับ operation ของ user interface เพียงอย่างเดียว หลายๆ method เช่น delete ถูกเรียกใช้ในที่เดียวเท่านั้น ผลที่ตามมาคือ นักพัฒนาที่ทำงานบน user interface ต้องเรียนรู้ methods จำนวนมากของคลาส text
แนวทางนี้ทำให้เกิด information leakage ระหว่าง user interface และคลาส text abstractions ที่เกี่ยวข้องกับ user interface เช่น selection หรือปุ่ม backspace ถูกสะท้อนอยู่ในคลาส text ซึ่งเพิ่ม cognitive load ให้กับนักพัฒนาที่ทำงานบนคลาส text operation ใหม่แต่ละอย่างของ user interface ต้องมี method ใหม่ถูกกำหนดในคลาส text ดังนั้นนักพัฒนาที่ทำงานบน user interface ก็มักจะต้องมาทำงานบนคลาส text ด้วย เป้าหมายอย่างหนึ่งในการออกแบบคลาสคือการให้แต่ละคลาสสามารถพัฒนาได้อย่างอิสระ แต่แนวทางแบบ specialized กลับผูกมัด user interface และคลาส text เข้าด้วยกัน
6.3 A more general-purpose API (API ที่เป็น general-purpose มากขึ้น)
แนวทางที่ดีกว่าคือการทำให้คลาส text เป็น generic มากขึ้น API ของมันควรถูกกำหนดในแง่ของฟีเจอร์พื้นฐานของข้อความเท่านั้น โดยไม่สะท้อน operation ระดับสูงที่จะ implement ด้วยมัน ตัวอย่างเช่น จำเป็นต้องใช้เพียงสอง methods สำหรับการแก้ไขข้อความ:
void insert(Position position, String newText);
void delete(Position start, Position end);
method แรกแทรก string ใดๆ ที่ตำแหน่งใดๆ ภายในข้อความ และ method ที่สองลบอักขระทั้งหมดที่ตำแหน่งมากกว่าหรือเท่ากับ start แต่น้อยกว่า end API นี้ยังใช้ type ที่เป็น generic มากขึ้นอย่าง Position แทนที่จะเป็น Cursor ซึ่งสะท้อนถึง user interface ที่เฉพาะเจาะจง คลาส text ควรมี facilities แบบ general-purpose สำหรับจัดการตำแหน่งภายในข้อความด้วย เช่น:
Position changePosition(Position position, int numChars);
method นี้คืนตำแหน่งใหม่ที่ห่างจากตำแหน่งที่กำหนดตามจำนวนอักขระที่กำหนด ถ้า argument numChars เป็นบวก ตำแหน่งใหม่จะอยู่หลัง position ในไฟล์ ถ้า numChars เป็นลบ ตำแหน่งใหม่จะอยู่ก่อน position method จะข้ามไปยังบรรทัดถัดไปหรือก่อนหน้าโดยอัตโนมัติเมื่อจำเป็น ด้วย methods เหล่านี้ การกดปุ่ม delete สามารถ implement ได้ด้วยโค้ดต่อไปนี้ (สมมติว่าตัวแปร cursor เก็บตำแหน่ง cursor ปัจจุบัน):
text.delete(cursor, text.changePosition(cursor, 1));
ในทำนองเดียวกัน ปุ่ม backspace สามารถ implement ได้ดังนี้:
text.delete(text.changePosition(cursor, -1), cursor);
ด้วย text API แบบ general-purpose โค้ดสำหรับ implement ฟังก์ชัน user interface อย่าง delete และ backspace จะยาวกว่าแนวทางดั้งเดิมที่ใช้ specialized text API เล็กน้อย อย่างไรก็ตาม โค้ดใหม่นี้ชัดเจนกว่าโค้ดเก่า นักพัฒนาที่ทำงานในโมดูล user interface น่าจะสนใจว่าอักขระใดบ้างที่ถูกลบโดยปุ่ม backspace ด้วยโค้ดใหม่นี้ สิ่งนี้ชัดเจนในทันที ส่วนโค้ดเก่า นักพัฒนาต้องไปที่คลาส text และอ่าน documentation และ/หรือโค้ดของ method backspace เพื่อยืนยันพฤติกรรม นอกจากนี้ แนวทาง general-purpose ยังมีโค้ดโดยรวมน้อยกว่าแนวทาง specialized เนื่องจากมันแทนที่ methods แบบ special-purpose จำนวนมากในคลาส text ด้วย methods แบบ general-purpose ที่มีจำนวนน้อยกว่า
คลาส text ที่ implement ด้วยอินเทอร์เฟซแบบ general-purpose สามารถนำไปใช้กับวัตถุประสงค์อื่นนอกเหนือจาก interactive editor ได้ ตัวอย่างเช่น สมมติว่าคุณกำลังสร้างแอปพลิเคชันที่แก้ไขไฟล์ที่ระบุโดยการแทนที่ทุกครั้งที่พบ string หนึ่งด้วย string อื่น methods จาก specialized text class เช่น backspace และ delete จะมีประโยชน์น้อยสำหรับแอปพลิเคชันนี้ แต่คลาส text แบบ general-purpose จะมีฟังก์ชันส่วนใหญ่ที่จำเป็นสำหรับแอปพลิเคชันใหม่แล้ว สิ่งที่ขาดไปคือ method สำหรับค้นหาการเกิดครั้งถัดไปของ string ที่กำหนด เช่น:
Position findNext(Position start, String string);
แน่นอนว่า interactive text editor มักจะมี mechanism สำหรับการค้นหาและแทนที่ ในกรณีนี้คลาส text ก็จะมี method นี้อยู่แล้ว
6.4 Generality leads to better information hiding (ความเป็นทั่วไปนำไปสู่การซ่อนข้อมูลที่ดีขึ้น)
แนวทาง general-purpose ให้การแยกส่วนที่สะอาดขึ้นระหว่างคลาส text และ user interface ซึ่งส่งผลให้มีการซ่อนข้อมูลที่ดีขึ้น คลาส text ไม่จำเป็นต้องรู้รายละเอียดเฉพาะของ user interface เช่น การจัดการปุ่ม backspace รายละเอียดเหล่านี้ถูก encapsulate ไว้ในคลาส user interface แล้ว สามารถเพิ่มฟีเจอร์ user interface ใหม่ได้โดยไม่ต้องสร้างฟังก์ชันสนับสนุนใหม่ในคลาส text อินเทอร์เฟซแบบ general-purpose ยังลด cognitive load ด้วย: นักพัฒนาที่ทำงานบน user interface แค่ต้องเรียนรู้ methods ง่ายๆ ไม่กี่อย่าง ซึ่งสามารถนำไปใช้ซ้ำได้กับวัตถุประสงค์ต่างๆ
method backspace ในเวอร์ชันดั้งเดิมของคลาส text เป็น false abstraction มันอ้างว่าซ่อนข้อมูลเกี่ยวกับอักขระที่ถูกลบ แต่โมดูล user interface จำเป็นต้องรู้สิ่งนี้จริงๆ นักพัฒนา user interface มักจะอ่านโค้ดของ method backspace เพื่อยืนยันพฤติกรรมที่แม่นยำ การใส่ method ไว้ในคลาส text แค่ทำให้นักพัฒนา user interface ได้ข้อมูลที่ต้องการได้ยากขึ้น หนึ่งในองค์ประกอบที่สำคัญที่สุดของการออกแบบซอฟต์แวร์คือการกำหนดว่าใครจำเป็นต้องรู้อะไร และเมื่อใด เมื่อรายละเอียดมีความสำคัญ ควรทำให้มันชัดเจนและเห็นได้ชัดที่สุดเท่าที่จะทำได้ เช่น การ implement ใหม่ของ operation backspace การซ่อนข้อมูลนี้ไว้เบื้องหลังอินเทอร์เฟซแค่สร้างความคลุมเครือ
6.5 Questions to ask yourself (คำถามที่ควรถามตัวเอง)
การจำแนกการออกแบบคลาสแบบ general-purpose ที่ดีนั้นง่ายกว่าการสร้างมันขึ้นมา นี่คือคำถามบางข้อที่คุณสามารถถามตัวเอง ซึ่งจะช่วยให้คุณหาสมดุลที่เหมาะสมระหว่าง general-purpose และ special-purpose สำหรับอินเทอร์เฟซได้
What is the simplest interface that will cover all my current needs? (อินเทอร์เฟซที่ง่ายที่สุดที่ครอบคลุมความต้องการปัจจุบันทั้งหมดของคุณคืออะไร) ถ้าคุณลดจำนวน methods ใน API โดยไม่ลดความสามารถโดยรวมลง แสดงว่าคุณกำลังสร้าง methods ที่เป็น general-purpose มากขึ้น API text แบบ special-purpose มีอย่างน้อยสาม methods สำหรับการลบข้อความ: backspace , delete , และ deleteSelection API ที่เป็น general-purpose มากขึ้นมีแค่ method เดียวสำหรับการลบข้อความ ซึ่งรองรับทั้งสามวัตถุประสงค์ การลดจำนวน methods จะสมเหตุสมผลก็ต่อเมื่อ API ของแต่ละ method ยังคงเรียบง่าย ถ้าคุณต้องเพิ่ม arguments จำนวนมากเพื่อลดจำนวน methods คุณอาจจะไม่ได้ทำให้อะไรง่ายขึ้นจริงๆ
In how many situations will this method be used? (method นี้จะถูกใช้ในกี่สถานการณ์) ถ้า method ถูกออกแบบมาเพื่อการใช้งานเฉพาะอย่าง เช่น method backspace นั่นคือ red flag ว่ามันอาจจะ special-purpose เกินไป ลองดูว่าคุณสามารถแทนที่ methods แบบ special-purpose หลายๆ ตัวด้วย method แบบ general-purpose ตัวเดียวได้หรือไม่
Is this API easy to use for my current needs? (API นี้ใช้งานง่ายสำหรับความต้องการปัจจุบันของคุณหรือไม่) คำถามนี้ช่วยให้คุณตัดสินได้ว่าคุณทำ API ให้เรียบง่ายและเป็น general-purpose มากเกินไปหรือยัง ถ้าคุณต้องเขียนโค้ดเพิ่มเติมจำนวนมากเพื่อใช้คลาสสำหรับวัตถุประสงค์ปัจจุบันของคุณ นั่นคือ red flag ว่าอินเทอร์เฟซไม่ได้ให้ฟังก์ชันการทำงานที่เหมาะสม ตัวอย่างเช่น แนวทางหนึ่งสำหรับคลาส text คือการออกแบบมันรอบ operation ระดับอักขระเดี่ยว: insert แทรกอักขระเดี่ยวและ delete ลบอักขระเดี่ยว API นี้ทั้งเรียบง่ายและเป็น general-purpose อย่างไรก็ตาม มันจะไม่สะดวกในการใช้งานสำหรับ text editor: โค้ดระดับสูงกว่าจะมี loops มากมายในการแทรกหรือลบช่วงของอักขระ แนวทางระดับอักขระเดี่ยวยังไม่มีประสิทธิภาพสำหรับ operation ขนาดใหญ่ ดังนั้นจึงดีกว่าที่คลาส text จะมี built-in support สำหรับการทำงานกับช่วงของอักขระ
6.6 Conclusion (บทสรุป)
อินเทอร์เฟซแบบ general-purpose มีข้อดีหลายอย่างเหนือกว่าแบบ special-purpose อินเทอร์เฟซเหล่านี้มักจะเรียบง่ายกว่า มี methods น้อยกว่าที่ลึกกว่า นอกจากนี้ยังให้การแยกส่วนระหว่างคลาสที่สะอาดกว่า ในขณะที่อินเทอร์เฟซแบบ special-purpose มักจะทำให้เกิดข้อมูลรั่วไหลระหว่างคลาส การทำให้โมดูลของคุณเป็น somewhat general-purpose เป็นหนึ่งในวิธีที่ดีที่สุดในการลดความซับซ้อนของระบบโดยรวม