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 ที่เป็นรูปธรรม
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”สอง 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 shapeclass 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 superclassinterface 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; }}from abc import ABC, abstractmethod
# Before — two classes independently grew the same shapeclass Department: def __init__(self, name, staff_count): self.name = name self.staff_count = staff_count
def monthly_charge(self): return self.staff_count * 100
class Employee: def __init__(self, name, salary): self.name = name self.salary = salary
def monthly_charge(self): return self.salary / 12
# After — abstract base holds the shared field and the shared protocolclass Party(ABC): def __init__(self, name): self.name = name
@abstractmethod def monthly_charge(self): ...
class Department(Party): def __init__(self, name, staff_count): super().__init__(name) self.staff_count = staff_count
def monthly_charge(self): return self.staff_count * 100
class Employee(Party): def __init__(self, name, salary): super().__init__(name) self.salary = salary
def monthly_charge(self): return self.salary / 12// Go has no superclass. "Extract interface" is natural and idiomatic:// declare the shared protocol as an interface that both structs satisfy// structurally. "Extract superclass" becomes embedding a small shared// struct that holds the common fields.
// Shared protocol — extracted as an interfacetype Billable interface { MonthlyCharge() float64}
// Shared data — extracted into an embeddable base structtype Party struct { Name string}
type Department struct { Party StaffCount int}
func (d Department) MonthlyCharge() float64 { return float64(d.StaffCount) * 100}
type Employee struct { Party Salary float64}
func (e Employee) MonthlyCharge() float64 { return e.Salary / 12}
// Any Department or Employee value can now be used as a Billable.func totalCharge(items []Billable) float64 { total := 0.0 for _, it := range items { total += it.MonthlyCharge() } return total}// Rust has no superclass. "Extract interface" maps directly to extracting// a trait that both types implement. "Extract superclass" becomes sharing// a common struct by composition (a held `party` field).
struct Party { name: String,}
// Extracted protocol — a traittrait Billable { fn monthly_charge(&self) -> f64;}
struct Department { party: Party, staff_count: u32,}impl Billable for Department { fn monthly_charge(&self) -> f64 { self.staff_count as f64 * 100.0 }}
struct Employee { party: Party, salary: f64,}impl Billable for Employee { fn monthly_charge(&self) -> f64 { self.salary / 12.0 }}
// Callers depend on the trait, not the concrete type.fn total_charge(items: &[&dyn Billable]) -> f64 { items.iter().map(|it| it.monthly_charge()).sum()}classDiagram
class Party {
+name string
+monthlyCharge() number
}
class Department
class Employee
Party <|-- Department
Party <|-- Employee
class Billable {
<<interface>>
+monthlyCharge() number
}
Billable <|.. Party กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- ยืนยันส่วนที่ทับซ้อน ลิสต์ field และ method ที่สอง class ใช้ร่วมกัน ถ้าใช้ทั้ง state และ behavior ร่วมกัน ให้เอนไปทาง superclass หากใช้เพียง signature ร่วมกัน ให้เอนไปทาง interface หรือ trait
- สร้าง superclass เปล่า (หรือ interface / trait) ใน Go และ Rust ให้สร้าง struct ที่ใช้ร่วมกันและ interface หรือ trait
- สำหรับ superclass ให้ทั้งสอง class extend superclass ตัวนั้น จากนั้น Pull Up field และ method ที่ใช้ร่วมกันทีละตัว แล้วรัน test หลังทุกครั้ง
- สำหรับ interface: ประกาศ method signature ที่ใช้ร่วมกัน แล้วทำให้แต่ละ class ประกาศว่า implement interface นั้น (อัตโนมัติด้วย structural typing ของ Go; เป็น
implที่ชัดเจนใน Rust) - ชี้ code ฝั่ง client ไปยัง abstraction ใหม่ — รับ type ที่เป็น interface หรือ superclass ทุกที่ที่ไม่จำเป็นต้องใช้ตัวที่เป็นรูปธรรม
- รัน 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 code | superclass จะกลาย God Class ที่รวมทุกอย่าง |
| client code ควร depend on abstraction ไม่ใช่ implementation | inheritance ไม่เหมาะ — ใช้ composition แทน |
⚠️ ไม่ควร Extract Superclass / Interface เมื่อ:
- class ที่คล้ายกันมีความแตกต่างที่ลึกกว่าความเหมือน
- สร้าง abstraction ก่อนที่จะมี 3 concrete class — “Rule of Three”
- inheritance จะ couple classes ที่ควรเป็นอิสระจากกัน