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

Extract Superclass & Interface

Extract Superclass สังเกตเห็นสอง class ที่เติบโตขึ้นอย่างเป็นอิสระจนมี field และ behavior ร่วมกัน แล้วแนะนำพ่อแม่ร่วมตัวใหม่ขึ้นมาเพื่อเก็บส่วนที่ทับซ้อนกัน — จากนั้นดึงสมาชิกที่ใช้ร่วมกันขึ้นไปไว้ในนั้น ส่วน Extract Interface เป็นญาติที่เบากว่า: เมื่อ class ต่าง ๆ มีเพียง protocol ร่วมกัน (ชุดของ method signature ที่ client พึ่งพา) แต่ไม่มี implementation คุณก็ extract protocol นั้นออกมาเป็น interface — เป็น trait ใน Rust, เป็น structural interface ใน Go — เพื่อให้ caller พึ่งพา contract แทนที่จะเป็น type ที่เป็นรูปธรรม

สอง class ที่มี field และ method ทับซ้อนกันอย่างน่าสงสัยคือรูปแบบหนึ่งของ Duplicated Code ที่เงียบ ๆ — ต่างฝ่ายไม่รู้ว่าอีกฝ่ายมีอยู่ แต่ทั้งคู่ต่างก็ดูแล name, monthlyCharge, ที่อยู่ Extract Superclass ให้บ้านกับส่วนที่ทับซ้อนนั้น เมื่อส่วนทับซ้อนเป็นเชิง behavior ล้วน ๆ — หลาย class ที่ไม่เกี่ยวข้องกันแต่ทั้งหมดต้องถูก เรียกเก็บเงิน, หรือ แสดงผล, หรือ เปรียบเทียบ — และไม่มีข้อมูลร่วมกัน interface ก็จับ contract ไว้ได้โดยไม่บังคับให้เกิดลำดับชั้นข้อมูลที่ผิด

Department และ Employee ต่างก็มีชื่อและคำนวณค่าใช้จ่ายรายเดือน เรา extract superclass Party สำหรับส่วนที่ใช้ร่วมกัน และ interface Billable สำหรับ protocol ที่ใช้ร่วมกัน

// Before — two classes independently grew the same shape
class Department {
constructor(public name: string, public staffCount: number) {}
monthlyCharge(): number {
return this.staffCount * 100;
}
}
class Employee {
constructor(public name: string, public salary: number) {}
monthlyCharge(): number {
return this.salary / 12;
}
}
// After — shared protocol as an interface, shared field via a superclass
interface Billable {
monthlyCharge(): number;
}
abstract class Party implements Billable {
constructor(public name: string) {}
abstract monthlyCharge(): number;
}
class Department extends Party {
constructor(name: string, public staffCount: number) {
super(name);
}
monthlyCharge(): number {
return this.staffCount * 100;
}
}
class Employee extends Party {
constructor(name: string, public salary: number) {
super(name);
}
monthlyCharge(): number {
return this.salary / 12;
}
}
classDiagram
  class Party {
    +name string
    +monthlyCharge() number
  }
  class Department
  class Employee
  Party <|-- Department
  Party <|-- Employee
  class Billable {
    <<interface>>
    +monthlyCharge() number
  }
  Billable <|.. Party
Department และ Employee ได้พ่อแม่ Party ร่วมกันและ contract Billable
  1. ยืนยันส่วนที่ทับซ้อน ลิสต์ field และ method ที่สอง class ใช้ร่วมกัน ถ้าใช้ทั้ง state และ behavior ร่วมกัน ให้เอนไปทาง superclass หากใช้เพียง signature ร่วมกัน ให้เอนไปทาง interface หรือ trait
  2. สร้าง superclass เปล่า (หรือ interface / trait) ใน Go และ Rust ให้สร้าง struct ที่ใช้ร่วมกันและ interface หรือ trait
  3. สำหรับ superclass ให้ทั้งสอง class extend superclass ตัวนั้น จากนั้น Pull Up field และ method ที่ใช้ร่วมกันทีละตัว แล้วรัน test หลังทุกครั้ง
  4. สำหรับ interface: ประกาศ method signature ที่ใช้ร่วมกัน แล้วทำให้แต่ละ class ประกาศว่า implement interface นั้น (อัตโนมัติด้วย structural typing ของ Go; เป็น impl ที่ชัดเจนใน Rust)
  5. ชี้ code ฝั่ง client ไปยัง abstraction ใหม่ — รับ type ที่เป็น interface หรือ superclass ทุกที่ที่ไม่จำเป็นต้องใช้ตัวที่เป็นรูปธรรม
  6. รัน test หลังการย้ายแต่ละครั้ง

ทำ Extract Superclass เมื่อสอง class ใช้ implementation จริง ๆ ร่วมกันซึ่งคุณเบื่อที่จะดูแลซ้ำสองรอบ ทำ Extract Interface เมื่อ type หลายตัวต้องถูกปฏิบัติอย่างเป็นเอกภาพโดย client แต่ไม่มีข้อมูลร่วมกัน — เช่น อะไรก็ตามที่ “billable”, “serializable”, หรือ “comparable”

ข้อแลกเปลี่ยนคือ superclass มัด subclass ทุกตัวเข้าด้วยกัน และในภาษาที่เป็น single-inheritance ก็ใช้ได้ครั้งเดียวจบ จึงต้องใช้อย่างระมัดระวัง ส่วน interface ไม่มีต้นทุนเชิงโครงสร้าง แต่เพิ่มชั้นการอ้อมเข้ามา ใน Go และ Rust นั้น Extract Interface (interface หรือ trait) คือเครื่องมือ ตั้งต้น ส่วน Extract Superclass ถูกแทนด้วยการประกอบ struct ที่ใช้ร่วมกัน เพราะไม่มี parent class ให้ extract

ใช้ Extract Superclass / Interface เมื่อหลีกเลี่ยงเมื่อ
หลาย class มี method/field ที่ซ้ำกันและควรใช้ร่วมกันclass มีความซ้ำซ้อนน้อยมาก — ยังเร็วเกินไป
ต้องการ polymorphism โดยไม่ต้อง duplicate codesuperclass จะกลาย God Class ที่รวมทุกอย่าง
client code ควร depend on abstraction ไม่ใช่ implementationinheritance ไม่เหมาะ — ใช้ composition แทน

⚠️ ไม่ควร Extract Superclass / Interface เมื่อ:

  • class ที่คล้ายกันมีความแตกต่างที่ลึกกว่าความเหมือน
  • สร้าง abstraction ก่อนที่จะมี 3 concrete class — “Rule of Three”
  • inheritance จะ couple classes ที่ควรเป็นอิสระจากกัน
เมื่อไรคุณควรเลือก Extract Interface มากกว่า Extract Superclass?
ใน Go "extract interface" ถูกแสดงออกอย่างไร?
ใน Rust อะไรทำหน้าที่เป็น interface ที่ถูก extract?
อะไรคือต้นทุนสำคัญของการ extract superclass ในภาษาที่มี single-inheritance?