Collapse Hierarchy
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Collapse Hierarchy รวม superclass กับ subclass ที่ไม่ต่างกันมากพอจะมีอยู่เป็นสอง type แยกกันอีกต่อไป สมาชิกของตัวหนึ่งถูกพับเข้าไปในอีกตัว class ที่ตอนนี้ซ้ำซ้อนถูกลบทิ้ง และผู้อ่านทุกคนก็รอดพ้นจากชั้นของการอ้อมที่ไม่ได้ให้อะไรพวกเขาเลย
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”ลำดับชั้นสึกกร่อนได้ การ refactor ดึงสมาชิกขึ้นบ้างผลักลงบ้าง จนสุดท้าย subclass แทบไม่เหลืออะไรเพิ่ม อาจมี field จิ๊บจ๊อยตัวเดียว หรือมี override ที่ตอนนี้เหมือน superclass เป๊ะ นี่คือ Lazy Class ในคราบ subclass คือมี type เพิ่ม ไฟล์เพิ่ม และการกระโดดเพิ่มให้ผู้อ่านทุกคน ทั้งหมดเพื่อแสดงความต่างที่หายไปเงียบ ๆ นานแล้ว เมื่อ subclass กับ superclass กลายเป็นฝาแฝดกัน ท่าที่ซื่อตรงที่สุดคือยุบรวมเป็นตัวเดียว
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”subclass Salesperson เคยต่อยอด Employee อย่างมีความหมาย แต่ refactoring หลายรอบทำให้เหลือ field เดียว ซึ่ง Employee ก็ถือเองได้ดีพอกัน เราจึงยุบรวม
// Before — Salesperson adds almost nothing over Employeeclass Employee { constructor(public name: string, public monthlyPay: number) {} annualCost(): number { return this.monthlyPay * 12; }}
class Salesperson extends Employee { constructor(name: string, monthlyPay: number, public region: string) { super(name, monthlyPay); }}
// After — one Employee carries the region field directlyclass Employee { constructor( public name: string, public monthlyPay: number, public region: string = "", ) {} annualCost(): number { return this.monthlyPay * 12; }}# Before — Salesperson adds almost nothing over Employeeclass Employee: def __init__(self, name, monthly_pay): self.name = name self.monthly_pay = monthly_pay
def annual_cost(self): return self.monthly_pay * 12
class Salesperson(Employee): def __init__(self, name, monthly_pay, region): super().__init__(name, monthly_pay) self.region = region
# After — one Employee carries the region field directlyclass Employee: def __init__(self, name, monthly_pay, region=""): self.name = name self.monthly_pay = monthly_pay self.region = region
def annual_cost(self): return self.monthly_pay * 12// Go has no class hierarchy. The equivalent "hierarchy" is a thin struct// that embeds a base just to add one field. Collapsing means folding that// field into the base struct and deleting the wrapper.
// Before — Salesperson embeds Employee only to add Regiontype Employee struct { Name string MonthlyPay float64}
func (e Employee) AnnualCost() float64 { return e.MonthlyPay * 12 }
type Salesperson struct { Employee Region string}
// After — one Employee struct carries Region directlytype Employee struct { Name string MonthlyPay float64 Region string}
func (e Employee) AnnualCost() float64 { return e.MonthlyPay * 12 }// Rust has no inheritance. The equivalent is a wrapper struct that holds// a base struct just to bolt on one field. Collapsing folds the field into// the base and removes the wrapper.
// Before — Salesperson wraps Employee only to add a regionstruct Employee { name: String, monthly_pay: f64,}
impl Employee { fn annual_cost(&self) -> f64 { self.monthly_pay * 12.0 }}
struct Salesperson { employee: Employee, region: String,}
// After — one Employee struct carries region directlystruct Employee { name: String, monthly_pay: f64, region: String,}
impl Employee { fn annual_cost(&self) -> f64 { self.monthly_pay * 12.0 }}classDiagram
class Employee {
+name string
+grade number
+annualCost() number
}
note for Employee "Salesperson merged back in — one type now" กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- ตัดสินใจว่า class ไหนจะอยู่รอด ปกติ superclass จะดูดซับ subclass เข้าไป แต่ถ้าชื่อ subclass เข้ากับแนวคิดที่รวมแล้วมากกว่า ก็เก็บชื่อนั้นไว้แทน
- ใช้ Pull Up และ Push Down เพื่อย้าย field และ method ทั้งหมดเข้าไปใน class เดียวที่จะอยู่รอด จน class ที่กำลังจะลบเหลือว่างเปล่า
- ชี้ทุกการอ้างอิงและการเรียก constructor ใหม่จาก class ที่ถูกลบไปยัง class ที่อยู่รอด แล้วรัน test
- ลบ class ที่ตอนนี้ว่างเปล่าทิ้ง (และเอา embedding/wrapper ออกใน Go และ Rust)
- รัน test อีกครั้งเป็นครั้งสุดท้าย โปรแกรมควรทำงานเหมือนเดิมเป๊ะ ๆ โดยมี type ให้นำทางน้อยลงหนึ่งตัว
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”ยุบลำดับชั้นเมื่อ subclass เพิ่มอะไรน้อยมากจน type ที่เกินมากลายเป็นภาระล้วน ๆ คือไม่มี behavior ที่มีความหมาย ไม่มีความต่างจริง เหลือแต่การอ้อม พอถอดออกแล้ว code สั้นลงและแบนลง ผู้อ่านก็เลิกต้องกระโดดไปมาระหว่าง superclass กับ subclass เพียงเพื่อประกอบแนวคิดเดียว
ข้อแลกเปลี่ยนคือการย้อนกลับได้: ถ้าความแตกต่างกลับมาในภายหลัง คุณก็จะ re-extract subclass (หรือ interface) ขึ้นมาใหม่ ซึ่งไม่เป็นไร — Collapse Hierarchy คือสิ่งตรงข้ามตามธรรมชาติของ Extract Superclass และการ refactoring ก็ตั้งใจให้ไหลได้ทั้งสองทางเมื่อความเข้าใจของคุณเปลี่ยน ใน Go และ Rust ไม่เคยมีลำดับชั้นของ class ตั้งแต่แรก ดังนั้น “collapse” จึงเป็นเพียงการลบ wrapper struct ที่ไม่จำเป็นและฝัง field เดียวเข้าไป
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Collapse Hierarchy เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| subclass ไม่มี behavior ของตัวเองเลย — เหมือน parent ทั้งหมด | subclass มี behavior เฉพาะที่ยังใช้อยู่ |
| subclass มีอยู่เพราะ refactoring เก่าที่ไม่ถูก clean up | การ collapse จะทำให้ class ที่เหลือใหญ่เกินไป |
| inheritance tree ลึกเกินไปจน navigate ยาก | มี test ที่ test behavior ของ subclass โดยเฉพาะ |
⚠️ ไม่ควร Collapse Hierarchy เมื่อ:
- subclass นั้นอาจถูก extend ในอนาคต — ลบไปจะต้องสร้างใหม่
- subclass มีชื่อที่สื่อ domain concept ชัดเจน แม้ body จะเล็ก
- ยังมี test ที่ test interface ของ subclass อยู่ — collapse จะ break test