บทที่ 4 ได้กล่าวว่าโมดูลควรจะลึก (deep) บทนี้และอีกสองสามบทถัดไปจะพูดถึงเทคนิคในการสร้างโมดูลที่ลึก
5.1 Information hiding (การซ่อนข้อมูล)
เทคนิคที่สำคัญที่สุดในการสร้างโมดูลที่ลึกคือ การซ่อนข้อมูล (information hiding) เทคนิคนี้ถูกอธิบายครั้งแรกโดย David Parnas 1 แนวคิดพื้นฐานคือ แต่ละโมดูลควรห่อหุ้มความรู้บางส่วน ซึ่งแสดงถึงการตัดสินใจในการออกแบบ ความรู้นั้นถูกฝังอยู่ใน Implementation ของโมดูล แต่ไม่ปรากฏใน Interface ดังนั้นโมดูลอื่นจึงไม่สามารถมองเห็นได้
ข้อมูลที่ถูกซ่อนอยู่ในโมดูลมักประกอบด้วยรายละเอียดเกี่ยวกับวิธีการ Implement กลไกบางอย่าง ต่อไปนี้คือตัวอย่างบางส่วนของข้อมูลที่อาจถูกซ่อนไว้ในโมดูล:
- วิธีการจัดเก็บข้อมูลใน B-tree และวิธีการเข้าถึงอย่างมีประสิทธิภาพ
- วิธีการระบุบล็อกดิสก์จริงที่สอดคล้องกับแต่ละบล็อกตรรกะภายในไฟล์
- วิธีการ Implement โปรโตคอลเครือข่าย TCP
- วิธีการจัดตารางเวลาธร็ดบนโปรเซสเซอร์แบบ multi-core
- วิธีการแยกวิเคราะห์เอกสาร JSON
ข้อมูลที่ถูกซ่อนรวมถึงโครงสร้างข้อมูล (data structures) และขั้นตอนวิธี (algorithms) ที่เกี่ยวข้องกับกลไกนั้น นอกจากนี้ยังรวมถึงรายละเอียดระดับต่ำ เช่น ขนาดของหน้า (page size) และ รวมถึงแนวคิดระดับสูงที่เป็นนามธรรมมากขึ้น เช่น การสันนิษฐานว่าไฟล์ส่วนใหญ่มีขนาดเล็ก
การซ่อนข้อมูลช่วยลดความซับซ้อนได้สองวิธี วิธีแรก มันทำให้ Interface ของโมดูลง่ายขึ้น Interface จะสะท้อนมุมมองที่เรียบง่ายและเป็นนามธรรมมากขึ้นเกี่ยวกับฟังก์ชันการทำงานของโมดูล และซ่อนรายละเอียดไว้ ซึ่งช่วยลดภาระทางปัญญา (cognitive load) ของนักพัฒนาที่ใช้โมดูลนั้น ตัวอย่างเช่น นักพัฒนาที่ใช้ B-tree class ไม่จำเป็นต้องกังวลเกี่ยวกับ fanout ที่เหมาะสมของโหนดใน tree หรือวิธีการทำให้ tree สมดุล วิธีที่สอง การซ่อนข้อมูลทำให้ระบบพัฒนาและปรับเปลี่ยนได้ง่ายขึ้น หากข้อมูลชิ้นหนึ่งถูกซ่อนไว้ ก็จะไม่มี Dependency ใด ๆ ต่อข้อมูลนั้นนอกโมดูลที่เก็บข้อมูลนั้น ดังนั้นการเปลี่ยนแปลงการออกแบบที่เกี่ยวข้องกับข้อมูลนั้นจะส่งผลกระทบเฉพาะโมดูลนั้นเท่านั้น ตัวอย่างเช่น หากโปรโตคอล TCP เปลี่ยนไป (เช่น เพื่อเพิ่มกลไกใหม่สำหรับ congestion control) Implementation ของโปรโตคอลจะต้องถูกแก้ไข แต่ไม่ควรต้องเปลี่ยนแปลงอะไรในโค้ดระดับสูงที่ใช้ TCP ในการส่งและรับข้อมูล
เมื่อออกแบบโมดูลใหม่ คุณควรคิดอย่างรอบคอบว่าข้อมูลใดบ้างที่สามารถซ่อนไว้ในโมดูลนั้นได้ หากคุณสามารถซ่อนข้อมูลได้มากขึ้น คุณก็ควรจะสามารถทำให้ Interface ของโมดูลง่ายขึ้นได้เช่นกัน และนี่จะทำให้โมดูลลึกขึ้น
ข้อควรจำ: การซ่อนตัวแปรและเมธอดในคลาสโดยการประกาศให้เป็น private ไม่ใช่สิ่งเดียวกันกับการซ่อนข้อมูล (information hiding) สมาชิกแบบ private สามารถช่วยในการซ่อนข้อมูลได้ เพราะมันทำให้ไม่สามารถเข้าถึงสิ่งเหล่านั้นจากภายนอกคลาสโดยตรง อย่างไรก็ตาม ข้อมูลเกี่ยวกับสมาชิกแบบ private ยังคงสามารถถูกเปิดเผยผ่าน public methods เช่น getter และ setter methods ได้ เมื่อเกิดเหตุการณ์เช่นนี้ ลักษณะและการใช้งานของตัวแปรก็จะถูกเปิดเผยไม่ต่างจากตอนที่ตัวแปรเป็น public
รูปแบบที่ดีที่สุดของการซ่อนข้อมูลคือเมื่อข้อมูลถูกซ่อนไว้อย่างสมบูรณ์ภายในโมดูล จนทำให้มันไม่เกี่ยวข้องและมองไม่เห็นสำหรับผู้ใช้โมดูล อย่างไรก็ตาม การซ่อนข้อมูลบางส่วนก็มีคุณค่าเช่นกัน ตัวอย่างเช่น หากคุณสมบัติหรือข้อมูลบางอย่างจำเป็นเฉพาะผู้ใช้คลาสเพียงไม่กี่คน และมันถูกเข้าถึงผ่านเมธอดแยกต่างหากเพื่อไม่ให้ปรากฏให้เห็นในกรณีการใช้งานทั่วไป ข้อมูลนั้นก็ถือว่าถูกซ่อนไว้เป็นส่วนใหญ่ ข้อมูลแบบนี้จะสร้าง Dependency น้อยกว่าข้อมูลที่มองเห็นได้โดยผู้ใช้ทุกคนของคลาส
5.2 Information leakage (การรั่วไหลของข้อมูล)
สิ่งที่ตรงกันข้ามกับการซ่อนข้อมูลคือ การรั่วไหลของข้อมูล (information leakage) การรั่วไหลของข้อมูลเกิดขึ้นเมื่อการตัดสินใจในการออกแบบ (design decision) สะท้อนอยู่ในหลายโมดูล สิ่งนี้สร้าง Dependency ระหว่างโมดูลเหล่านั้น: การเปลี่ยนแปลงใด ๆ ต่อการตัดสินใจในการออกแบบนั้นจะต้องเปลี่ยนแปลง โมดูลที่เกี่ยวข้องทั้งหมด หากข้อมูลบางส่วนปรากฏใน Interface ของโมดูล นั่นหมายความว่าข้อมูลได้รั่วไหลออกไปแล้ว ดังนั้น Interface ที่เรียบง่ายกว่ามักจะสัมพันธ์กับการซ่อนข้อมูลที่ดีกว่า อย่างไรก็ตาม ข้อมูลสามารถรั่วไหลได้แม้จะไม่ปรากฏใน Interface ของโมดูลก็ตาม สมมติว่าสองคลาสต่างมีความรู้เกี่ยวกับรูปแบบไฟล์แบบหนึ่ง (บางทีคลาสหนึ่งอ่านไฟล์ในรูปแบบนั้นและอีกคลาสหนึ่งเขียนไฟล์) แม้ว่าทั้งสองคลาสจะไม่เปิดเผยข้อมูลนั้นใน Interface ของมัน แต่ทั้งคู่ก็ต้องพึ่งพารูปแบบไฟล์นั้น: หากรูปแบบเปลี่ยนไป ทั้งสองคลาสจะต้องถูกแก้ไข การรั่วไหลแบบลับหลัง (back-door leakage) แบบนี้เป็นอันตรายมากกว่าการรั่วไหลผ่าน Interface เพราะมันไม่ชัดเจน
การรั่วไหลของข้อมูลเป็นหนึ่งในสัญญาณเตือน (red flags) ที่สำคัญที่สุดในการออกแบบซอฟต์แวร์ หนึ่งในทักษะที่ดีที่สุดที่คุณสามารถเรียนรู้ได้ในฐานะนักออกแบบซอฟต์แวร์คือการมีระดับความไวสูงต่อการรั่วไหลของข้อมูล หากคุณพบการรั่วไหลของข้อมูลระหว่างคลาส ให้ถามตัวเองว่า "ฉันจะจัดระเบียบคลาสเหล่านี้ใหม่ได้อย่างไร เพื่อให้ความรู้นั้นส่งผลกระทบต่อคลาสเดียวเท่านั้น?" หากคลาสที่ได้รับผลกระทบมีขนาดค่อนข้างเล็กและเกี่ยวข้องอย่างใกล้ชิดกับข้อมูลที่รั่วไหล การรวมคลาสเหล่านั้นเป็นคลาสเดียวอาจเป็นทางเลือกที่สมเหตุสมผล อีกแนวทางหนึ่งคือการดึงข้อมูลออกจากคลาสที่เกี่ยวข้องทั้งหมดและสร้างคลาสใหม่ที่ห่อหุ้มเฉพาะข้อมูลนั้น อย่างไรก็ตาม วิธีนี้จะมีประสิทธิภาพก็ต่อเมื่อคุณสามารถหา Interface ที่เรียบง่ายซึ่งซ่อนรายละเอียดได้ หากคลาสใหม่เปิดเผยความรู้ส่วนใหญ่ผ่าน Interface ของมัน มันก็จะไม่มีคุณค่ามากนัก (คุณเพียงแค่เปลี่ยนจากการรั่วไหลแบบลับหลังเป็นการรั่วไหลผ่าน Interface)
Red Flag: Information Leakage (สัญญาณเตือน: การรั่วไหลของข้อมูล)
การรั่วไหลของข้อมูลเกิดขึ้นเมื่อความรู้เดียวกันถูกใช้ในหลาย ๆ ที่ เช่น สองคลาสที่ต่างก็เข้าใจรูปแบบไฟล์ชนิดเดียวกัน
5.3 Temporal decomposition (การแยกส่วนตามลำดับเวลา)
สาเหตุทั่วไปอย่างหนึ่งของการรั่วไหลของข้อมูลคือรูปแบบการออกแบบที่ผมเรียกว่า temporal decomposition (การแยกส่วนตามลำดับเวลา) ในการแยกส่วนตามลำดับเวลา โครงสร้างของระบบจะสอดคล้องกับลำดับเวลาที่การดำเนินการต่าง ๆ เกิดขึ้น ลองพิจารณาแอปพลิเคชันที่อ่านไฟล์ในรูปแบบใดรูปแบบหนึ่ง แก้ไขเนื้อหาของไฟล์ จากนั้นเขียนไฟล์ออกมาใหม่ ด้วย temporal decomposition แอปพลิเคชันนี้อาจถูกแบ่งออกเป็นสามคลาส: คลาสหนึ่งสำหรับอ่านไฟล์ อีกคลาสสำหรับแก้ไข และคลาสที่สามสำหรับเขียนเวอร์ชันใหม่ ทั้งขั้นตอนการอ่านและเขียนไฟล์ต่างมีความรู้เกี่ยวกับรูปแบบไฟล์ ซึ่งส่งผลให้เกิดการรั่วไหลของข้อมูล วิธีแก้คือรวมกลไกหลักสำหรับการอ่านและเขียนไฟล์ไว้ในคลาสเดียว คลาสนี้จะถูกใช้ทั้งในช่วงการอ่านและการเขียนของแอปพลิเคชัน การตกอยู่ในกับดักของ temporal decomposition นั้นเกิดขึ้นได้ง่าย เพราะลำดับที่การดำเนินการต้องเกิดขึ้นมักจะอยู่ในความคิดของคุณขณะเขียนโค้ด อย่างไรก็ตาม การตัดสินใจในการออกแบบส่วนใหญ่มักปรากฏในหลายช่วงเวลาตลอดอายุของแอปพลิเคชัน ดังนั้น temporal decomposition จึงมักส่งผลให้เกิดการรั่วไหลของข้อมูล
ลำดับมีความสำคัญแน่นอน ดังนั้นมันจะถูกสะท้อนไว้ที่ใดที่หนึ่งในแอปพลิเคชัน อย่างไรก็ตาม มันไม่ควรถูกสะท้อนในโครงสร้างของโมดูล เว้นแต่โครงสร้างนั้นจะสอดคล้องกับการซ่อนข้อมูล (บางทีขั้นตอนต่าง ๆ อาจใช้ข้อมูลที่แตกต่างกันโดยสิ้นเชิง) เมื่อออกแบบโมดูล ให้โฟกัสที่ความรู้ที่จำเป็นในการทำงานแต่ละอย่าง ไม่ใช่ลำดับที่งานเกิดขึ้น
Red Flag: Temporal Decomposition (สัญญาณเตือน: การแยกส่วนตามลำดับเวลา)
ใน temporal decomposition ลำดับการทำงานจะสะท้อนอยู่ในโครงสร้างโค้ด: การดำเนินการที่เกิดขึ้นในเวลาต่างกันจะอยู่ในเมธอดหรือคลาสที่ต่างกัน หากความรู้เดียวกันถูกใช้ในจุดต่าง ๆ ของการทำงาน มันจะถูกเข้ารหัสในหลาย ๆ ที่ ส่งผลให้เกิดการรั่วไหลของข้อมูล
5.4 Example: HTTP server (ตัวอย่าง: HTTP server)
เพื่อแสดงให้เห็นถึงประเด็นในการซ่อนข้อมูล เรามาดูการตัดสินใจออกแบบของนักศึกษาที่ Implement โปรโตคอล HTTP ในวิชาออกแบบซอฟต์แวร์กัน เป็นประโยชน์ที่จะเห็นทั้งสิ่งที่พวกเขาทำได้ดีและจุดที่มีปัญหา
HTTP เป็นกลไกที่เว็บเบราว์เซอร์ใช้สื่อสารกับเว็บเซิร์ฟเวอร์ เมื่อผู้ใช้คลิกลิงก์ในเว็บเบราว์เซอร์หรือส่งฟอร์ม เบราว์เซอร์จะใช้ HTTP เพื่อส่งคำขอ (request) ผ่านเครือข่ายไปยังเว็บเซิร์ฟเวอร์ เมื่อเซิร์ฟเวอร์ประมวลผล คำขอเสร็จแล้ว มันจะส่งการตอบกลับ (response) กลับไปยังเบราว์เซอร์ โดยปกติการตอบกลับจะมีหน้าเว็บใหม่เพื่อแสดงผล โปรโตคอล HTTP กำหนดรูปแบบของคำขอและการตอบกลับ ซึ่งทั้งสองอย่างแสดงในรูปแบบข้อความ รูปที่ 5.1 แสดงตัวอย่างคำขอ HTTP ที่อธิบายการส่งฟอร์ม นักศึกษาในวิชานี้ถูกขอให้ Implement หนึ่งคลาสหรือมากกว่า เพื่อให้เว็บเซิร์ฟเวอร์สามารถรับคำขอ HTTP ขาเข้าและส่งการตอบกลับได้ง่ายขึ้น
รูปที่ 5.1: คำขอแบบ POST ในโปรโตคอล HTTP ประกอบด้วยข้อความที่ส่งผ่าน TCP socket แต่ละคำขอประกอบด้วยบรรทัดเริ่มต้น (initial line) กลุ่มของ headers ที่สิ้นสุดด้วยบรรทัดว่าง และ body ที่เป็นตัวเลือก บรรทัดเริ่มต้นประกอบด้วยชนิดของคำขอ ( POST ใช้สำหรับส่งข้อมูลฟอร์ม), URL ที่ระบุการดำเนินการ ( /comments/create ) และพารามิเตอร์ที่เป็นตัวเลือก ( photo_id มีค่าเป็น 246) และเวอร์ชันของโปรโตคอล HTTP ที่ผู้ส่งใช้ แต่ละบรรทัดของ header ประกอบด้วยชื่อ เช่น Content-Length ตามด้วยค่าของมัน สำหรับคำขอนี้ body จะมีพารามิเตอร์เพิ่มเติม ( comment และ priority )
5.5 Example: too many classes (ตัวอย่าง: คลาสมากเกินไป)
ข้อผิดพลาดที่พบบ่อยที่สุดของนักศึกษาคือการแบ่งโค้ดออกเป็นคลาสตื้น ๆ (shallow classes) จำนวนมาก ซึ่งนำไปสู่การรั่วไหลของข้อมูลระหว่างคลาส ทีมหนึ่งใช้สองคลาสที่แตกต่างกันในการรับคำขอ HTTP; คลาสแรกอ่านคำขอจากการเชื่อมต่อเครือข่ายเป็นสตริง และคลาสที่สองแยกวิเคราะห์สตริง นี่คือตัวอย่างของ temporal decomposition ("ก่อนอื่นเราอ่านคำขอ จากนั้นเราแยกวิเคราะห์มัน") การรั่วไหลของข้อมูลเกิดขึ้นเพราะไม่สามารถอ่านคำขอ HTTP ได้โดยไม่ต้องแยกวิเคราะห์ข้อความส่วนใหญ่ ตัวอย่างเช่น Content-Length header ระบุความยาวของ body ของคำขอ ดังนั้น headers จะต้องถูกแยกวิเคราะห์เพื่อคำนวณความยาวรวมของคำขอ ส่งผลให้ทั้งสองคลาสต้องเข้าใจ โครงสร้างของคำขอ HTTP เป็นส่วนใหญ่ และโค้ดการแยกวิเคราะห์ถูกทำซ้ำในทั้งสองคลาส วิธีนี้ยังสร้างความซับซ้อนเพิ่มเติมให้กับผู้เรียก (callers) ซึ่งต้องเรียกสองเมธอดในคลาสที่ต่างกัน ตามลำดับที่เฉพาะเจาะจง เพื่อรับคำขอ
เพราะคลาสทั้งสองแชร์ข้อมูลกันมาก มันจะดีกว่าถ้ารวมพวกมันเป็นคลาสเดียวที่จัดการทั้งการอ่านและการแยกวิเคราะห์คำขอ สิ่งนี้ให้การซ่อนข้อมูลที่ดีกว่า เพราะมันแยกความรู้ทั้งหมดเกี่ยวกับรูปแบบคำขอไว้ในคลาสเดียว และยังให้ Interface ที่เรียบง่ายกว่าสำหรับผู้เรียก (แค่เรียกเมธอดเดียว)
ตัวอย่างนี้แสดงให้เห็นแนวคิดทั่วไปในการออกแบบซอฟต์แวร์: การซ่อนข้อมูลมักจะสามารถปรับปรุงได้โดยการทำให้คลาสใหญ่ขึ้นเล็กน้อย เหตุผลหนึ่งคือการรวบรวมโค้ดทั้งหมดที่เกี่ยวข้องกับความสามารถหนึ่ง ๆ (เช่น การแยกวิเคราะห์คำขอ HTTP) เข้าด้วยกัน เพื่อให้คลาสที่ได้มีทุกอย่างที่เกี่ยวข้องกับความสามารถนั้น เหตุผลที่สองคือการยกระดับของ Interface; ตัวอย่างเช่น แทนที่จะมีเมธอดแยกกันสำหรับแต่ละขั้นตอนของการคำนวณ ให้มีเมธอดเดียวที่ดำเนินการคำนวณทั้งหมด ซึ่งจะส่งผลให้ Interface เรียบง่ายขึ้น ทั้งสองประโยชน์นี้ใช้ได้กับตัวอย่างในย่อหน้าก่อน: การรวมคลาสเข้าด้วยกันจะรวบรวมโค้ดทั้งหมดที่เกี่ยวข้องกับการแยกวิเคราะห์คำขอ HTTP และแทนที่เมธอดที่มองเห็นจากภายนอกสองตัวด้วยเมธอดเดียว คลาสที่รวมกันแล้วจะลึกกว่าคลาสดั้งเดิม
แน่นอนว่ามันเป็นไปได้ที่จะใช้แนวคิดเรื่องคลาสใหญ่เกินไป (เช่น คลาสเดียวสำหรับทั้งแอปพลิเคชัน) บทที่ 9 จะพูดถึงเงื่อนไขที่การแยกโค้ดออกเป็นคลาสย่อยหลาย ๆ คลาสนั้นสมเหตุสมผล
5.6 Example: HTTP parameter handling (ตัวอย่าง: การจัดการพารามิเตอร์ HTTP)
หลังจากที่เซิร์ฟเวอร์ได้รับคำขอ HTTP แล้ว เซิร์ฟเวอร์จำเป็นต้องเข้าถึงข้อมูลบางส่วนจากคำขอ โค้ดที่จัดการคำขอใน รูปที่ 5.1 อาจต้องรู้ค่าของพารามิเตอร์ photo_id พารามิเตอร์สามารถระบุได้ในบรรทัดแรกของคำขอ ( photo_id ใน รูปที่ 5.1 ) หรือในบางครั้ง ใน body ( comment และ priority ใน รูปที่ 5.1 ) แต่ละพารามิเตอร์มีชื่อและค่า ค่าของพารามิเตอร์ใช้การเข้ารหัสพิเศษที่เรียกว่า URL encoding ; ตัวอย่างเช่น ในค่าของ comment ใน รูปที่ 5.1 "+" ใช้แทนช่องว่าง และ "%21" ใช้แทน "!" ในการประมวลผลคำขอ เซิร์ฟเวอร์จะต้องใช้ค่าสำหรับพารามิเตอร์บางตัว และต้องการให้อยู่ในรูปแบบที่ถอดรหัสแล้ว
โปรเจกต์ของนักศึกษาส่วนใหญ่เลือกทางเลือกที่ดีสองทางเกี่ยวกับการจัดการพารามิเตอร์ อย่างแรก พวกเขาตระหนักว่าแอปพลิเคชันเซิร์ฟเวอร์ไม่สนใจว่าพารามิเตอร์ จะถูกระบุใน header line หรือ body ของคำขอ ดังนั้นพวกเขาจึงซ่อนความแตกต่างนี้จากผู้เรียกและรวมพารามิเตอร์จากทั้งสองตำแหน่งเข้าด้วยกัน อย่างที่สอง พวกเขาซ่อนความรู้เกี่ยวกับ URL encoding: ตัวแยกวิเคราะห์ HTTP จะถอดรหัสค่าพารามิเตอร์ก่อนที่จะส่งกลับไปยังเว็บเซิร์ฟเวอร์ เพื่อให้ค่าของพารามิเตอร์ comment ใน รูปที่ 5.1 จะถูกส่งกลับเป็น "What a cute baby!" ไม่ใช่ "What+a+cute+baby%21" ในทั้งสองกรณีนี้ การซ่อนข้อมูลส่งผลให้ API ง่ายขึ้นสำหรับโค้ดที่ใช้โมดูล HTTP
อย่างไรก็ตาม นักศึกษาส่วนใหญ่ใช้ Interface สำหรับการคืนค่าพารามิเตอร์ที่ตื้นเกินไป และนี่ทำให้สูญเสียโอกาสในการซ่อนข้อมูล โปรเจกต์ส่วนใหญ่ใช้อ็อบเจกต์ชนิด HTTPRequest เพื่อเก็บคำขอ HTTP ที่แยกวิเคราะห์แล้ว และคลาส HTTPRequest มีเมธอดเดียวเช่นต่อไปนี้เพื่อคืนค่าพารามิเตอร์:
public Map<String, String> getParams() {
return this.params;
}
แทนที่จะคืนค่าพารามิเตอร์ตัวเดียว เมธอดนี้คืนค่า reference ไปยัง Map ที่ใช้ภายในเพื่อเก็บพารามิเตอร์ทั้งหมด เมธอดนี้ตื้น และมันเปิดเผยการแสดงผลภายใน (internal representation) ที่คลาส HTTPRequest ใช้เก็บพารามิเตอร์ การเปลี่ยนแปลงใด ๆ ต่อการแสดงผลนั้นจะส่งผลให้เกิดการเปลี่ยนแปลงใน Interface ซึ่งจะต้องแก้ไขผู้เรียกทั้งหมด เมื่อ Implementation ถูกแก้ไข การเปลี่ยนแปลงมักเกี่ยวข้องกับการเปลี่ยนแปลงการแสดงผลของโครงสร้างข้อมูลที่สำคัญ (เช่น เพื่อเพิ่มประสิทธิภาพ) ดังนั้นจึงเป็นสิ่งสำคัญที่จะหลีกเลี่ยงการเปิดเผยโครงสร้างข้อมูลภายในให้มากที่สุด วิธีนี้ยังสร้างงานให้ผู้เรียกมากขึ้น: ผู้เรียกต้องเรียก getParams ก่อน จากนั้นจึงเรียกเมธอดอื่นเพื่อดึงค่าพารามิเตอร์ที่ต้องการออกจาก Map สุดท้าย ผู้เรียกต้องตระหนักว่าพวกเขาไม่ควรแก้ไข Map ที่ส่งกลับโดย getParams เพราะจะส่งผลต่อสถานะภายในของ HTTPRequest
นี่คือ Interface ที่ดีกว่าสำหรับการดึงค่าพารามิเตอร์:
public String getParameter(String name) { ... }
public int getIntParameter(String name) { ... }
getParameter คืนค่าพารามิเตอร์เป็นสตริง มันให้ Interface ที่ลึกกว่า getParams เล็กน้อย; ที่สำคัญกว่านั้น มันซ่อนการแสดงผลภายในของพารามิเตอร์ getIntParameter แปลงค่าของพารามิเตอร์จากรูปแบบสตริงในคำขอ HTTP เป็นจำนวนเต็ม (เช่น พารามิเตอร์ photo_id ใน รูปที่ 5.1 ) สิ่งนี้ช่วยให้ผู้เรียกไม่ต้องขอการแปลงสตริงเป็นจำนวนเต็มแยกต่างหาก และซ่อนกลไกนั้นจากผู้เรียก สามารถกำหนดเมธอดเพิ่มเติมสำหรับชนิดข้อมูลอื่น ๆ เช่น getDoubleParameter ได้หากจำเป็น (เมธอดทั้งหมดนี้จะ throw exception หากพารามิเตอร์ที่ต้องการไม่มีอยู่ หรือไม่สามารถแปลงเป็น ชนิดที่ต้องการ; การประกาศ exception ถูกละเว้นในโค้ดด้านบน)
5.7 Example: defaults in HTTP responses (ตัวอย่าง: ค่าเริ่มต้นในการตอบกลับ HTTP)
โปรเจกต์ HTTP ยังต้องรองรับการสร้างการตอบกลับ HTTP ด้วย ข้อผิดพลาดที่พบบ่อยที่สุดของนักศึกษาในส่วนนี้คือค่าเริ่มต้น (defaults) ที่ไม่เพียงพอ การตอบกลับ HTTP แต่ละครั้งต้องระบุเวอร์ชันของโปรโตคอล HTTP; ทีมหนึ่งกำหนดให้ผู้เรียกระบุเวอร์ชันนี้อย่างชัดเจนเมื่อสร้างอ็อบเจกต์การตอบกลับ อย่างไรก็ตาม เวอร์ชันการตอบกลับต้องสอดคล้องกับในอ็อบเจกต์คำขอ และคำขอนั้นต้องถูกส่งเป็นอาร์กิวเมนต์อยู่แล้วเมื่อส่งการตอบกลับ (มันระบุตำแหน่งที่จะส่งการตอบกลับ) ดังนั้น จึงสมเหตุสมผลกว่าที่คลาส HTTP จะระบุเวอร์ชันการตอบกลับโดยอัตโนมัติ ผู้เรียกไม่น่าจะรู้ว่าจะระบุเวอร์ชันใด และหากผู้เรียกระบุค่าขึ้นมา ก็อาจทำให้เกิดการรั่วไหลของข้อมูลระหว่าง HTTP library และผู้เรียก การตอบกลับ HTTP ยังรวมถึง Date header ที่ระบุเวลาที่ส่งการตอบกลับ; HTTP library ควรให้ค่าเริ่มต้นที่เหมาะสมสำหรับสิ่งนี้เช่นกัน
ค่าเริ่มต้นแสดงให้เห็นถึงหลักการที่ว่า Interface ควรถูกออกแบบมาเพื่อทำให้กรณีทั่วไปง่ายที่สุดเท่าที่จะเป็นไปได้ พวกมันยังเป็นตัวอย่างของการซ่อนข้อมูลบางส่วน: ในกรณีปกติ ผู้เรียกไม่จำเป็นต้องรู้ถึงการมีอยู่ของค่าที่ถูกตั้งค่าเริ่มต้น ในกรณีที่หายากซึ่งผู้เรียกจำเป็นต้องแทนที่ค่าเริ่มต้น พวกเขาจะต้องรู้เกี่ยวกับค่านั้นและสามารถเรียกเมธอดพิเศษเพื่อแก้ไขมัน
ทุกครั้งที่ทำได้ คลาสควร "ทำสิ่งที่ถูกต้อง" โดยไม่ต้องถูกขออย่างชัดเจน ค่าเริ่มต้นเป็นตัวอย่างหนึ่งของเรื่องนี้ ตัวอย่าง Java I/O ในหน้าที่ 26 แสดงให้เห็นประเด็นนี้ในเชิงลบ การทำ buffering ใน I/O ของไฟล์เป็นสิ่งที่พึงปรารถนาอย่างเป็นสากลจนไม่มีใครควรต้องขออย่างชัดแจ้ง หรือแม้แต่รับรู้ถึงการมีอยู่ของมัน; คลาส I/O ควรทำสิ่งที่ถูกต้องและจัดหาให้โดยอัตโนมัติ คุณสมบัติที่ดีที่สุดคือ คุณสมบัติที่คุณได้มาโดยไม่รู้ด้วยซ้ำว่ามันมีอยู่
Red Flag: Overexposure (สัญญาณเตือน: การเปิดเผยมากเกินไป)
หาก API สำหรับคุณสมบัติที่ใช้งานบ่อยบังคับให้ผู้ใช้ต้องเรียนรู้เกี่ยวกับคุณสมบัติอื่น ๆ ที่ไม่ค่อยได้ใช้ สิ่งนี้จะเพิ่มภาระทางปัญญาให้กับผู้ใช้ที่ไม่ต้องการคุณสมบัติที่ไม่ค่อยได้ใช้เหล่านั้น
5.8 Information hiding within a class (การซ่อนข้อมูลภายในคลาส)
ตัวอย่างในบทนี้เน้นที่การซ่อนข้อมูลที่เกี่ยวข้องกับ API ที่มองเห็นจากภายนอกของคลาส แต่การซ่อนข้อมูลยังสามารถนำไปใช้ในระดับอื่น ๆ ของระบบได้ เช่น ภายในคลาส ลองออกแบบเมธอด private ภายในคลาสเพื่อให้แต่ละเมธอดห่อหุ้มข้อมูลหรือความสามารถบางอย่างและซ่อนมันจากส่วนอื่นของคลาส นอกจากนี้ พยายามลดจำนวนสถานที่ที่ตัวแปรแต่ละตัวถูกใช้งานลง ตัวแปรบางตัวอาจต้องถูกเข้าถึงอย่างกว้างขวางทั่วทั้งคลาส แต่ตัวอื่นอาจจำเป็นในไม่กี่ที่เท่านั้น; หากคุณสามารถลดจำนวนที่ที่ตัวแปรถูกใช้งานได้ คุณจะกำจัด Dependency ภายในคลาสและลดความซับซ้อนของมันลง
5.9 Taking it too far (การทำมากเกินไป)
การซ่อนข้อมูลจะสมเหตุสมผลก็ต่อเมื่อข้อมูลที่ถูกซ่อนนั้นไม่จำเป็นต้องใช้ภายนอกโมดูลของมัน หากข้อมูลจำเป็นต้องใช้ภายนอกโมดูล คุณก็ต้อง ไม่ ซ่อนมัน สมมติว่าประสิทธิภาพของโมดูลขึ้นอยู่กับพารามิเตอร์การตั้งค่าบางอย่าง และการใช้งานโมดูลที่แตกต่างกันจะต้องมีการตั้งค่าพารามิเตอร์ที่แตกต่างกัน ในกรณีนี้ สิ่งสำคัญคือพารามิเตอร์เหล่านี้ต้องถูกเปิดเผยใน Interface ของโมดูล เพื่อให้สามารถปรับแต่งได้อย่างเหมาะสม ในฐานะนักออกแบบซอฟต์แวร์ เป้าหมายของคุณควรลดปริมาณข้อมูลที่จำเป็นต้องใช้ภายนอกโมดูล; ตัวอย่างเช่น หากโมดูลสามารถปรับแต่งการตั้งค่าของมันเองได้โดยอัตโนมัติ ก็จะดีกว่าการเปิดเผยพารามิเตอร์การตั้งค่า แต่สิ่งสำคัญคือต้องตระหนักว่าข้อมูลใดที่จำเป็นต้องใช้ภายนอกโมดูล และมั่นใจว่ามันถูกเปิดเผย
5.10 Conclusion (บทสรุป)
การซ่อนข้อมูลและโมดูลที่ลึกมีความสัมพันธ์กันอย่างใกล้ชิด หากโมดูลซ่อนข้อมูลไว้มาก มันมีแนวโน้มที่จะเพิ่มปริมาณฟังก์ชันการทำงานที่โมดูลให้ไว้ ในขณะเดียวกันก็ลดขนาด Interface ลง สิ่งนี้ทำให้โมดูลลึกขึ้น ในทางกลับกัน หากโมดูลไม่ซ่อนข้อมูลมากนัก ไม่ว่ามันจะมีฟังก์ชันการทำงานไม่มาก หรือมี Interface ที่ซับซ้อน ไม่ทางใดก็ทางหนึ่ง โมดูลนั้นก็จะตื้น
เมื่อแบ่งระบบออกเป็นโมดูล พยายามอย่าให้อิทธิพลของลำดับการดำเนินการที่จะเกิดขึ้นใน runtime มีผลต่อคุณ เพราะนั่นจะนำคุณไปสู่เส้นทางของ temporal decomposition ซึ่งจะส่งผลให้เกิดการรั่วไหลของข้อมูลและโมดูลที่ตื้น แต่ให้คิดถึงความรู้ชิ้นต่าง ๆ ที่จำเป็นในการทำงานของแอปพลิเคชันของคุณ และออกแบบแต่ละโมดูลให้ห่อหุ้มความรู้หนึ่งหรือสองสามชิ้น สิ่งนี้จะสร้างการออกแบบที่สะอาดและเรียบง่ายพร้อมโมดูลที่ลึก
1 David Parnas, "On the Criteria to be Used in Decomposing Systems into Modules," Communications of the ACM , ธันวาคม 1972