Replace Subclass with Delegation
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Replace Subclass with Delegation ยุบครอบครัวของ subclass ที่ต่างกันตาม แกนของความผันแปรเพียงแกนเดียว ให้เหลือ class เดียวที่ถือ strategy object ไว้ แทนที่จะมี subclass RegularBooking และ PremiumBooking คุณมี Booking เดียวที่ delegate behavior ที่ผันแปรไปยัง PricingPolicy ที่ถูก inject เข้ามา ความผันแปรย้ายจากลำดับชั้นของ type ไปสู่ field ที่คุณตั้งค่าได้ — หรือกระทั่งเปลี่ยนได้ขณะ runtime
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”การ 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 tierabstract 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 strategyinterface 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); }}# Before — one subclass per pricing tierclass Booking: def __init__(self, base): self.base = base
def price(self): raise NotImplementedError
class RegularBooking(Booking): def price(self): return self.base
class PremiumBooking(Booking): def price(self): return self.base * 1.5 + 20
# After — one Booking that delegates pricing to a strategyclass RegularPricing: def price(self, base): return base
class PremiumPricing: def price(self, base): return base * 1.5 + 20
class Booking: def __init__(self, base, pricer): self.base = base self.pricer = pricer
def price(self): return self.pricer.price(self.base)// Go has no subclasses to begin with. The idiomatic shape is exactly the// "after": a Booking value holds a PricingPolicy interface and delegates.
type PricingPolicy interface { Price(base float64) float64}
type RegularPricing struct{}
func (RegularPricing) Price(base float64) float64 { return base }
type PremiumPricing struct{}
func (PremiumPricing) Price(base float64) float64 { return base*1.5 + 20 }
type Booking struct { Base float64 Pricer PricingPolicy}
func (b Booking) Price() float64 { return b.Pricer.Price(b.Base)}
// booking := Booking{Base: 100, Pricer: PremiumPricing{}}// Rust has no subclasses. The strategy is a trait object (or a generic// type parameter) held by the single Booking struct.
trait PricingPolicy { fn price(&self, base: f64) -> f64;}
struct RegularPricing;impl PricingPolicy for RegularPricing { fn price(&self, base: f64) -> f64 { base }}
struct PremiumPricing;impl PricingPolicy for PremiumPricing { fn price(&self, base: f64) -> f64 { base * 1.5 + 20.0 }}
struct Booking { base: f64, pricer: Box<dyn PricingPolicy>,}
impl Booking { fn price(&self) -> f64 { self.pricer.price(self.base) }}
// let booking = Booking { base: 100.0, pricer: Box::new(PremiumPricing) };classDiagram
class Booking {
-pricer: PricingPolicy
+price() number
}
class PricingPolicy {
<<interface>>
+price(base) number
}
class RegularPricing
class PremiumPricing
Booking --> PricingPolicy : delegates
PricingPolicy <|.. RegularPricing
PricingPolicy <|.. PremiumPricing กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- ระบุแกนเดียวที่ subclass ต่างกัน — method (หรือกลุ่มเล็ก ๆ ของ method) ที่บอดี้ต่างกัน
- นิยาม strategy interface (interface ใน TypeScript/Go, protocol หรือ duck-typed class ใน Python, trait ใน Rust) ที่จับ behavior ที่ผันแปรนั้นไว้
- สำหรับแต่ละ subclass ที่มีอยู่ ให้สร้าง strategy implementation ที่ถือ logic ผันแปรเวอร์ชัน
- เพิ่ม field บน base class เพื่อถือ strategy ไว้ แล้วทำให้ base class delegate method ที่ผันแปรไปยัง field นั้น
- แทนการเรียก
new SubclassX()แต่ละครั้งด้วยnew Base(..., new StrategyX())แล้วรัน test - ลบ 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” ใน runtime | subclass type คงที่ตลอด lifecycle |
| object ต้องการเป็นหลาย type พร้อมกัน | subclass มีแค่ 2 variant และไม่น่าจะเพิ่ม |
| inheritance hierarchy ลึกเกินไปและซับซ้อน | delegation จะเพิ่ม indirection ที่ไม่ได้ประโยชน์ |
⚠️ ไม่ควร Replace Subclass with Delegation เมื่อ:
- subclass type ไม่เปลี่ยนใน runtime — inheritance เหมาะกว่า
- มีแค่ 2 subclass ที่ง่ายและไม่น่าจะซับซ้อนขึ้น
- delegation จะเพิ่ม code มากกว่าที่ inheritance ลด