Better Together Or Better Apart? (รวมกันดีกว่า หรือแยกกันดีกว่า?)

หนึ่งในคำถามพื้นฐานที่สุดในการออกแบบซอฟต์แวร์ก็คือ: เมื่อมีส่วนของคุณลักษณะสองส่วน ควรจะนำมาดำเนินการด้วยกันในที่เดียวกัน หรือควรแยกการดำเนินการของมัน? คำถามนี้ใช้ได้กับทุกระดับในระบบ เช่น Function, Method, Class, และ Service ตัวอย่างเช่น Buffering ควรรวมอยู่ในคลาสที่มี Stream-Oriented File I/O หรือควรอยู่ในคลาสแยกต่างหาก? การ Parse HTTP Request ควรดำเนินการทั้งหมดในหนึ่ง Method หรือควรแบ่งออกไปหลาย Method (หรือแม้แต่หลาย Class)? บทนี้กล่าวถึงปัจจัยต่างๆ ที่ต้องพิจารณาเมื่อตัดสินใจ บางปัจจัยได้กล่าวถึงมาแล้วในบทก่อนหน้านี้ แต่จะกลับมาพูดซ้ำอีกครั้งเพื่อความสมบูรณ์

เมื่อตัดสินใจว่าจะรวมหรือแยก เป้าหมายคือการลดความซับซ้อนของระบบโดยรวมและปรับปรุง Modularity ของมัน อาจดูเหมือนว่าวิธีที่ดีที่สุดเพื่อให้บรรลุเป้าหมายนี้คือการแบ่งระบบออกเป็นองค์ประกอบเล็ก ๆ จำนวนมาก: ยิ่งเล็ก Component ยิ่งง่าย แต่การแบ่งแยกนั้นสร้างความซับซ้อนเพิ่มเติมที่ไม่มีอยู่ก่อน:

  • ความซับซ้อนบางส่วนมาจากจำนวนของ Component: ยิ่งมี Component มากเท่าไหร่ยิ่งยากที่จะติดตามพวกมันทั้งหมด และยิ่งยากที่จะหาส่วนประกอบที่ต้องการในคอลเลกชันขนาดใหญ่ การแบ่งแยกมักจะทำให้มี Interface มากขึ้น และ Interface ใหม่ทุกตัวจะเพิ่มความซับซ้อน
  • การแบ่งแยกอาจส่งผลให้มีโค้ดเพิ่มเติมเพื่อจัดการ Component: ตัวอย่างเช่น ส่วนของโค้ดที่ใช้ Object ตัวเดียวก่อนแบ่งแยกอาจต้องจัดการ Object หลายตัวตอนนี้
  • การแบ่งแยกสร้างการแยกตัวออก: Component ที่แบ่งแยกจะอยู่ห่างจากกันมากขึ้น กว่าก่อนแบ่งแยก ตัวอย่างเช่น Method ที่อยู่ด้วยกันในคลาสเดียวก่อนแบ่งแยกอาจอยู่ในคลาสต่างกันหลังแบ่งแยก และอาจอยู่ในไฟล์ต่างกันด้วย การแยกตัวออกทำให้ยากสำหรับ Developer ที่จะเห็น Component พร้อมกัน หรือแม้แต่ตระหนักถึงการมีอยู่ของพวกมัน ถ้า Component เป็นอิสระจริงๆ แล้ว การแยกก็ดี: มันช่วยให้ Developer มุ่งเน้นไปที่ Component เดียว โดยไม่ถูกรบกวนจากอื่นๆ ในทางกลับกัน ถ้ามี Dependency ระหว่าง Component แล้ว การแยกจะไม่ดี: Developer จะต้องพลิกไปมาระหว่าง Component ด้วยกัน แย่ยิ่งกว่านั้น พวกเขาอาจไม่ตระหนักถึง Dependency นี้ซึ่งอาจนำไปสู่บั๊ก
  • การแบ่งแยกอาจส่งผลให้มีการซ้ำกันของโค้ด: โค้ดที่อยู่ในอินสแตนซ์เดียวก่อนแบ่งแยกอาจจำเป็นต้องมีอยู่ในแต่ละ Component ที่แบ่งแยก

การนำส่วนของโค้ดมารวมกันจะเป็นประโยชน์มากที่สุดถ้าพวกมันมีความสัมพันธ์กันอย่างใกล้ชิด ถ้าส่วนต่างๆ ไม่เกี่ยวข้องกัน อาจจะดีกว่าที่จะแยกออก นี่คือบางสัญญาณที่บ่งชี้ว่าโค้ดสองชุดมีความสัมพันธ์กัน:

  • พวกมันแบ่งปันข้อมูล; ตัวอย่างเช่น ส่วนของโค้ดทั้งสองอาจขึ้นอยู่กับ Syntax ของประเภท Document ที่เฉพาะเจาะจง
  • พวกมันถูกใช้ด้วยกัน: ใครก็ตามที่ใช้ส่วนของโค้ดชิ้นหนึ่งก็มีแนวโน้มที่จะใช้อีกชิ้นหนึ่งด้วย ความสัมพันธ์รูปแบบนี้มีความสำคัญเฉพาะในกรณีที่เป็นแบบสองทาง เป็นตัวอย่างการโต้แย้ง Disk Block Cache มักจะเกี่ยวข้องกับ Hash Table แต่ Hash Table สามารถใช้ได้ในหลายสถานการณ์ที่ไม่เกี่ยวข้องกับ Block Cache; ดังนั้น Module เหล่านี้ควรแยกกัน
  • พวกมันทับซ้อนกันในแนวความคิด ในแง่ที่มีหมวดหมู่ระดับสูงที่ง่ายซึ่งรวมถึงส่วนของโค้ดทั้งสอง ตัวอย่างเช่น การค้นหา Substring และการแปลงตัวพิมพ์ใหญ่-เล็ก ทั้งคู่อยู่ภายใต้หมวดหมู่ของ String Manipulation; Flow Control และ Reliable Delivery ทั้งคู่อยู่ภายใต้หมวดหมู่ของ Network Communication
  • มันยากที่จะเข้าใจส่วนของโค้ดชิ้นหนึ่งโดยไม่ดูส่วนอื่น

