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

Pull Up Constructor Body

Pull Up Constructor Body คือ Pull Up Method ที่นำมาใช้กับ method เพียงตัวเดียวที่มีกฎพิเศษ: constructor เมื่อ subclass พี่น้องหลายตัวเริ่ม constructor ด้วย code ตั้งค่า field ที่เหมือนกัน การ initialize ที่ใช้ร่วมกันนั้นจะย้ายเข้าไปอยู่ใน constructor ของ superclass จากนั้น constructor ของแต่ละ subclass ก็ เรียกขึ้นไป หาพ่อแม่สำหรับส่วนที่เหมือนกัน และเก็บเฉพาะบรรทัดที่เป็นเอกลักษณ์ของตัวเองไว้

เปิด 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 setup
class 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 fields
class 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);
}
}
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)
การตั้งค่า field ที่เหมือนกันย้ายไปอยู่ใน constructor ของ Employee แล้ว subclass เรียกขึ้นไป
  1. เพิ่ม constructor บน superclass ถ้ายังไม่มี แล้วใส่ parameter สำหรับ field ที่ subclass ตั้งค่าเหมือนกัน
  2. ย้ายบรรทัดการ assign ที่ใช้ร่วมกันเข้าไปใน constructor ของ superclass นั้น
  3. ใน constructor ของแต่ละ subclass ให้แทนบรรทัดเหล่านั้นด้วยการเรียกขึ้นไปหาพ่อแม่ — super(...) ใน TypeScript/Python — โดยส่ง argument ที่ใช้ร่วมกันไป
  4. ตรวจให้แน่ใจว่าการเรียกขึ้นไปเกิด ก่อน ก่อนที่ subclass จะแตะ field ใด ๆ เพราะการตั้งค่า field ขึ้นกับว่าฐานถูก initialize แล้ว
  5. รัน test object ที่ถูกสร้างต้องถือค่า field เดียวกับก่อนหน้า
  6. ระวังอันตรายเรื่องลำดับ: ถ้า 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 constructorsuperclass constructor จะซับซ้อนเกินไปจาก variation
จะ add validation ที่ทุก subclass ต้องผ่านsubclass ต้องการ call super() ในลำดับที่ต่างกัน

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

  • initialization logic ต่างกันในแต่ละ subclass อย่างมีนัยสำคัญ
  • การ pull up จะทำให้ base class constructor มี parameter มากเกินไป
  • ภาษากำหนด constraint ที่ทำให้ constructor sharing ซับซ้อน
เมื่อไรที่ Pull Up Constructor Body เป็นการเคลื่อนไหวที่ถูกต้อง?
ใน TypeScript และ Python constructor ของ subclass นำการตั้งค่าที่ถูกดึงขึ้นมาใช้ซ้ำอย่างไร?
กฎเรื่องลำดับใดที่คุณต้องเคารพเมื่อดึง constructor body ขึ้น?
Go และ Rust แสดงเจตนานี้โดยไม่มี inheritance อย่างไร?