Composition Over Inheritance
สองวิธีในการนำพฤติกรรมกลับมาใช้ซ้ำ
หัวข้อที่มีชื่อว่า “สองวิธีในการนำพฤติกรรมกลับมาใช้ซ้ำ”เมื่อ object หนึ่งต้องการความสามารถของอีก object หนึ่ง คุณมีทางเลือกกว้าง ๆ สองทาง Inheritance บอกว่า “FlyingDuck เป็น Duck” และดึงพฤติกรรมของ parent เข้ามาโดยอัตโนมัติ ส่วน Composition บอกว่า “Duck มี flying behavior” และเก็บ reference ไปยัง object แยกต่างหากที่จัดหาความสามารถนั้นให้ ทั้งสองนำ code กลับมาใช้ซ้ำ แต่แก่ตัวลงไปคนละแบบมากเมื่อความต้องการเปลี่ยนไป
ที่ที่ inheritance ลึก ๆ สร้างความเจ็บปวด
หัวข้อที่มีชื่อว่า “ที่ที่ inheritance ลึก ๆ สร้างความเจ็บปวด”inheritance ผูก subclass เข้ากับ parent ของตัวเองตอน compile time นั่นแข็งทื่อในสามแง่ แง่แรก subclass สืบทอดทุกอย่าง รวมถึงพฤติกรรมที่ไม่ต้องการ — ดังนั้น RubberDuck ที่บินไม่ได้ก็ยังสืบทอด fly() มา แง่ที่สอง พฤติกรรมถูกตรึงไว้ตลอดอายุของ object คุณไม่สามารถเปลี่ยนวิธีที่เป็ดบินได้ตอน runtime แง่ที่สาม ลำดับชั้นที่ลึก coupling ฟีเจอร์ที่ไม่เกี่ยวข้องกันเข้าด้วยกัน: เมื่อคุณต้องการทุกชุดผสมของบินได้หรือบินไม่ได้และร้องได้หรือร้องไม่ได้ ต้นไม้ของ subclass ก็ระเบิดแบบ combinatorial และการเปลี่ยนแปลงใกล้รากก็กระเพื่อมไปถึงทุกใบ
composition ช่วยได้อย่างไร
หัวข้อที่มีชื่อว่า “composition ช่วยได้อย่างไร”composition แทนที่ต้นไม้ด้วยชุดชิ้นส่วนที่สับเปลี่ยนกันได้จำนวนน้อย ๆ Duck ถือ FlyBehavior และ QuackBehavior แต่ละอันเป็น interface ที่มี implementation ที่เป็นรูปธรรมไม่กี่อัน ชุดผสมใหม่ ๆ ไม่มีค่าใช้จ่ายเลย — คุณแค่เสียบชิ้นส่วนต่าง ๆ เข้าด้วยกัน — และเพราะชิ้นส่วนถูกเก็บเป็น field คุณจึงสลับชิ้นส่วนได้แม้ตอน runtime ตัวอย่างด้านล่างปรับโครงสร้าง Duck ที่ hard-code การบินผ่าน inheritance ให้กลายเป็นตัวที่ delegate ไปยัง FlyBehavior ที่ถูก inject เข้ามา
// Composition: the duck delegates flying to an injected behaviour.interface FlyBehavior { fly(): string;}
class FlyWithWings implements FlyBehavior { fly(): string { return 'flying with wings'; }}
class NoFly implements FlyBehavior { fly(): string { return 'cannot fly'; }}
class Duck { constructor(private behavior: FlyBehavior) {}
performFly(): string { return this.behavior.fly(); }
setFly(behavior: FlyBehavior): void { this.behavior = behavior; // swap behaviour at runtime }}
const rubber = new Duck(new NoFly());console.log(rubber.performFly()); // cannot flyrubber.setFly(new FlyWithWings());console.log(rubber.performFly()); // flying with wingsfrom typing import Protocol
# Composition: the duck delegates flying to an injected behaviour.class FlyBehavior(Protocol): def fly(self) -> str: ...
class FlyWithWings: def fly(self) -> str: return "flying with wings"
class NoFly: def fly(self) -> str: return "cannot fly"
class Duck: def __init__(self, behavior: FlyBehavior) -> None: self._behavior = behavior
def perform_fly(self) -> str: return self._behavior.fly()
def set_fly(self, behavior: FlyBehavior) -> None: self._behavior = behavior # swap behaviour at runtime
rubber = Duck(NoFly())print(rubber.perform_fly()) # cannot flyrubber.set_fly(FlyWithWings())print(rubber.perform_fly()) # flying with wingspackage main
import "fmt"
// Composition: the duck delegates flying to an injected behaviour.type FlyBehavior interface { Fly() string}
type FlyWithWings struct{}
func (FlyWithWings) Fly() string { return "flying with wings" }
type NoFly struct{}
func (NoFly) Fly() string { return "cannot fly" }
type Duck struct { behavior FlyBehavior}
func (d Duck) PerformFly() string { return d.behavior.Fly() }
func (d *Duck) SetFly(b FlyBehavior) { d.behavior = b } // swap at runtime
func main() { rubber := Duck{behavior: NoFly{}} fmt.Println(rubber.PerformFly()) // cannot fly rubber.SetFly(FlyWithWings{}) fmt.Println(rubber.PerformFly()) // flying with wings}// Composition: the duck delegates flying to an injected behaviour.trait FlyBehavior { fn fly(&self) -> &'static str;}
struct FlyWithWings;impl FlyBehavior for FlyWithWings { fn fly(&self) -> &'static str { "flying with wings" }}
struct NoFly;impl FlyBehavior for NoFly { fn fly(&self) -> &'static str { "cannot fly" }}
struct Duck { behavior: Box<dyn FlyBehavior>,}
impl Duck { fn perform_fly(&self) -> &'static str { self.behavior.fly() }
fn set_fly(&mut self, behavior: Box<dyn FlyBehavior>) { self.behavior = behavior; // swap behaviour at runtime }}
fn main() { let mut rubber = Duck { behavior: Box::new(NoFly) }; println!("{}", rubber.perform_fly()); // cannot fly rubber.set_fly(Box::new(FlyWithWings)); println!("{}", rubber.perform_fly()); // flying with wings}ต้นไม้กับชิ้นส่วน
หัวข้อที่มีชื่อว่า “ต้นไม้กับชิ้นส่วน”ทางซ้าย ลำดับชั้นที่แข็งทื่อต้องการ subclass ใหม่สำหรับทุกชุดผสมของความสามารถ ทางขวา เป็ดถือพฤติกรรมที่สามารถสลับได้
classDiagram
class Duck
class FlyingDuck
class NonFlyingDuck
Duck <|-- FlyingDuck
Duck <|-- NonFlyingDuck
class FlexibleDuck
class FlyBehavior {
<<interface>>
+fly()
}
FlexibleDuck --> FlyBehavior : has-a, swappable แนวทาง “นิยม composition มากกว่า inheritance” นี้คือกระดูกสันหลังของหลาย pattern: Strategy, Decorator และ State ล้วนแทนที่ subclass ด้วย object ที่ถูกถือไว้และสับเปลี่ยนกันได้ ไม่ได้ห้าม inheritance — ความสัมพันธ์แบบ is-a ที่แท้จริงพร้อม base ที่มั่นคงก็ยังเป็นเครื่องมือที่ถูกต้อง บทเรียนคือให้เอื้อมไปหา composition ก่อน แล้วปล่อยให้ inheritance พิสูจน์ว่าจำเป็น