ส่วนที่เหลือของบทนี้ใช้กฎเฉพาะเจาะจงมากขึ้นตลอดจนตัวอย่างเพื่อแสดงว่าเมื่อใดที่สมควรนำส่วนของโค้ดมารวมกัน และเมื่อใดที่สมควรแยกออก

9.1 Bring together if information is shared (รวมกัน ถ้าข้อมูลได้รับการแบ่งปัน)

Section 5.4 ได้แนะนำ Principle นี้ในบริบทของโปรเจกต์ที่นำ HTTP Server มาใช้งาน ในการนำมาใช้งานครั้งแรก โปรเจกต์ใช้ Method สองแบบที่แตกต่างกันในคลาสต่างกันเพื่อ Read และ Parse HTTP Request Method แรก Read Text ของ Request ที่เข้ามาจาก Network Socket และวางไว้ใน String Object Method ที่สอง Parse String เพื่อ Extract ส่วนต่างๆ ของ Request ด้วยการแยกตัวแบบนี้ Method ทั้งสองจบลงด้วยความรู้อย่างมากเกี่ยวกับ Format ของ HTTP Request: Method แรกพยายามอ่าน Request เท่านั้น ไม่ได้พยายาม Parse มัน แต่มันไม่สามารถระบุจุดสิ้นสุดของ Request ได้โดยไม่ทำงานส่วนใหญ่ของการ Parse (ตัวอย่างเช่น มันต้อง Parse Header Lines เพื่อระบุ Header ที่มีความยาวของ Request โดยรวม) เนื่องจากข้อมูลที่แบ่งปันนี้ จึงดีกว่าที่จะอ่าน และ Parse Request ด้วยกันในที่เดียวกัน; เมื่อ Class ทั้งสองรวมกันเป็นหนึ่ง โค้ดจึงสั้นลงและง่ายขึ้น

9.2 Bring together if it will simplify the interface (รวมกัน ถ้ามันจะทำให้ Interface ง่ายขึ้น)

เมื่อ Module สองตัวหรือมากกว่าถูกรวมเข้ากับ Module เดียว อาจสามารถกำหนด Interface สำหรับ Module ใหม่ที่ง่ายกว่าหรือใช้งานได้ง่ายกว่า Interface ดั้งเดิม เรื่องนี้มักเกิดขึ้นเมื่อ Module ดั้งเดิมแต่ละตัว นำวิธีแก้ไขปัญหาชิ้นหนึ่งมาใช้ ในตัวอย่าง HTTP Server จากส่วนก่อนหน้า Method ดั้งเดิมต้องการ Interface เพื่อ Return HTTP Request String จาก Method แรก และส่งผ่านไปยัง Method ที่สอง เมื่อ Method ถูกรวมเข้าด้วยกัน Interface เหล่านี้จึงถูกกำจัด

นอกจากนี้ เมื่อ Functionality ของคลาสสองตัวหรือมากกว่า ถูกรวมเข้าด้วยกัน อาจสามารถดำเนินการบางอย่างโดยอัตโนมัติ เพื่อให้ผู้ใช้ส่วนใหญ่ไม่จำเป็นต้องรับรู้ถึงการมีอยู่ของมัน Java I/O Library แสดงความเป็นไปได้นี้ ถ้า FileInputStream และ BufferedInputStream Class ถูกรวมกัน และ Buffering ถูก Provide โดยค่าเริ่มต้น ผู้ใช้ส่วนใหญ่ไม่เคยต้องทำให้ตระหนักถึงการมีอยู่ของ Buffering FileInputStream Class ที่รวมกันอาจ Provide Method เพื่อปิดการใช้งานหรือแทนที่ Buffering กลไกที่เป็นค่าเริ่มต้น แต่ผู้ใช้ส่วนใหญ่จะไม่จำเป็นต้องเรียนรู้เกี่ยวกับพวกมัน

9.3 Bring together to eliminate duplication (รวมกัน เพื่อกำจัดการซ้ำกัน)

