บทที่ 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)

img Red Flag: Information Leakage (สัญญาณเตือน: การรั่วไหลของข้อมูล) img

การรั่วไหลของข้อมูลเกิดขึ้นเมื่อความรู้เดียวกันถูกใช้ในหลาย ๆ ที่ เช่น สองคลาสที่ต่างก็เข้าใจรูปแบบไฟล์ชนิดเดียวกัน

5.3    Temporal decomposition (การแยกส่วนตามลำดับเวลา)

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

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

img Red Flag: Temporal Decomposition (สัญญาณเตือน: การแยกส่วนตามลำดับเวลา) img

ใน temporal decomposition ลำดับการทำงานจะสะท้อนอยู่ในโครงสร้างโค้ด: การดำเนินการที่เกิดขึ้นในเวลาต่างกันจะอยู่ในเมธอดหรือคลาสที่ต่างกัน หากความรู้เดียวกันถูกใช้ในจุดต่าง ๆ ของการทำงาน มันจะถูกเข้ารหัสในหลาย ๆ ที่ ส่งผลให้เกิดการรั่วไหลของข้อมูล

5.4    Example: HTTP server (ตัวอย่าง: HTTP server)

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

HTTP เป็นกลไกที่เว็บเบราว์เซอร์ใช้สื่อสารกับเว็บเซิร์ฟเวอร์ เมื่อผู้ใช้คลิกลิงก์ในเว็บเบราว์เซอร์หรือส่งฟอร์ม เบราว์เซอร์จะใช้ HTTP เพื่อส่งคำขอ (request) ผ่านเครือข่ายไปยังเว็บเซิร์ฟเวอร์ เมื่อเซิร์ฟเวอร์ประมวลผล คำขอเสร็จแล้ว มันจะส่งการตอบกลับ (response) กลับไปยังเบราว์เซอร์ โดยปกติการตอบกลับจะมีหน้าเว็บใหม่เพื่อแสดงผล โปรโตคอล HTTP กำหนดรูปแบบของคำขอและการตอบกลับ ซึ่งทั้งสองอย่างแสดงในรูปแบบข้อความ รูปที่ 5.1 แสดงตัวอย่างคำขอ HTTP ที่อธิบายการส่งฟอร์ม นักศึกษาในวิชานี้ถูกขอให้ Implement หนึ่งคลาสหรือมากกว่า เพื่อให้เว็บเซิร์ฟเวอร์สามารถรับคำขอ HTTP ขาเข้าและส่งการตอบกลับได้ง่ายขึ้น

img

รูปที่ 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 ควรทำสิ่งที่ถูกต้องและจัดหาให้โดยอัตโนมัติ คุณสมบัติที่ดีที่สุดคือ คุณสมบัติที่คุณได้มาโดยไม่รู้ด้วยซ้ำว่ามันมีอยู่

img Red Flag: Overexposure (สัญญาณเตือน: การเปิดเผยมากเกินไป) img

หาก 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