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

Replace Inheritance with Delegation

Replace Inheritance with Delegation ปลด subclass ออกจาก superclass แล้วให้ถือ field ที่เก็บ instance ของ superclass เดิมไว้แทน จากนั้น class จะ delegate เฉพาะการเรียกที่ต้องใช้จริง ส่วนที่เหลือก็ไม่ต้องสนใจ ใช้ท่านี้เมื่อ subclass ใช้ interface ของ superclass แค่เสี้ยวเดียว หรือเมื่อ inheritance ที่เขียนไว้จริง ๆ แล้วคือความสัมพันธ์ “has-a” ที่แต่งตัวมาเป็น “is-a”

อาการคลาสสิกคือ Refused Bequest เช่น Stack extends List ทั้งที่ stack ไม่ควรเปิดให้ caller insertAt ตรงกลางหรือ remove จากก้นได้ การ inherit ทำให้ Stack เปิดเผย interface ทั้งชุดของ List แล้วทำลาย invariant ของตัวเองอย่างเงียบ ๆ ความสัมพันธ์ที่วางไว้จึงผิด เพราะ stack มี list ไม่ได้ เป็น list พอเปลี่ยนมาใช้ delegation แทน Stack ก็เปิดเผยแค่ push กับ pop และเก็บ list ไว้เป็น private

Stack ที่สร้างด้วยการ extend List จะปล่อยให้ operation ของ list รั่วออกไปหมด หลัง refactor จะเปลี่ยนเป็น ถือ list ไว้ แล้วเปิดเผยเฉพาะ operation ของ stack

// Before — Stack IS-A List, leaking add/get/removeAt to callers
class List<T> {
private items: T[] = [];
add(item: T): void { this.items.push(item); }
removeLast(): T | undefined { return this.items.pop(); }
size(): number { return this.items.length; }
}
class Stack<T> extends List<T> {
push(item: T): void { this.add(item); }
pop(): T | undefined { return this.removeLast(); }
}
// After — Stack HAS-A List and delegates only what it needs
class Stack<T> {
private list = new List<T>();
push(item: T): void { this.list.add(item); }
pop(): T | undefined { return this.list.removeLast(); }
size(): number { return this.list.size(); }
}
classDiagram
  class List {
    +add(item)
    +size() number
  }
  class Stack
  Stack --> List : holds & delegates
  note for Stack "Stack HAS-A List, not IS-A List"
Stack เลิกเป็น List แล้วเริ่มถือ List ไว้แทน
  1. สร้าง field ใน subclass สำหรับถือ instance ของ superclass เดิม แล้วกำหนดค่าเริ่มต้นให้ จะสร้าง instance ใหม่ หรือใช้ตัว object เองถ้ากำลังแกะ self-reference อยู่ก็ได้
  2. สำหรับแต่ละ method ของ superclass ที่ subclass ใช้จริง ให้เพิ่ม method ที่ delegate ซึ่งส่งต่อการเรียกไปยัง field นั้น
  3. ลบความสัมพันธ์ extends / embedding ออก เพื่อให้ class ไม่ inherit interface ทั้งหมดของพ่อแม่อีกต่อไป
  4. ปรับ caller ที่พึ่งพาสมาชิกที่ inherit มาซึ่งไม่ควรเข้าถึงได้ตั้งแต่แรก — นี่คือรอยรั่วที่คุณกำลังอุด
  5. รัน test หลังเดินสาย method ที่ delegate แต่ละตัวเสร็จ
  6. เมื่อ delegation อยู่ที่แล้ว คุณก็มีอิสระที่จะแคบ interface ที่เปิดเผยและปกป้อง invariant ที่ inheritance เดิมละเมิด

ใช้ท่านี้เมื่อ subclass ปฏิเสธมรดกที่ได้รับเสียเป็นส่วนใหญ่ เมื่อ inheritance ปล่อย operation ที่ทำลายกฎของ subclass เองรั่วออกมา หรือเมื่อ “is-a” ไม่เคยเป็นจริงตั้งแต่แรก หลังย้ายเสร็จ คุณจะคุมได้เป๊ะ ๆ ว่า operation ไหนเป็น public จึงบังคับ invariant ได้จริง

ต้นทุนคือ boilerplate ของการส่งต่อเล็กน้อย — หนึ่ง method สั้น ๆ ต่อหนึ่ง operation ที่ delegate นั่นเป็นราคาเล็ก ๆ และซื่อตรงสำหรับความสัมพันธ์ที่ถูกต้องแม่นยำ ใน Go นี่เป็นเพียงการเลือก field ที่ถือไว้แทน embedding ส่วนใน Rust delegation คือเส้นทาง เดียว ดังนั้น refactoring นี้จึงเป็นแค่ “เขียน Rust แบบ idiomatic ตั้งแต่แรก”

ใช้ Replace Inheritance with Delegation เมื่อหลีกเลี่ยงเมื่อ
subclass ใช้เพียงบางส่วนของ superclass interfacesubclass เป็น “is-a” อย่างแท้จริง ไม่ใช่แค่ “has-a”
inheritance ทำให้ subclass ผูกพันกับ superclass implementationdelegation จะเพิ่ม forwarding methods มากเกินไป
ต้องการ swap implementation โดยไม่เปลี่ยน interfaceclient code ต้อง downcast ไปยัง superclass type

⚠️ ไม่ควร Replace Inheritance with Delegation เมื่อ:

  • subclass ใช้ superclass interface ครบทุก method
  • Liskov Substitution Principle ยังคงใช้ได้ — subclass คือ superclass จริง ๆ
  • delegation จะสร้าง wrapper ที่ไม่เพิ่ม value
Replace Inheritance with Delegation แก้ปัญหาใด?
หลัง refactoring class นำพ่อแม่เดิมมาใช้ซ้ำอย่างไร?
ทำไม refactoring นี้จึง "ฟรี" ใน Rust?
อะไรคือต้นทุนหลักของการเลือก delegation แทน inheritance?