ถ้าคุณพบ Pattern ของโค้ดเดียวกันซ้ำแล้วซ้ำเล่า ให้ดูว่าคุณสามารถปรับปรุง Code ใหม่เพื่อกำจัดการซ้ำได้หรือไม่ วิธีหนึ่งคือ Factor โค้ดที่ซ้ำออก เข้าไป Separate Method และแทนที่ Code Snippet ที่ซ้ำกับการเรียก Method วิธีนี้จะมีประสิทธิภาพสูงสุดถ้า Code Snippet ที่ซ้ำยาวและ Replacement Method มี Simple Signature ถ้า Snippet เพียง 1-2 บรรทัด อาจจะไม่มีประโยชน์มากนักในการแทนที่ด้วยการเรียก Method ถ้า Snippet มีปฏิสัมพันธ์ที่ซับซ้อนกับ Environment ของมัน (เช่น โดยการเข้าถึง Local Variable หลายตัว) แล้ว Replacement Method อาจต้องการ Signature ที่ซับซ้อน (เช่น Arguments ที่ Refer โดยการส่งต่อหลายตัว) ซึ่งจะลดค่าของมัน

อีกวิธีหนึ่งในการกำจัดการซ้ำคือการ Refactor Code เพื่อให้ Snippet ที่เป็นปัญหาต้องถูก Execute เพียงครั้งเดียว สมมติว่าคุณกำลังเขียน Method ที่ต้องอ้างอิง Error ที่หลายจุดต่างกัน และต้องทำ Cleanup Actions เดียวกัน ที่แต่ละจุดก่อนอ้างอิง (ดู Figure 9.1 สำหรับตัวอย่าง) ถ้า Programming Language รองรับ goto คุณสามารถย้าย Cleanup Code ไปที่จุดสิ้นสุดของ Method และจากนั้น goto Snippet นั้นที่แต่ละจุดที่ต้องการอ้างอิง Error เช่นเดียวกับใน Figure 9.2 Goto Statement โดยทั่วไปถือว่าเป็นแนวคิดที่ไม่ดี และอาจทำให้เกิด Code ที่ยากต่อการเข้าใจถ้าใช้โดยประมาณ แต่มีประโยชน์ในสถานการณ์เช่นนี้ที่ใช้ในการ Escape จาก Nested Code

9.4 Separate general-purpose and special-purpose code (แยก General-Purpose และ Special-Purpose Code)

ถ้า Module มี Mechanism ที่สามารถใช้เพื่อวัตถุประสงค์ต่างๆ มากมาย มันควรเป็น Provide เพียง Mechanism ทั่วไปเดียว มันไม่ควรรวม Code ที่ทำให้ Mechanism เฉพาะสำหรับการใช้งานที่เฉพาะเจาะจง และไม่ควรมี Mechanism ทั่วไปอื่นๆ Code ที่มีจุดประสงค์พิเศษ ซึ่งเกี่ยวข้องกับ Mechanism ที่มีวัตถุประสงค์ทั่วไป ควรปกติจะไปอยู่ใน Module ต่างกัน (โดยปกติจะเป็นอันที่เกี่ยวข้องกับวัตถุประสงค์นั้น) บทพูดคุยเกี่ยวกับ GUI Editor ใน Chapter 6 ได้แสดงให้เห็น Principle นี้: Design ที่ดีที่สุดคือ Text Class ให้ General-Purpose Text Operation ในขณะที่ Operation ที่เฉพาะเจาะจงกับ User Interface (เช่น Deleting Selection) ถูก Implement ใน User Interface Module วิธีนี้กำจัด Information Leakage และ Interface เพิ่มเติม ที่มีอยู่ใน Design ก่อนหน้าซึ่ง Specialized User Interface Operation ถูก Implement ใน Text Class

img Red Flag: Repetition (ธง สีแดง: การซ้ำกัน) img

ถ้าส่วนของโค้ดเดียวกัน (หรือโค้ดที่เกือบจะเหมือนกัน) ปรากฏซ้ำแล้วซ้ำเล่า นั่นคือธงสีแดงที่บ่งชี้ว่าคุณยังไม่ได้หา Abstraction ที่ถูกต้อง

img

Figure 9.1: โค้ดนี้ประมวลผล Incoming Network Packet ของประเภท ต่างกัน; สำหรับแต่ละประเภท ถ้า Packet สั้นเกินไปสำหรับประเภทนั้น ข้อความจะถูก Log ในเวอร์ชันของโค้ดนี้ LOG Statement ถูกซ้ำสำหรับหลายประเภท Packet

img

Figure 9.2: การปรับปรุงใหม่ของโค้ดจาก Figure 9.1 เพื่อให้มี LOG Statement เพียงหนึ่ง Copy

โดยทั่วไป Layer ที่ต่ำกว่าของระบบมีแนวโน้มที่จะเป็น General-Purpose มากขึ้น และ Layer ที่สูงกว่ามี Special-Purpose มากขึ้น ตัวอย่างเช่น Layer ด้านบนสุดของ Application ประกอบด้วย Feature ที่เฉพาะเจาะจงกับ Application นั้นอย่างสมบูรณ์ วิธีการ Separate Special-Purpose Code จาก General-Purpose Code คือการดึง Special-Purpose Code ขึ้นไป เข้าไป Layer ที่สูงกว่า ปล่อยให้ Layer ที่ต่ำกว่า General-Purpose เมื่อคุณพบ Class ที่รวม General-Purpose และ Special-Purpose Feature เข้าด้วยกัน สำหรับ Abstraction เดียวกัน ให้ดูว่า Class สามารถแยกออกเป็น Class สองตัว ตัวหนึ่งมี General-Purpose Feature และอีกตัวหนึ่งอยู่ชั้นบน เพื่อ Provide Special-Purpose Feature ได้หรือไม่

