Collapse Hierarchy
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Collapse Hierarchy รวม superclass กับ subclass ที่ไม่ต่างกันมากพอจะมีอยู่เป็นสอง type แยกกันอีกต่อไป สมาชิกของตัวหนึ่งถูกพับเข้าไปในอีกตัว class ที่ตอนนี้ซ้ำซ้อนถูกลบทิ้ง และผู้อ่านทุกคนก็รอดพ้นจากชั้นของการอ้อมที่ไม่ได้ให้อะไรพวกเขาเลย
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”ลำดับชั้นสึกกร่อนได้ การ refactor ดึงสมาชิกขึ้นบ้างผลักลงบ้าง จนสุดท้าย subclass แทบไม่เหลืออะไรเพิ่ม อาจมี field จิ๊บจ๊อยตัวเดียว หรือมี override ที่ตอนนี้เหมือน superclass เป๊ะ นี่คือ Lazy Element (Fowler บทที่ 3; ฉบับพิมพ์ครั้งแรกเรียก 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 เดียวเข้าไป