ข้ามไปยังเนื้อหา

Pull Up & Push Down

Pull Up หยิบ method หรือ field ที่เหมือนกันใน subclass ตั้งแต่สองตัวขึ้นไป ยกขึ้นไปไว้บน superclass ร่วม logic จะได้อยู่ที่เดียวเป๊ะ ๆ ส่วน Push Down ทำกลับกัน คือสมาชิกที่นั่งอยู่บน superclass แต่มีแค่บาง subclass ใช้จริง ให้ย้ายลงไปอยู่เฉพาะ subclass เหล่านั้น superclass จะได้เลิกสัญญาว่ามี behavior ที่ลูกส่วนใหญ่ไม่ต้องการ

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 subclasses
abstract 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 only
abstract 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;
}
}
classDiagram
  class Employee {
    +annualCost() number
  }
  class Salaried
  class Contractor
  Employee <|-- Salaried
  Employee <|-- Contractor
  note for Employee "annualCost pulled up here once"
annualCost ถูกยกขึ้นไปไว้ที่พ่อแม่ Employee ที่ใช้ร่วมกัน
  1. ยืนยันว่าสมาชิกเหมือนกันจริง (สำหรับ Pull Up) หรือมีแค่บาง subclass ใช้จริง (สำหรับ Push Down) ถ้าสองสำเนาต่างกันนิดหน่อย ให้ค่อย ๆ refactor ย่อย ๆ จนตรงกันก่อน
  2. สำหรับ Pull Up: สร้างสมาชิกบน superclass (หรือ embedded base / trait default ใน Go และ Rust) คัดลอกบอดี้ของ subclass หนึ่งตัวลงไปในนั้น
  3. ลบสมาชิกออกจากแต่ละ subclass ทีละตัว แล้วรัน test หลังการลบแต่ละครั้ง
  4. สำหรับ Push Down ให้คัดลอกสมาชิกลงไปในทุก subclass ที่ต้องใช้ แล้วลบออกจาก superclass
  5. subclass ตัวไหนที่ยังต้องการ behavior เฉพาะของตัวเอง ให้เรียกขึ้นไปหาเวอร์ชันที่ใช้ร่วมกันแล้วต่อยอดจากตรงนั้น
  6. รัน 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 ที่ควร sharemethod ที่ขึ้นไปที่ superclass ไม่ได้ถูกใช้โดย subclass ทั้งหมด
field ใช้โดย subclass ทุกตัวและควรอยู่ที่เดียวpush down method ที่ยังคงถูกเรียกจาก superclass
behavior เดียวกันต้องการ single point of maintenanceabstraction ยังไม่ชัดเจนพอที่จะดึงขึ้น

⚠️ ไม่ควร Pull Up เมื่อ:

  • method/field ที่จะดึงขึ้นไม่ได้ใช้โดย subclass ทุกตัว
  • การดึงขึ้นจะทำให้ superclass รู้เรื่อง subclass มากเกินไป
  • method มี logic ที่ต่างกันใน subclass แม้จะชื่อเดียวกัน
เมื่อไรคือจังหวะที่เหมาะจะทำ Pull Up?
Push Down จัดการกับกลิ่นไม่ดีใด?
Go และ Rust แสดง "pull up" โดยไม่มี inheritance ได้อย่างไร?
อะไรคือความเสี่ยงของการดึงสมาชิกสองตัวที่เพียงเกือบจะเหมือนกันขึ้นไป?