9.5 Example: insertion cursor and selection (ตัวอย่าง: Insertion Cursor และ Selection)

ส่วนต่อไปทำงานผ่าน ตัวอย่างสามตัวอย่าง ซึ่งแสดงให้เห็น Principle ที่กล่าวถึงข้างต้น ในสองตัวอย่าง วิธีที่ดีที่สุดคือการแยก Piece ของโค้ดที่เกี่ยวข้องกัน; ในตัวอย่างที่สาม จะดีกว่าที่จะ Join พวกมันเข้าด้วยกัน

ตัวอย่างแรกประกอบด้วย Insertion Cursor และ Selection ใน GUI Editor Project จาก Chapter 6 Editor แสดง Blinking Vertical Line ที่บ่งชี้ว่าข้อความที่พิมพ์โดยผู้ใช้จะปรากฏในเอกสาร นอกจากนี้ยังแสดง Highlighted Range ของอักขระที่เรียกว่า Selection ซึ่งใช้สำหรับการ Copy หรือ Delete ข้อความ Insertion Cursor มองเห็นได้เสมอ แต่อาจมีเวลาที่ไม่มีการเลือกข้อความ ถ้า Selection มีอยู่ Insertion Cursor จะตั้งตำแหน่งที่ปลายด้านหนึ่งของมันเสมอ

Selection และ Insertion Cursor มีความสัมพันธ์กันในบางทาง ตัวอย่างเช่น Cursor จะตั้งตำแหน่งที่ปลายด้านหนึ่งของ Selection เสมอ และ Cursor และ Selection มีแนวโน้มที่จะถูกจัดการเข้าด้วยกัน: การคลิกและลากเมาส์จะตั้งค่าทั้งสองอย่าง และการแทรก Text จะ Delete Text ที่เลือก ถ้ามี และจากนั้นแทรก Text ใหม่ที่ Cursor Position ดังนั้น อาจดูเหมือนว่าสมควรใช้ Object เดียวเพื่อจัดการทั้ง Selection และ Cursor และทีมโปรเจกต์หนึ่งได้นำวิธีนี้มาใช้ Object เก็บสองตำแหน่งในไฟล์ เข้าด้วยกับ Boolean บ่งชี้ว่าปลายด้านไหนคือ Cursor และว่า Selection มีอยู่หรือไม่

อย่างไรก็ตาม Combined Object ก็ยุ่งยาก มันไม่ได้มีประโยชน์ใด ๆ สำหรับ Higher-Level Code เพราะ Higher-Level Code ยังคงต้องตระหนักถึง Selection และ Cursor เป็นเอนทิตี้ที่แตกต่างกัน และจัดการพวกมันแยกกัน (ในการแทรก Text มันเรียก Method ครั้งแรกบน Combined Object เพื่อ Delete Text ที่เลือก; จากนั้นเรียก Method อื่นเพื่อ Retrieve Cursor Position เพื่อแทรก Text ใหม่) Combined Object มีความซับซ้อนมากขึ้นในการ Implement กว่า Separate Object นั้นหลีกเลี่ยงการจัดเก็บ Cursor Position เป็น Separate Entity แต่แทน ต้องจัดเก็บ Boolean บ่งชี้ว่าปลายด้านไหนของ Selection คือ Cursor เพื่อ Retrieve Cursor Position Combined Object ต้อง Test Boolean ครั้งแรก และจากนั้น Choose ปลายที่เหมาะสมของ Selection

img Red Flag: Special-General Mixture (ธง สีแดง: Special-General Mixture) img

ธงสีแดงนี้เกิดขึ้นเมื่อ General-Purpose Mechanism ยังมี Code ที่ Specialized สำหรับการใช้งาน Mechanism นั้นเพียงอย่างเดียว สิ่งนี้ทำให้ Mechanism ซับซ้อนขึ้นและสร้าง Information Leakage ระหว่าง Mechanism และ Use Case ที่เฉพาะเจาะจง: Modification ในอนาคตต่อ Use Case มีแนวโน้มที่จะต้องการ Change ต่อ Mechanism ที่เป็นพื้นฐาน

ในกรณีนี้ Selection และ Cursor ไม่มีความสัมพันธ์กันอย่างใกล้ชิดพอที่จะรวมพวกมันเข้าด้วยกัน เมื่อโค้ดถูก Revise เพื่อแยก Selection และ Cursor การใช้งานและ Implementation ทั้งคู่จึงง่ายขึ้น Separate Object ให้ Interface ที่ง่ายกว่า Combined Object ซึ่ง Selection และ Cursor Information ต้องถูก Extract Cursor Implementation ยังง่ายขึ้นเพราะ Cursor Position ถูกแสดงโดยตรง มากกว่าโดยอ้อมผ่าน Selection และ Boolean ในความเป็นจริง ในเวอร์ชันที่ Revised ไม่ได้ใช้ Special Class ใด ๆ สำหรับ Selection หรือ Cursor แทน Position Class ใหม่ได้ถูก Introduce เพื่อแสดง Location ในไฟล์ (หมายเลขบรรทัด และตัวอักษรภายในบรรทัด) Selection ถูกแสดง ด้วย Positions สอง Position และ Cursor ด้วยหนึ่ง Positions ยังพบการใช้งานอื่นๆ ในโปรเจกต์ ตัวอย่างนี้ยังแสดง Benefit ของ Lower-Level แต่ General-Purpose Interface มากขึ้น ซึ่งได้กล่าวถึงใน Chapter 6

