ข้ามไปยังเนื้อหา

Generalization & Inheritance

กลุ่ม generalization and inheritance ย้ายฟีเจอร์ทั้ง แนวตั้ง และ แนวข้าง ไปมาในลำดับชั้นของ type ที่เกี่ยวข้องกัน สมาชิกที่ subclass พี่น้องมีเหมือนกันจะดึงขึ้นไปไว้บน superclass สมาชิกที่มีแค่สาขาเดียวใช้ก็ผลักลงมา ส่วนสอง class ที่ไม่เกี่ยวกันแต่โตจนคล้ายกันก็งอก superclass ร่วมขึ้นมา และเมื่อ inheritance กลายเป็นเครื่องมือที่ผิด คือ subclass ใช้ superclass แค่เสี้ยวเดียว หรือความสัมพันธ์ “is-a” เป็นเรื่องโกหก ท่าเหล่านี้ก็ช่วยให้คุณสลับไปใช้ composition ได้โดยไม่เปลี่ยนสิ่งที่โปรแกรมทำ

inheritance ทรงพลังและล้ำเส้นได้ง่ายมาก เช่น method ชุดเดียวกันถูก copy-paste ลงไปใน subclass สามตัว หรือ field วางอยู่บน superclass แต่มี subclass แค่ครึ่งเดียวที่แตะ หรือ class หนึ่ง extends อีก class เพียงเพื่อยืม helper สองตัว แล้วพลอย inherit อีกสิบสองตัวที่ต้องแกล้งทำเป็นไม่มี ทุกกรณีคือลำดับชั้นที่เลิกบอกความจริงเรื่องความสัมพันธ์ระหว่าง type ไปแล้ว refactoring ในโมดูลนี้จะจัดลำดับชั้นให้ตรง หรือสลายทิ้งไปเป็น delegation เมื่อ composition เหมาะกว่า

Code Smellอาการที่บ่งชี้ท่าที่แนะนำ
Duplicated Codemethod เดิมคัดลอกวางใน subclass พี่น้องหลายตัวPull Up Method / Field
Refused Bequestsubclass inherit แต่ต้องแกล้งทำเป็นว่า member ไม่มีอยู่Replace Inheritance with Delegation
Speculative Generalityhierarchy ที่สร้างขึ้นสำหรับอนาคตที่ยังไม่มาถึงCollapse Hierarchy
Inappropriate Intimacysubclass ใช้ superclass เพียงเสี้ยวเดียวแต่ยึดทั้งหมดReplace Subclass with Delegation
Parallel Inheritance Hierarchiesเพิ่ม subclass ที่หนึ่งต้องเพิ่มอีก hierarchy ด้วยExtract Superclass / Interface
classDiagram
  class Superclass {
    +shared() members
  }
  class SubclassA
  class SubclassB
  Superclass <|-- SubclassA : pull up / push down
  Superclass <|-- SubclassB : extract superclass
  class Interface {
    <<interface>>
    +protocol()
  }
  Interface <|.. SubclassA : extract interface
  class Delegate {
    +behaviour()
  }
  SubclassB --> Delegate : replace inheritance / subclass with delegation
ฟีเจอร์เคลื่อนข้ามลำดับชั้นอย่างไรในโมดูลนี้

refactoring แปดตัว แต่ละตัวย้อนกลับได้:

  • Pull Up Method / Field — เมื่อ subclass พี่น้องมีสมาชิกเหมือนกัน ให้ยกขึ้นไปไว้บน superclass จะได้อยู่ที่เดียวเป๊ะ ๆ ส่วน Push Down เป็นท่ากลับกัน คือสมาชิกที่มีแค่บาง subclass ใช้ ก็ผลักลงไปอยู่ใน subclass เหล่านั้น
  • Extract Superclass / Interface — เมื่อสอง class มีฟีเจอร์ร่วมกัน ให้ยกความเหมือนขึ้นไปเป็นพ่อแม่ใหม่ และเมื่อมีเพียง protocol ร่วมกัน ให้ extract เป็น interface (เป็น trait ใน Rust)
  • Replace Inheritance with Delegation — เมื่อ subclass พึ่ง superclass แค่บางส่วน ให้ถือ superclass เดิมไว้เป็น field แล้ว delegate เข้าไป
  • Replace Subclass with Delegation — เมื่อ subclass แตกต่างกันตามแกนเดียว ให้แทนลำดับชั้นทั้งหมดด้วย class เดียวที่ถือ strategy object ไว้
  • Collapse Hierarchy — เมื่อ subclass กับ superclass ใกล้กันเกินกว่าจะแยกเป็นสอง type ก็ยุบรวมเป็นตัวเดียว
  • Replace Subclass with Fields — เมื่อ subclass ต่างกันแค่ข้อมูลคงที่ที่ method คืนออกมา ให้ยุบเหลือ class เดียวที่เก็บข้อมูลนั้นไว้เป็น field
  • Replace Superclass with Delegation — เมื่อ subclass ใช้ superclass แค่บางส่วน หรือ inherit interface ที่เก้งก้างมา ให้ถือ superclass เดิมไว้เป็น field แล้ว delegate เข้าไป
  • Pull Up Constructor Body — เมื่อ constructor ของ subclass พี่น้อง initialize เหมือนกัน ให้ย้ายส่วนที่เหมือนกันไปไว้ใน constructor ของ superclass แล้วเรียกขึ้นไป

แต่ละบทเรียนแสดง before-and-after เดียวกันใน TypeScript, Python, Go และ Rust สังเกตว่า Go และ Rust ไม่มี class inheritance: Go ประกอบ behavior ผ่าน struct embedding และ interface ส่วน Rust ผ่าน trait และ field ที่ถือไว้ ดังนั้นตัวอย่างในภาษาเหล่านั้นจึงจำลองเจตนาเดียวกันด้วย composition — ซึ่งอย่างที่คุณจะได้เห็น คือจุดที่ refactoring หลายตัวเหล่านี้พยายามนำคุณไปอยู่แล้ว

refactoring ในโมดูลนี้ย้ายอะไรเป็นหลัก?
Go และ Rust จำลองเจตนาของ refactoring แบบ inheritance เหล่านี้อย่างไร?
Push Down อธิบายได้ดีที่สุดว่าเป็นสิ่งตรงข้ามของ refactoring ตัวใด?