Pull Up & Push Down
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Pull Up หยิบ method หรือ field ที่เหมือนกันใน subclass ตั้งแต่สองตัวขึ้นไป ยกขึ้นไปไว้บน superclass ร่วม logic จะได้อยู่ที่เดียวเป๊ะ ๆ ส่วน Push Down ทำกลับกัน คือสมาชิกที่นั่งอยู่บน superclass แต่มีแค่บาง subclass ใช้จริง ให้ย้ายลงไปอยู่เฉพาะ subclass เหล่านั้น superclass จะได้เลิกสัญญาว่ามี behavior ที่ลูกส่วนใหญ่ไม่ต้องการ
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”Pull Up คือยาแก้ Duplicated Code ที่กระจายอยู่ในบรรดา subclass พี่น้อง เช่น body ของ annualCost ชุดเดียวกันถูก copy-paste ลงไปทั้งใน Salaried และ Contractor แล้วค่อย ๆ เคลื่อนห่างจากกันอย่างแนบเนียนเมื่อเวลาผ่านไป ส่วน Push Down จัดการ smell ฝั่งตรงข้าม คือ superclass ที่รกด้วยสมาชิกซึ่งกลายเป็น Refused Bequest เช่น subclass ครึ่งหนึ่ง inherit field commission มาโดยไม่เคยแตะเลย field นั้นจึงทำให้ทุกคนที่อ่าน superclass เข้าใจผิด ทั้งสองกรณีคือลำดับชั้นที่ประกาศสิ่งที่ไม่ตรงกับความจริง
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”subclass พนักงานสองตัวมีการคำนวณ annualCost เหมือนกันเป๊ะ เราจึงดึงขึ้นไปไว้ข้างบน ส่วน field commission ที่มีแค่ contractor ใช้ ก็ผลักลงไปด้วยหลักคิดเดียวกัน
// Before — annualCost duplicated in both subclassesabstract class Employee { constructor(public monthlyPay: number) {}}
class Salaried extends Employee { annualCost(): number { return this.monthlyPay * 12; }}
class Contractor extends Employee { annualCost(): number { return this.monthlyPay * 12; }}
// After — pulled up once; commission pushed down to Contractor onlyabstract class Employee { constructor(public monthlyPay: number) {} annualCost(): number { return this.monthlyPay * 12; }}
class Salaried extends Employee {}
class Contractor extends Employee { constructor(monthlyPay: number, public commission: number) { super(monthlyPay); } annualCost(): number { return super.annualCost() + this.commission; }}# Before — annual_cost duplicated in both subclassesclass Employee: def __init__(self, monthly_pay): self.monthly_pay = monthly_pay
class Salaried(Employee): def annual_cost(self): return self.monthly_pay * 12
class Contractor(Employee): def annual_cost(self): return self.monthly_pay * 12
# After — pulled up once; commission pushed down to Contractor onlyclass Employee: def __init__(self, monthly_pay): self.monthly_pay = monthly_pay
def annual_cost(self): return self.monthly_pay * 12
class Salaried(Employee): pass
class Contractor(Employee): def __init__(self, monthly_pay, commission): super().__init__(monthly_pay) self.commission = commission
def annual_cost(self): return super().annual_cost() + self.commission// Go has no inheritance — the shared method lives on an embedded base// struct, and both employee kinds embed it. "Pull up" becomes "move the// method onto the embedded type"; "push down" becomes "add a field only// to the kind that needs it".
// Before — AnnualCost duplicated on each kindtype Salaried struct{ MonthlyPay float64 }
func (s Salaried) AnnualCost() float64 { return s.MonthlyPay * 12 }
type Contractor struct{ MonthlyPay float64 }
func (c Contractor) AnnualCost() float64 { return c.MonthlyPay * 12 }
// After — shared method pulled up onto an embedded basetype Employee struct{ MonthlyPay float64 }
func (e Employee) AnnualCost() float64 { return e.MonthlyPay * 12 }
type Salaried struct{ Employee }
type Contractor struct { Employee Commission float64 // pushed down: only contractors carry it}
func (c Contractor) AnnualCost() float64 { return c.Employee.AnnualCost() + c.Commission}// Rust has no inheritance — shared behaviour lives in a default trait// method, and each kind holds the common data by composition. "Pull up"// becomes "give the trait a default method"; "push down" becomes "store a// field only on the kind that needs it".
struct Base { monthly_pay: f64,}
trait Employee { fn base(&self) -> &Base; // Pulled up: one default implementation shared by all kinds. fn annual_cost(&self) -> f64 { self.base().monthly_pay * 12.0 }}
struct Salaried { base: Base,}impl Employee for Salaried { fn base(&self) -> &Base { &self.base }}
struct Contractor { base: Base, commission: f64, // pushed down: only contractors carry it}impl Employee for Contractor { fn base(&self) -> &Base { &self.base } fn annual_cost(&self) -> f64 { self.base.monthly_pay * 12.0 + self.commission }}classDiagram
class Employee {
+annualCost() number
}
class Salaried
class Contractor
Employee <|-- Salaried
Employee <|-- Contractor
note for Employee "annualCost pulled up here once" กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- ยืนยันว่าสมาชิกเหมือนกันจริง (สำหรับ Pull Up) หรือมีแค่บาง subclass ใช้จริง (สำหรับ Push Down) ถ้าสองสำเนาต่างกันนิดหน่อย ให้ค่อย ๆ refactor ย่อย ๆ จนตรงกันก่อน
- สำหรับ Pull Up: สร้างสมาชิกบน superclass (หรือ embedded base / trait default ใน Go และ Rust) คัดลอกบอดี้ของ subclass หนึ่งตัวลงไปในนั้น
- ลบสมาชิกออกจากแต่ละ subclass ทีละตัว แล้วรัน test หลังการลบแต่ละครั้ง
- สำหรับ Push Down ให้คัดลอกสมาชิกลงไปในทุก subclass ที่ต้องใช้ แล้วลบออกจาก superclass
- subclass ตัวไหนที่ยังต้องการ behavior เฉพาะของตัวเอง ให้เรียกขึ้นไปหาเวอร์ชันที่ใช้ร่วมกันแล้วต่อยอดจากตรงนั้น
- รัน test behavior ต้องไม่เปลี่ยนในทุกขั้นตอน
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”ทำ Pull Up ทุกครั้งที่เจอ method หรือ field เดียวกันซ้ำอยู่ใน subclass พี่น้อง เพราะเป็นหนึ่งในวิธีลบ code ที่ฟินที่สุด และทำ Push Down ทุกครั้งที่ superclass ประกาศสมาชิกที่ subclass ส่วนใหญ่ไม่สนใจหรือ override ทิ้ง interface ของ superclass ควรสะท้อนเฉพาะสิ่งที่ลูก ทุกตัว มีร่วมกันจริง ๆ
ข้อแลกเปลี่ยนคือ coupling เพราะการดึงขึ้นไปมัด subclass ทุกตัวไว้กับนิยามชุดเดียวกัน วันที่ subclass เริ่มต่างกัน คุณก็ต้องผลักกลับลงไปหรือ override เอา ส่วนใน Go และ Rust เจตนาเดียวกันนี้แสดงผ่าน embedding และ trait default แทน inheritance ซึ่งเก็บ logic ที่ใช้ร่วมกันไว้ที่เดียวได้ โดยไม่ต้องประกาศความสัมพันธ์ “is-a” ที่ไม่จริง
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Pull Up / Push Down เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| method เดียวกันปรากฏในหลาย subclass ที่ควร share | method ที่ขึ้นไปที่ superclass ไม่ได้ถูกใช้โดย subclass ทั้งหมด |
| field ใช้โดย subclass ทุกตัวและควรอยู่ที่เดียว | push down method ที่ยังคงถูกเรียกจาก superclass |
| behavior เดียวกันต้องการ single point of maintenance | abstraction ยังไม่ชัดเจนพอที่จะดึงขึ้น |
⚠️ ไม่ควร Pull Up เมื่อ:
- method/field ที่จะดึงขึ้นไม่ได้ใช้โดย subclass ทุกตัว
- การดึงขึ้นจะทำให้ superclass รู้เรื่อง subclass มากเกินไป
- method มี logic ที่ต่างกันใน subclass แม้จะชื่อเดียวกัน