9.6 Example: separate class for logging (ตัวอย่าง: Separate Class สำหรับ Logging)

ตัวอย่างที่สองเกี่ยวข้องกับ Error Logging ใน Student Project Class มีสัดส่วน Code หลายสัดส่วนดังต่อไปนี้:

try {
      rpcConn = connectionPool.getConnection(dest);
} catch (IOException e) {
      NetworkErrorLogger.logRpcOpenError(req, dest, e);
      return null;
}

แทนที่จะ Log Error ที่จุดที่ตรวจพบแล้ว Separate Method ใน Special Error Logging Class ถูกเรียก Error Logging Class ถูก Define ที่จุดสิ้นสุดของไฟล์ Source เดียวกัน:

private static class NetworkErrorLogger {
     /**
      *  Output information relevant to an error that occurs when trying
      *  to open a connection to send an RPC.
      *
      *  @param req
      *       The RPC request that would have been sent through the connection
      *  @param dest
      *       The destination of the RPC
      *  @param e
      *       The caught error
      */
     public static void logRpcOpenError(RpcRequest req, AddrPortTuple dest, Exception e) {
         logger.log(Level.WARNING, "Cannot send message: " + req + ". \n" + "Unable to find or open connection to " + dest + " :" + e);
      }
...
}

NetworkErrorLogger Class มี Method หลายตัวเช่น logRpcSendError และ logRpcReceiveError โดยแต่ละตัวจะ Log Error ประเภทต่างกัน

การแยกนี้เพิ่มความซับซ้อนโดยไม่มีประโยชน์ใด ๆ Logging Method ต่างก็ Shallow: ส่วนใหญ่ประกอบด้วยบรรทัดของโค้ดบรรทัดเดียว แต่ต้องการ Documentation ที่พิจารณาอย่างมาก แต่ละ Method จะถูกเรียกเพียงแห่งเดียว Logging Method ขึ้นอยู่กับการเรียกของมันมาก: ใครก็ตามที่อ่าน Invocation มีแนวโน้มที่จะพลิกไปที่ Logging Method เพื่อให้แน่ใจว่า Information ที่ถูกต้องกำลังถูก Log; ในทำนองเดียวกัน ใครก็ตามที่อ่าน Logging Method อาจจะพลิกไปยัง Invocation Site เพื่อให้เข้าใจวัตถุประสงค์ของ Method

ในตัวอย่างนี้ จะดีกว่าที่จะกำจัด Logging Method และวาง Logging Statement ที่ Location ที่ Error ถูกตรวจพบ สิ่งนี้จะทำให้โค้ดอ่านได้ง่ายขึ้น และกำจัด Interface ที่ต้องการสำหรับ Logging Method

9.7 Example: editor undo mechanism (ตัวอย่าง: Editor Undo Mechanism)

ใน GUI Editor Project จาก Section 6.2 ข้อกำหนดอย่างหนึ่งคือการ Support Multi-Level Undo/Redo ไม่ใช่เฉพาะสำหรับ Change ต่อ Text เอง แต่ยังสำหรับ Change ต่อ Selection, Insertion Cursor, และ View ตัวอย่างเช่น ถ้าผู้ใช้เลือก Text บางส่วน ลบมัน Scroll ไปยังตำแหน่งอื่นในไฟล์ และจากนั้นเรียก Undo Editor ต้อง Restore State ของมันไปยัง State ที่เป็นมาก่อน Delete นี้ สิ่งนี้รวมถึง Restore Text ที่ถูกลบ เลือกอีกครั้ง และยัง Make Selected Text มองเห็นได้ในหน้าต่าง

Student Project บางตัว Implement Undo Mechanism ทั้งหมดเป็นส่วนหนึ่งของ Text Class Text Class Maintain รายการของ Change ทั้งหมดที่อาจ Undo มันจึง Automatically เพิ่ม Entry ต่อรายการนี้ทุกครั้งที่ Text ถูก Change สำหรับ Change ต่อ Selection, Insertion Cursor, และ View User Interface Code เรียก Method เพิ่มเติมใน Text Class ซึ่งจากนั้นเพิ่ม Entry สำหรับ Change เหล่านั้นต่อ Undo List เมื่อ Undo หรือ Redo ถูกขอโดยผู้ใช้ User Interface Code เรียก Method ใน Text Class ซึ่งจากนั้น Process Entry ใน Undo List สำหรับ Entry ที่เกี่ยวข้องกับ Text มันอัปเดต Internals ของ Text Class; สำหรับ Entry ที่เกี่ยวข้องกับสิ่งอื่น ๆ เช่น Selection Text Class เรียกกลับไปยัง User Interface Code เพื่อดำเนินการ Undo หรือ Redo

