Replace Superclass with Delegation
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Replace Superclass with Delegation เปลี่ยนความสัมพันธ์แบบ inheritance ให้เป็นแบบ has-a แทนที่จะเขียน Stack extends List ก็ให้ stack ถือ list ไว้ใน field private แล้วเรียกเข้าไปเฉพาะงานไม่กี่อย่างที่ต้องใช้จริง subclass ยังได้ behavior ที่ต้องพึ่งครบ แต่ไม่โดนบังคับให้ inherit และเปิดเผยสมาชิกทุกตัวของ superclass ที่ไม่เคยอยากได้
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”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 interfaceclass 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 needsclass 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(); }}# Before — Stack inherits the entire list interfaceclass CustomList: def __init__(self): self._items = []
def add(self, item): self._items.append(item)
def remove(self, item): self._items.remove(item)
def get(self, index): return self._items[index]
def size(self): return len(self._items)
# A Stack should not expose remove(item) or get(index)!class Stack(CustomList): def push(self, item): self.add(item)
# After — Stack holds a list and delegates only what it needsclass Stack: def __init__(self): self._storage = CustomList()
def push(self, item): self._storage.add(item)
def pop(self): n = self._storage.size() if n == 0: return None top = self._storage.get(n - 1) self._storage.remove(top) return top
def size(self): return self._storage.size()// Go has no inheritance. Embedding a List would promote ALL of its methods// onto Stack — the same leaky bequest. So the idiomatic shape is a NAMED// field (not embedding) holding the list, with Stack delegating explicitly.
type List[T any] struct { items []T}
func (l *List[T]) Add(item T) { l.items = append(l.items, item) }func (l *List[T]) Get(i int) T { return l.items[i] }func (l *List[T]) Size() int { return len(l.items) }func (l *List[T]) RemoveLast() { l.items = l.items[:len(l.items)-1] }
type Stack[T any] struct { storage List[T] // held as a field, not embedded — no leaked methods}
func (s *Stack[T]) Push(item T) { s.storage.Add(item) }
func (s *Stack[T]) Pop() (T, bool) { var zero T if s.storage.Size() == 0 { return zero, false } top := s.storage.Get(s.storage.Size() - 1) s.storage.RemoveLast() return top, true}
func (s *Stack[T]) Size() int { return s.storage.Size() }// Rust has no inheritance either. A Stack simply owns a Vec (or a custom// list) as a field and exposes only the stack operations. Delegation by// holding a field is the only option — and the right one.
struct List<T> { items: Vec<T>,}
impl<T> List<T> { fn new() -> Self { List { items: Vec::new() } } fn add(&mut self, item: T) { self.items.push(item); } fn remove_last(&mut self) -> Option<T> { self.items.pop() } fn size(&self) -> usize { self.items.len() }}
struct Stack<T> { storage: List<T>,}
impl<T> Stack<T> { fn new() -> Self { Stack { storage: List::new() } } fn push(&mut self, item: T) { self.storage.add(item); } fn pop(&mut self) -> Option<T> { self.storage.remove_last() } fn size(&self) -> usize { self.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) กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- เพิ่ม field ใน subclass สำหรับถือ instance ของ superclass เดิม แล้ว initialize ให้เรียบร้อย (ปกติจะสร้างตัวใหม่ขึ้นมา หรือรับเข้ามาเป็น argument ของ constructor)
- สำหรับแต่ละสมาชิกที่ inherit มาและ subclass ใช้จริง ให้เพิ่ม method forwarding เล็ก ๆ ที่ delegate ไปยัง field
- อัปเดต method ของ subclass เองให้เรียก field แทน
super - ลบสาย
extends/ inheritance ออก เพื่อให้ subclass ไม่เป็น subtype ของพ่อแม่อีกต่อไป รัน test หลังจากย้ายแต่ละสมาชิก - ตัด 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 ไม่เป็นจริงในทุก context | delegation จะทำให้ต้อง forward ทุก method ที่ต้องการ |
⚠️ ไม่ควร Replace Superclass with Delegation เมื่อ:
- relationship นั้น IS-A ที่ถูกต้องจริง ๆ — อย่า delegate โดยไม่จำเป็น
- caller ใช้ polymorphism กับ type นี้ — delegation ทำลาย type hierarchy
- delegation จะเพิ่ม boilerplate มากกว่า complexity ที่แก้ได้