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