วิธีการนี้ส่งผลให้มี Awkward Set ของ Feature ใน Text Class Core ของ Undo/Redo ประกอบด้วย General-Purpose Mechanism สำหรับ Manage รายการของ Action ที่ได้ Execute และ Step Through ระหว่าง Undo และ Redo Operation Core ตั้งอยู่ใน Text Class พร้อมกับ Special-Purpose Handler ที่ Implement Undo และ Redo สำหรับสิ่งเฉพาะเจาะจงเช่น Text และ Selection Special-Purpose Undo Handler สำหรับ Selection และ Cursor ไม่มีเกี่ยวข้องกับสิ่งอื่น ๆ ใน Text Class; พวกมันส่งผล ใน Information Leakage ระหว่าง Text Class และ User Interface เช่นเดียวกับ Extra Method ในแต่ละ Module เพื่อส่ง Undo Information ไปมา ถ้า Entity ที่ Undoable ประเภทใหม่ได้ถูกเพิ่มเข้าไปในระบบในอนาคต มันจะต้องการ Change ต่อ Text Class รวมถึง Method ใหม่ที่เฉพาะสำหรับ Entity นั้น นอกจากนี้ General-Purpose Undo Core มีความเกี่ยวข้องเล็กน้อยกับ General-Purpose Text Facility ใน Class

ปัญหาเหล่านี้สามารถแก้ไขได้โดยการ Extract General-Purpose Core ของ Undo/Redo Mechanism และวาง ไปใน Separate Class:

public class History {
        public interface Action {
               public void redo();
               public void undo();
        }
        History() {...}
        void addAction(Action action) {...}
        void addFence() {...}
        void undo() {...}
        void redo() {...}
}

ใน Design นี้ History Class Manage Collection ของ Object ที่ Implement History.Action Interface โดยแต่ละ History.Action อธิบาย Single Operation เช่น Text Insertion หรือ Change ใน Cursor Location และมันให้ Method ที่สามารถ Undo หรือ Redo Operation History Class รู้เรื่องอะไรเกี่ยวกับ Information ที่เก็บไว้ใน Action หรือวิธี Implement Undo และ Redo Method ของมัน History Maintain History List อธิบาย Action ทั้งหมดที่ Execute ในช่วง Lifetime ของ Application และให้ Undo และ Redo Method ที่ Walk Backward และ Forward ผ่าน List ในการตอบสนอง Undo และ Redo ที่ขอโดยผู้ใช้ โดยเรียก Undo และ Redo Method ใน History.Actions

History.Actions คือ Special-Purpose Object: โดยแต่ละตัวเข้าใจ Operation ที่ Undoable ประเภทเฉพาะเจาะจง พวกมันถูก Implement นอก History Class ใน Module ที่เข้าใจประเภท Undoable Action พิเศษ Text Class อาจ Implement UndoableInsert และ UndoableDelete Object เพื่ออธิบาย Text Insertion และ Deletion ทุกครั้งที่มันแทรก Text Text Class สร้าง UndoableInsert Object ใหม่อธิบาย Insertion และเรียก History.addAction เพื่อเพิ่มมันต่อ History List Editor ของ User Interface Code อาจสร้าง UndoableSelection และ UndoableCursor Object ที่อธิบาย Change ต่อ Selection และ Insertion Cursor

History Class ยังอนุญาตให้ Action ถูก Group เพื่อให้ตัวอย่างเช่น Single Undo Request จากผู้ใช้สามารถ Restore Text ที่ถูกลบ Reselect Text ที่ถูกลบ และ Reposition Insertion Cursor มีวิธีหลายวิธี Group Action; History Class ใช้ Fence ซึ่งเป็น Marker ที่วาง ใน History List เพื่อแยก Group ของ Action ที่เกี่ยวข้องกัน แต่ละการเรียก History.redo Walk Backward ผ่าน History List Undoing Action จนกว่ามันจะถึง Fence ถัดไป Placement ของ Fence ถูก Determine โดย Higher-Level Code โดยเรียก History.addFence

วิธีการนี้แบ่ง Functionality ของ Undo เข้าไป 3 Category โดยแต่ละตัว Implement ในที่ต่างกัน:

  • A General-Purpose Mechanism สำหรับ Manage และ Group Action และเรียก Undo/Redo Operation (Implement โดย History Class)
  • The Specifics ของ Particular Action (Implement โดย Variety ของ Class โดยแต่ละตัวเข้าใจ Action Type เล็ก ๆ น้อย ๆ)
  • The Policy สำหรับ Group Action (Implement โดย High-Level User Interface Code เพื่อให้ Right Overall Application Behavior)

แต่ละ Category เหล่านี้สามารถ Implement โดยไม่มี Understanding ของ Category อื่น ๆ History Class รู้เรื่องอะไรว่า Action ประเภทใด ที่ถูก Undo; มันสามารถ ใช้ใน Application ต่างกัน แต่ละ Action Class เข้าใจเพียง Single Action Type และไม่ History Class และไม่ Action Class ต้องรับรู้ถึง Policy สำหรับ Group Action

