Pull Up Constructor Body
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Pull Up Constructor Body คือ Pull Up Method ที่นำมาใช้กับ method เพียงตัวเดียวที่มีกฎพิเศษ: constructor เมื่อ subclass พี่น้องหลายตัวเริ่ม constructor ด้วย code ตั้งค่า field ที่เหมือนกัน การ initialize ที่ใช้ร่วมกันนั้นจะย้ายเข้าไปอยู่ใน constructor ของ superclass จากนั้น constructor ของแต่ละ subclass ก็ เรียกขึ้นไป หาพ่อแม่สำหรับส่วนที่เหมือนกัน และเก็บเฉพาะบรรทัดที่เป็นเอกลักษณ์ของตัวเองไว้
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”เปิด subclass พี่น้องสองตัวขึ้นมาดู แล้วพบว่า constructor เริ่มต้นเหมือนกันเป๊ะ คือ this.name = name; this.id = id; this.hiredAt = now(); ซ้ำกันคำต่อคำ บรรทัดต่อบรรทัด นั่นคือ Duplicated Code ในจุดที่รับน้ำหนักมากที่สุด นั่นคือการตั้งค่า object พรุ่งนี้พอเพิ่ม field ให้แนวคิดฐาน คุณก็ต้องจำเดินสายผ่าน constructor ของ subclass ทุกตัว ในเมื่อการตั้งค่าชุดนี้เป็นของแนวคิดที่ใช้ร่วมกัน ก็ควรไปอยู่ใน constructor ที่ใช้ร่วมกัน
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”Manager และ Engineer ต่างก็ initialize name และ id ด้วยตัวเอง หลังจากนั้น constructor ของ superclass เป็นเจ้าของส่วนนั้น และแต่ละ subclass เรียกขึ้นไปก่อนจะทำส่วนของตัวเอง
// Before — each subclass constructor repeats the common setupclass Employee { name!: string; id!: string;}
class Manager extends Employee { reports: string[]; constructor(name: string, id: string, reports: string[]) { super(); this.name = name; // duplicated this.id = id; // duplicated this.reports = reports; }}
class Engineer extends Employee { stack: string; constructor(name: string, id: string, stack: string) { super(); this.name = name; // duplicated this.id = id; // duplicated this.stack = stack; }}
// After — the superclass constructor owns the common fieldsclass Employee { constructor(public name: string, public id: string) {}}
class Manager extends Employee { constructor(name: string, id: string, public reports: string[]) { super(name, id); }}
class Engineer extends Employee { constructor(name: string, id: string, public stack: string) { super(name, id); }}# Before — each subclass __init__ repeats the common setupclass Employee: pass
class Manager(Employee): def __init__(self, name, id, reports): self.name = name # duplicated self.id = id # duplicated self.reports = reports
class Engineer(Employee): def __init__(self, name, id, stack): self.name = name # duplicated self.id = id # duplicated self.stack = stack
# After — the superclass __init__ owns the common fieldsclass Employee: def __init__(self, name, id): self.name = name self.id = id
class Manager(Employee): def __init__(self, name, id, reports): super().__init__(name, id) self.reports = reports
class Engineer(Employee): def __init__(self, name, id, stack): super().__init__(name, id) self.stack = stack// Go has no constructors or inheritance. The equivalent move is to embed a// shared Employee struct and give it a constructor function the "subtype"// constructors call up to — so the common field setup lives in one place.
type Employee struct { Name string ID string}
func newEmployee(name, id string) Employee { return Employee{Name: name, ID: id} // common setup, one place}
type Manager struct { Employee // embedded Reports []string}
func NewManager(name, id string, reports []string) Manager { return Manager{Employee: newEmployee(name, id), Reports: reports}}
type Engineer struct { Employee // embedded Stack string}
func NewEngineer(name, id, stack string) Engineer { return Engineer{Employee: newEmployee(name, id), Stack: stack}}// Rust has no inheritance or constructors. The equivalent is a shared// Employee struct with a constructor function, held as a field by each// "subtype", whose own constructors call the shared one for common setup.
struct Employee { name: String, id: String,}
impl Employee { fn new(name: String, id: String) -> Self { Employee { name, id } // common setup, one place }}
struct Manager { base: Employee, reports: Vec<String>,}
impl Manager { fn new(name: String, id: String, reports: Vec<String>) -> Self { Manager { base: Employee::new(name, id), reports } }}
struct Engineer { base: Employee, stack: String,}
impl Engineer { fn new(name: String, id: String, stack: String) -> Self { Engineer { base: Employee::new(name, id), stack } }}classDiagram
class Employee {
+name: string
+id: string
+Employee(name, id)
}
class Manager {
+Manager(name, id, reports)
}
class Engineer {
+Engineer(name, id, stack)
}
Employee <|-- Manager : super(name, id)
Employee <|-- Engineer : super(name, id) กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- เพิ่ม constructor บน superclass ถ้ายังไม่มี แล้วใส่ parameter สำหรับ field ที่ subclass ตั้งค่าเหมือนกัน
- ย้ายบรรทัดการ assign ที่ใช้ร่วมกันเข้าไปใน constructor ของ superclass นั้น
- ใน constructor ของแต่ละ subclass ให้แทนบรรทัดเหล่านั้นด้วยการเรียกขึ้นไปหาพ่อแม่ —
super(...)ใน TypeScript/Python — โดยส่ง argument ที่ใช้ร่วมกันไป - ตรวจให้แน่ใจว่าการเรียกขึ้นไปเกิด ก่อน ก่อนที่ subclass จะแตะ field ใด ๆ เพราะการตั้งค่า field ขึ้นกับว่าฐานถูก initialize แล้ว
- รัน test object ที่ถูกสร้างต้องถือค่า field เดียวกับก่อนหน้า
- ระวังอันตรายเรื่องลำดับ: ถ้า subclass รัน code ที่ใช้ร่วมกันใน จุดที่ต่างกัน เมื่อเทียบกับ logic ของตัวเอง ให้ปรับให้สอดคล้องก่อนจะดึงขึ้น
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”หยิบท่านี้มาใช้ทุกครั้งที่ constructor ของ subclass พี่น้องมีส่วนหัวของการ initialize เหมือนกัน พอรวบไว้ที่เดียว field ใหม่ที่ใช้ร่วมกันก็จะเดินสายที่เดียวจบ และ constructor ของ subclass ก็หดเหลือเฉพาะสิ่งที่ทำให้แต่ละ type ต่างกันจริง ๆ
ข้อควรระวังคือกฎที่เข้มงวดของ constructor การเรียกขึ้นไปหา superclass ต้องรันก่อนที่ subtype จะใช้ field ที่ inherit มา และถ้า subclass พี่น้องเอาการตั้งค่าส่วนร่วมกับส่วนเฉพาะมาสลับลำดับกัน คุณต้องแกะให้เรียบร้อยก่อน บางครั้งทางที่ปลอดภัยกว่าคือ extract งานส่วนร่วมออกเป็น helper แล้วให้ constructor เรียก แทนที่จะยกเข้าไปใน constructor ของ superclass ตรง ๆ ส่วนใน Go และ Rust ไม่มี constructor ให้ดึงขึ้นอยู่แล้ว คุณสื่อเจตนาเดียวกันด้วย constructor function ที่ใช้ร่วมกัน บวกกับ embedding (Go) หรือ struct ฐานที่ถือไว้ (Rust) การตั้งค่าส่วนร่วมจึงยังอยู่ที่เดียวเป๊ะ ๆ เหมือนกัน
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Pull Up Constructor Body เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| subclass constructor มี initialization logic เหมือนกัน | แต่ละ subclass ต้องการ init sequence ที่ต่างกัน |
| ต้องการ enforce invariant ใน base class constructor | superclass constructor จะซับซ้อนเกินไปจาก variation |
| จะ add validation ที่ทุก subclass ต้องผ่าน | subclass ต้องการ call super() ในลำดับที่ต่างกัน |
⚠️ ไม่ควร Pull Up Constructor Body เมื่อ:
- initialization logic ต่างกันในแต่ละ subclass อย่างมีนัยสำคัญ
- การ pull up จะทำให้ base class constructor มี parameter มากเกินไป
- ภาษากำหนด constraint ที่ทำให้ constructor sharing ซับซ้อน