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

Composition Over Inheritance

เมื่อ object หนึ่งต้องการความสามารถของอีก object หนึ่ง คุณมีทางเลือกกว้าง ๆ สองทาง Inheritance บอกว่า “FlyingDuck เป็น Duck” และดึงพฤติกรรมของ parent เข้ามาโดยอัตโนมัติ ส่วน Composition บอกว่า “Duck มี flying behavior” และเก็บ reference ไปยัง object แยกต่างหากที่จัดหาความสามารถนั้นให้ ทั้งสองนำ code กลับมาใช้ซ้ำ แต่แก่ตัวลงไปคนละแบบมากเมื่อความต้องการเปลี่ยนไป

inheritance ผูก subclass เข้ากับ parent ของตัวเองตอน compile time นั่นแข็งทื่อในสามแง่ แง่แรก subclass สืบทอดทุกอย่าง รวมถึงพฤติกรรมที่ไม่ต้องการ — ดังนั้น RubberDuck ที่บินไม่ได้ก็ยังสืบทอด fly() มา แง่ที่สอง พฤติกรรมถูกตรึงไว้ตลอดอายุของ object คุณไม่สามารถเปลี่ยนวิธีที่เป็ดบินได้ตอน runtime แง่ที่สาม ลำดับชั้นที่ลึก coupling ฟีเจอร์ที่ไม่เกี่ยวข้องกันเข้าด้วยกัน: เมื่อคุณต้องการทุกชุดผสมของบินได้หรือบินไม่ได้และร้องได้หรือร้องไม่ได้ ต้นไม้ของ subclass ก็ระเบิดแบบ combinatorial และการเปลี่ยนแปลงใกล้รากก็กระเพื่อมไปถึงทุกใบ

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 fly
rubber.setFly(new FlyWithWings());
console.log(rubber.performFly()); // 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
inheritance ตรึงพฤติกรรมไว้ในต้นไม้ ส่วน composition ถือชิ้นส่วนที่สลับได้

แนวทาง “นิยม composition มากกว่า inheritance” นี้คือกระดูกสันหลังของหลาย pattern: Strategy, Decorator และ State ล้วนแทนที่ subclass ด้วย object ที่ถูกถือไว้และสับเปลี่ยนกันได้ ไม่ได้ห้าม inheritance — ความสัมพันธ์แบบ is-a ที่แท้จริงพร้อม base ที่มั่นคงก็ยังเป็นเครื่องมือที่ถูกต้อง บทเรียนคือให้เอื้อมไปหา composition ก่อน แล้วปล่อยให้ inheritance พิสูจน์ว่าจำเป็น

อะไรคือความต่างหลักระหว่าง inheritance และ composition?
ทำไมลำดับชั้น inheritance ที่ลึกจึงกลายเป็นแข็งทื่อ?
ในตัวอย่างที่ปรับโครงสร้างแล้ว เป็ดเปลี่ยนวิธีที่บินได้อย่างไร?
pattern ใดที่สร้างขึ้นบนการนิยม composition มากกว่า inheritance?