Key Design Decision คือ Decision ที่ Separate General-Purpose Part ของ Undo Mechanism จาก Special-Purpose Part และวาง General-Purpose Part ไปใน Class โดยตัวมันเอง เมื่อทำอย่างนั้นแล้ว Design ที่เหลือจึงหลุดออกมาตามธรรมชาติ

Note: Suggestion ในการ Separate General-Purpose Code จาก Special-Purpose Code หมายถึง Code ที่เกี่ยวข้องกับ Mechanism เฉพาะเจาะจง ตัวอย่างเช่น Special-Purpose Undo Code (เช่น Code เพื่อ Undo Text Insertion) ควรแยกจาก General-Purpose Undo Code (เช่น Code เพื่อ Manage History List) อย่างไรก็ตาม มักจะสมควรที่จะรวม Special-Purpose Code สำหรับ Mechanism หนึ่งเข้าด้วยกับ General-Purpose Code สำหรับอีก Mechanism หนึ่ง Text Class คือตัวอย่าง: มันนำ General-Purpose Mechanism สำหรับ Manage Text แต่มี Special-Purpose Code ที่เกี่ยวข้องกับการ Undo Code Undo นี้เป็น Special-Purpose เพราะมันจัดการเพียง Undo Operation สำหรับ Text Modification มันไม่สมควร Combine Code นี้กับ General-Purpose Undo Infrastructure ใน History Class แต่มันสมควรที่จะวาง ไปใน Text Class เพราะมันเกี่ยวข้องอย่างใกล้ชิดกับ Function อื่น ๆ ของ Text

9.8 Splitting and joining methods (การแยก และ Joining Method)

ปัญหาของเมื่อแบ่ง ไม่เพียงแต่ใช้กับ Class เท่านั้น แต่ยังใช้กับ Method ด้วย: มีเวลาที่ดีกว่าที่จะแบ่ง Existing Method เข้าไป Multiple Smaller Method? หรือ ควรรวม Two Smaller Method เข้าไป Larger One? Long Method มีแนวโน้มที่ยากต่อการเข้าใจมากกว่า Shorter One ดังนั้นหลายคนจึงเถียง ว่า Length Alone คือ Good Justification สำหรับ Breaking Up Method Student ใน Class มักถูกให้ Rigid Criteria เช่น "Split Up Any Method Longer Than 20 Lines!"

อย่างไรก็ตาม Length โดยตัวมันเอง ไม่ค่อยเป็น Good Reason สำหรับ Splitting Up Method โดยทั่วไป Developer มีแนวโน้มที่จะ Break Up Method มากเกินไป Splitting Up Method นำ Interface เพิ่มเติม ซึ่งเพิ่ม Complexity นอกจากนี้ยังแยก Piece ของ Method ดั้งเดิม ซึ่งทำให้โค้ดยากต่อการอ่านถ้า Piece นั้นมีความสัมพันธ์กันจริงๆ คุณไม่ควร Break Up Method ยกเว้นว่ามันทำให้ System โดยรวมง่ายขึ้น; ผมจะกล่าวถึงวิธีนี้ ไปด้านล่าง

Long Method ไม่ได้เลวไป ตัวอย่างเช่น สมมติว่า Method มี 20-Line Block ของ Code 5 Block ที่ Execute ในลำดับ ถ้า Block นั้นค่อนข้าง Independent แล้ว Method สามารถ Read และ Understand One Block ที่เวลา; มี Benefit ไม่มาก ใน Moving Each Block ไปยัง Separate Method ถ้า Block มี Complex Interaction มันมี Importance เท่า ในการ Keep พวกมันเข้าด้วยกัน เพื่อให้ Reader เห็น Code ทั้งหมด ในครั้งเดียว; ถ้า Each Block อยู่ใน Separate Method Reader จะต้อง Flip Back and Forth ระหว่าง Spread-Out Method นี้เพื่อให้เข้าใจ How They Work Together Method ที่มี Hundred ของ Line ของ Code เป็น Fine ถ้า Simple Signature และ Easy การ Read สิ่งเหล่านี้ Method มี Deep (Lots ของ Functionality Simple Interface) ซึ่งเป็น Good

img

Figure 9.3: A Method (a) สามารถแยกได้ โดยการ Extract Subtask (b) หรือโดยการแบ่ง Functionality ของมันเข้าไป 2 Separate Method (c) Method ไม่ควร Split ถ้า Result ใน Shallow Method ดังใน (d)

เมื่อ Design Method ยิ่งเป้าหมายที่สำคัญก็คือการให้ Clean และ Simple Abstraction แต่ละ Method ควร Do One Thing และ Do It Completely Method ควร Clean และ Simple Interface เพื่อให้ User ไม่จำเป็นต้องใช้ Information มากนักในหัวเพื่อใช้มันได้ถูก Method ควร Deep: Interface ของมันควร Simpler มากกว่า Implementation ของมัน ถ้า Method มี Property ทั้งหมดเหล่านี้ มันอาจจะไม่สำคัญว่า Long หรือไม่

