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

Replace Subclass with Delegation

Replace Subclass with Delegation ยุบครอบครัวของ subclass ที่ต่างกันตาม แกนของความผันแปรเพียงแกนเดียว ให้เหลือ class เดียวที่ถือ strategy object ไว้ แทนที่จะมี subclass RegularBooking และ PremiumBooking คุณมี Booking เดียวที่ delegate behavior ที่ผันแปรไปยัง PricingPolicy ที่ถูก inject เข้ามา ความผันแปรย้ายจากลำดับชั้นของ type ไปสู่ field ที่คุณตั้งค่าได้ — หรือกระทั่งเปลี่ยนได้ขณะ runtime

การ subclass เป็นการเลือกครั้งเดียวแบบ static: object เป็น PremiumBooking ตลอดไป และถ้า booking เดียวกันผันแปรตามแกนที่ สอง ด้วย — เช่น ราคาวันธรรมดากับวันหยุดสุดสัปดาห์ และ คืนเงินได้กับคืนเงินไม่ได้ — คุณก็จะเผชิญกับการระเบิดเชิงผสมของ subclass เมื่อความแตกต่างระหว่าง subclass แท้จริงเป็นเพียง “อัลกอริทึมตัวไหนทำงานตรงนี้” ความแตกต่างนั้นควรอยู่ใน delegate ไม่ใช่ใน type นี่คือการเคลื่อนไหวคลาสสิกจาก inheritance ไปสู่ pattern Strategy

booking ที่ราคาขึ้นกับ tier ก่อนหน้านี้แต่ละ tier เป็น subclass หลังจากนั้น tier เป็น PricingPolicy ที่ Booking เดียว delegate ไปหา

// Before — one subclass per pricing tier
abstract class Booking {
constructor(protected base: number) {}
abstract price(): number;
}
class RegularBooking extends Booking {
price(): number { return this.base; }
}
class PremiumBooking extends Booking {
price(): number { return this.base * 1.5 + 20; }
}
// After — one Booking that delegates pricing to a strategy
interface PricingPolicy {
price(base: number): number;
}
class RegularPricing implements PricingPolicy {
price(base: number): number { return base; }
}
class PremiumPricing implements PricingPolicy {
price(base: number): number { return base * 1.5 + 20; }
}
class Booking {
constructor(private base: number, private pricer: PricingPolicy) {}
price(): number { return this.pricer.price(this.base); }
}
classDiagram
  class Booking {
    -pricer: PricingPolicy
    +price() number
  }
  class PricingPolicy {
    <<interface>>
    +price(base) number
  }
  class RegularPricing
  class PremiumPricing
  Booking --> PricingPolicy : delegates
  PricingPolicy <|.. RegularPricing
  PricingPolicy <|.. PremiumPricing
ลำดับชั้นของ tier กลายเป็น Booking เดียวที่ delegate ไปยัง PricingPolicy
  1. ระบุแกนเดียวที่ subclass ต่างกัน — method (หรือกลุ่มเล็ก ๆ ของ method) ที่บอดี้ต่างกัน
  2. นิยาม strategy interface (interface ใน TypeScript/Go, protocol หรือ duck-typed class ใน Python, trait ใน Rust) ที่จับ behavior ที่ผันแปรนั้นไว้
  3. สำหรับแต่ละ subclass ที่มีอยู่ ให้สร้าง strategy implementation ที่ถือ logic ผันแปรเวอร์ชัน
  4. เพิ่ม field บน base class เพื่อถือ strategy ไว้ แล้วทำให้ base class delegate method ที่ผันแปรไปยัง field นั้น
  5. แทนการเรียก new SubclassX() แต่ละครั้งด้วย new Base(..., new StrategyX()) แล้วรัน test
  6. ลบ subclass ที่ตอนนี้ว่างเปล่าทิ้ง

หยิบสิ่งนี้มาใช้เมื่อ subclass ต่างกันเพียงในอัลกอริทึมที่สลับเปลี่ยนได้ เมื่อ object ต้อง เปลี่ยน รูปแบบขณะ runtime (สิ่งที่ inheritance ทำไม่ได้) หรือเมื่อแกนของความผันแปรสองแกนที่เป็นอิสระจะคูณกันกลายเป็นการระเบิดของ subclass — การประกอบ strategy field สองตัวขยายเป็นเชิงเส้น ส่วน subclass ขยายเป็นเชิงคูณ

ข้อแลกเปลี่ยนคือมี object เพิ่มมาหนึ่งตัวกับชั้นการอ้อมอีกชั้น เพราะตอนนี้ caller ต้องเดินสาย strategy ตอนสร้าง base object ซึ่งส่วนใหญ่ก็คุ้ม ส่วนใน Go และ Rust นี่คือวิธีปกติในการทำให้ behavior ผันแปรอยู่แล้ว เพราะทั้งสองภาษาไม่มีลำดับชั้น subclass ให้ใช้ คุณจึงเขียนแบบ delegate มาตั้งแต่ต้น

ใช้ Replace Subclass with Delegation เมื่อหลีกเลี่ยงเมื่อ
object ต้องการเปลี่ยน “type” ใน runtimesubclass type คงที่ตลอด lifecycle
object ต้องการเป็นหลาย type พร้อมกันsubclass มีแค่ 2 variant และไม่น่าจะเพิ่ม
inheritance hierarchy ลึกเกินไปและซับซ้อนdelegation จะเพิ่ม indirection ที่ไม่ได้ประโยชน์

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

  • subclass type ไม่เปลี่ยนใน runtime — inheritance เหมาะกว่า
  • มีแค่ 2 subclass ที่ง่ายและไม่น่าจะซับซ้อนขึ้น
  • delegation จะเพิ่ม code มากกว่าที่ inheritance ลด
Replace Subclass with Delegation เหมาะที่สุดเมื่อ subclass ต่างกันตามแกนของความผันแปรกี่แกน?
refactoring นี้เคลื่อนไปสู่ design pattern ใด?
strategy field ทำอะไรได้ที่การเลือก subclass ทำไม่ได้?
ทำไมนี่จึงเป็นรูปแบบค่าตั้งต้นใน Go และ Rust อยู่แล้ว?