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

Replace Superclass with Delegation

Replace Superclass with Delegation เปลี่ยนความสัมพันธ์แบบ inheritance ให้เป็นแบบ has-a แทนที่จะเขียน Stack extends List ก็ให้ stack ถือ list ไว้ใน field private แล้วเรียกเข้าไปเฉพาะงานไม่กี่อย่างที่ต้องใช้จริง subclass ยังได้ behavior ที่ต้องพึ่งครบ แต่ไม่โดนบังคับให้ inherit และเปิดเผยสมาชิกทุกตัวของ superclass ที่ไม่เคยอยากได้

class หนึ่ง extends อีก class เพียงเพื่อใช้ method ไม่กี่ตัวซ้ำ แล้วดันลาก public interface ทั้งชุดติดมาด้วย อย่าง Stack extends List ก็ inherit add, remove, get(index), clear มาหมด ผลคือ caller เรียก stack.remove(item) ดึงของจากตรงกลางได้ ทำลาย invariant ที่ stack มีไว้ปกป้องพอดี ข้ออ้าง “is-a” จึงเป็นเท็จ เพราะ stack ไม่ใช่ list ชนิดหนึ่ง แต่แค่ ใช้ list เท่านั้น interface ที่ inherit มาจึงเก้งก้าง รั่ว และใหญ่เกินจำเป็น เป็นตัวอย่างคลาสสิกของ Refused Bequest ที่ทายาทแอบทิ้งมรดกส่วนใหญ่ที่ได้รับมา

Stack extends List เพียงเพื่อเอาที่เก็บข้อมูล ผลคือ operation ของ list รั่วออกมาหมด หลัง refactor stack จะถือ List ไว้เฉย ๆ แล้วเปิดเผยแค่ push/pop/size

// Before — Stack inherits the entire List interface
class List<T> {
private items: T[] = [];
add(item: T): void { this.items.push(item); }
remove(item: T): void {
const i = this.items.indexOf(item);
if (i >= 0) this.items.splice(i, 1);
}
get(index: number): T { return this.items[index]; }
size(): number { return this.items.length; }
}
// A Stack should not expose remove(item) or get(index)!
class Stack<T> extends List<T> {
push(item: T): void { this.add(item); }
pop(): T | undefined { return undefined; /* awkward */ }
}
// After — Stack holds a List and delegates only what it needs
class Stack<T> {
private storage = new List<T>();
push(item: T): void { this.storage.add(item); }
pop(): T | undefined {
const n = this.storage.size();
if (n === 0) return undefined;
const top = this.storage.get(n - 1);
this.storage.remove(top);
return top;
}
size(): number { return this.storage.size(); }
}
classDiagram
  class List~T~ {
    +add(item)
    +remove(item)
    +size() number
    +clear()
  }
  class Stack~T~
  List <|-- Stack : before (extends)
  class StackAfter["Stack"] {
    -items: List~T~
    +push(item)
    +pop() T
  }
  StackAfter --> List : after (holds & delegates)
Stack เลิก extends List แล้วถือไว้เป็น field โดย delegate เฉพาะที่ต้องใช้
  1. เพิ่ม field ใน subclass สำหรับถือ instance ของ superclass เดิม แล้ว initialize ให้เรียบร้อย (ปกติจะสร้างตัวใหม่ขึ้นมา หรือรับเข้ามาเป็น argument ของ constructor)
  2. สำหรับแต่ละสมาชิกที่ inherit มาและ subclass ใช้จริง ให้เพิ่ม method forwarding เล็ก ๆ ที่ delegate ไปยัง field
  3. อัปเดต method ของ subclass เองให้เรียก field แทน super
  4. ลบสาย extends / inheritance ออก เพื่อให้ subclass ไม่เป็น subtype ของพ่อแม่อีกต่อไป รัน test หลังจากย้ายแต่ละสมาชิก
  5. ตัด surface ที่ delegate ให้เหลือเฉพาะ operation ที่ class ควรเปิดเผยจริง ๆ แล้วสมาชิกเก้งก้างที่เคย inherit มาก็จะหายไปจาก interface เอง

หยิบสิ่งนี้มาใช้เมื่อ subclass ใช้พ่อแม่เพียงเสี้ยวเดียว เมื่อการ inherit พ่อแม่ทำให้การทำงานรั่วออกมาจนทำลาย invariant ของ subclass หรือเมื่อความสัมพันธ์ “is-a” จริง ๆ แล้วเป็น “uses-a” delegation ให้คุณเปิดเผย interface ที่ถูกต้องเป๊ะ ๆ และทำให้สอง class นั้นมีอิสระที่จะวิวัฒน์แยกกัน

ต้นทุนคือ method forwarding ที่ต้องเขียนเองสำหรับสมาชิกที่คุณ เก็บ ไว้ ซึ่ง inheritance แบบตรง ๆ แถมให้ฟรี ถ้า subclass เป็น superclass จริงโดยชอบธรรม และต้องใช้ interface ทั้งชุด ก็ปล่อย inheritance ไว้แบบนั้น ท่ากลับกันคือ Replace Delegation with Inheritance ซึ่งคุ้มจะทำเฉพาะตอนที่คุณพบว่าตัวเอง forward ทั้ง interface แบบตรงตัวอยู่แล้ว ส่วน Go กับ Rust ตัดสินใจแทนคุณไปแล้ว เพราะไม่มี inheritance ให้ใช้ คุณจึงถือ field แล้ว delegate ตั้งแต่ต้น และ struct embedding ของ Go ก็เป็นเครื่องมือกลางทางที่จงใจออกแบบมา ให้หยิบมาใช้เฉพาะตอนที่คุณ ต้องการ ให้ interface ทั้งชุดถูกเลื่อนขึ้นมาจริง ๆ

ใช้ Replace Superclass with Delegation เมื่อหลีกเลี่ยงเมื่อ
subclass ใช้แค่บางส่วนของ superclass และต้องการซ่อนส่วนที่เหลือclass นั้น IS-A superclass จริง ๆ ทุก method เหมาะสม
subclass ต้องการ inherit จาก class อื่นด้วย (multiple inheritance)ต้องการ polymorphism — caller ต้องการ treat เป็น superclass type
IS-A relationship ไม่เป็นจริงในทุก contextdelegation จะทำให้ต้อง forward ทุก method ที่ต้องการ

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

  • relationship นั้น IS-A ที่ถูกต้องจริง ๆ — อย่า delegate โดยไม่จำเป็น
  • caller ใช้ polymorphism กับ type นี้ — delegation ทำลาย type hierarchy
  • delegation จะเพิ่ม boilerplate มากกว่า complexity ที่แก้ได้
สถานการณ์ใดเรียกร้องให้ใช้ Replace Superclass with Delegation?
หลังจาก refactoring แล้ว subclass เดิมเข้าถึง behavior ที่ต้องการอย่างไร?
การ inherit interface ที่ใหญ่เกินจำเป็นเป็นตัวอย่างของ code smell ตัวใด?
ทำไม Go จึงใช้ field แบบ NAMED แทน struct embedding สำหรับกรณีนี้?