Splitting Up Method เป็น Sense เฉพาะถ้า Result ใน Cleaner Abstraction โดยรวม มี 2 วิธีในการทำ ซึ่ง Diagram ใน Figure 9.3 Best Way คือโดยการ Factor Out Subtask เข้าไป Separate Method ดังแสดง ใน Figure 9.3(b) Subdivision Result ใน Child Method ที่มี Subtask และ Parent Method ที่มี Remainder ของ Method ดั้งเดิม; Parent Invoke Child Interface ของ New Parent Method นั้น Same กับ Method ดั้งเดิม Form นี้ ของ Subdivision เป็น Sense ถ้ามี Subtask ที่ Cleanly Separable จาก Method ดั้งเดิมที่เหลือ ซึ่งหมายถึง (a) ใครก็ตามที่อ่าน Child Method ไม่จำเป็นต้อง Know เรื่องอะไรเกี่ยวกับ Parent Method และ (b) ใครก็ตามที่อ่าน Parent Method ไม่จำเป็นต้อง Understand Implementation ของ Child Method โดยทั่วไป หมายถึง Child Method เป็น Relative General-Purpose: มันอาจคิด ถึง ที่ใช้โดย Method อื่นจาก Parent ถ้าคุณทำ Split ของ Form นี้ แล้ว Find ตัวเองที่พลิกไปมา ระหว่าง Parent และ Child เพื่อเข้าใจ How They Work Together นั่นคือ Red Flag ("Conjoined Method") บ่งชี้ว่า Split อาจจะเป็นแนวคิดที่ไม่ดี

Second Way เพื่อ Break Up Method คือการ Split มัน เข้าไป 2 Separate Method แต่ละอาจมองเห็นได้โดยผู้เรียก ของ Method ดั้งเดิม ดังใน Figure 9.3(c) สิ่งนี้ Sense ถ้า Method ดั้งเดิม มี Overly Complex Interface เพราะ Tried To Do Multiple Thing ที่ไม่มีความสัมพันธ์กันอย่างใกล้ชิด ถ้า Case นี้ มันอาจ สามารถแบ่ง Functionality ของ Method เข้าไป 2 หรือมากกว่า Smaller Method โดยแต่ละตัว มี Part เดียวเท่านั้น ของ Method ดั้งเดิม Functionality ถ้าคุณทำ Split เช่นนี้ Interface สำหรับแต่ละตัว ของ Resulting Method ควร Simpler กว่า Interface ของ Method ดั้งเดิม โดยทั่วไป ผู้เรียก ส่วนใหญ่ควรเรียก Method ใหม่ หนึ่งตัวเท่านั้น; ถ้า Caller ต้องเรียก Both ของ New Method นั่นเพิ่ม Complexity ซึ่งทำให้มันน้อยลง สำเร็จ Split เป็นแนวคิด ดี Method ใหม่ จะเป็น Focus มากขึ้น ใน What They Do มันเป็น Good Sign ถ้า New Method มี General-Purpose มากกว่า Method ดั้งเดิม (ที่คือ คุณสามารถ Imagine ใช้พวกมันแยกกัน ใน Situation อื่น ๆ)

Split ของ Form ดังแสดง ใน Figure 9.3(c) ไม่ Make Sense บ่อยมาก เพราะ Result ใน Caller ที่มี Deal ด้วย Multiple Method แทน One Method เมื่อ Split With Way นี้ คุณ Run Risk ของ Ending Up ด้วย Shallow Method หลายตัว ดังใน Figure 9.3(d) ถ้า Caller ต้อง Invoke Each ของ Separate Method Passing State Back and Forth ระหว่างพวกมัน Split นั้นไม่ Good Idea ถ้าคุณ Consider Split เช่น One ใน Figure 9.3(c) คุณควร Judge It ขึ้นอยู่กับ Simplify Thing สำหรับ Caller

มี Situation ด้วย Where System สามารถ Make Simpler โดยการ Join Method ด้วยกัน ตัวอย่างเช่น Joining Method อาจ Replace 2 Shallow Method ด้วย One Deeper Method; มันอาจ Eliminate Duplication ของ Code; มันอาจ Eliminate Dependency ระหว่าง Method ดั้งเดิม หรือ Intermediate Data Structure; มันอาจ Result ใน Better Encapsulation เพื่อให้ Knowledge ที่ Presence ก่อนใน Multiple Place นั้นตอนนี้ Isolate ใน Single Place; หรือ มันอาจ Result ใน Simpler Interface ตามที่กล่าวถึง Section 9.2

img Red Flag: Conjoined Methods (ธง สีแดง: Conjoined Method) img

ควรเป็นไปได้ที่จะเข้าใจแต่ละ Method แยกกัน ถ้าคุณไม่สามารถเข้าใจ Implementation ของ Method หนึ่งโดยไม่เข้าใจ Implementation ของอีก Method หนึ่งด้วย นั่นคือ Red Flag ธง แดงนี้อาจเกิด ใน Context อื่น ๆ ด้วย: ถ้า 2 Piece ของ Code เป็น Physically Separate แต่ Each สามารถ Understand โดยการ Look At The Other เท่านั้น นั่นคือ Red Flag

9.9 Conclusion (สรุป)

Decision To Split หรือ Join Module ควร Base บน Complexity Pick Structure ที่ Result ใน Best Information Hiding Fewest Dependency และ Deepest Interface