Replace Constructor with Factory Function
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”เปลี่ยนจากการเรียก constructor ตรง ๆ มาเรียก factory function แทน ตัว constructor มีข้อจำกัดสองข้อ คือต้องใช้ชื่อเดียวกับ type และต้องคืน instance ของ type นั้นเป๊ะ ๆ เสมอ ส่วน factory function เป็นอิสระจากทั้งสองข้อ คือตั้งชื่อให้สื่อความหมายได้ เลือกได้ว่าจะคืน subtype ตัวไหน คืน instance ที่ cache ไว้ก็ได้ หรือจะ validate ก่อนสร้างก็ได้ caller แค่บอก factory ว่าต้องการอะไร แล้ว factory เป็นคนตัดสินใจว่าจะสร้างอย่างไร
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”constructor ดิบ ๆ ทำให้การตัดสินใจเรื่องการสร้างรั่วไปถึง caller ทุกตัว ลองดู new Employee("Sara", "manager") แล้ว caller ต้องจำเองว่า string ตัวไหนแปลว่าอะไร ที่หนักกว่านั้นคือเมื่อ type มีหลายแบบ เช่น manager เป็นพนักงานคนละชนิดกับ engineer constructor เปล่า ๆ คืน subtype ที่ถูกต้องไม่ได้ caller จึงต้องแตก branch ตาม type code เอง แถมชื่อ constructor ยังถูกตรึงไว้กับ class จะเรียกว่า hire, fromJson หรือ default ก็ไม่ได้ ทุกข้อชี้ตรงกันว่า constructor แคบเกินไปสำหรับงานนี้
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”type พนักงานที่ constructor รับ stringtype หลังจากนั้น function factory เลือก subtype ที่ถูกต้องและอ่านได้ชัดเจน
// Beforeclass Employee { constructor(public name: string, public type: string) {} monthlyBonus(): number { return this.type === "manager" ? 1000 : 200; }}
const e = new Employee("Sara", "manager");
// Afterabstract class Employee { constructor(public name: string) {} abstract monthlyBonus(): number;}
class Manager extends Employee { monthlyBonus(): number { return 1000; }}
class Engineer extends Employee { monthlyBonus(): number { return 200; }}
function hire(name: string, role: string): Employee { switch (role) { case "manager": return new Manager(name); case "engineer": return new Engineer(name); default: throw new Error(`Unknown role: ${role}`); }}
const e = hire("Sara", "manager");# Beforeclass Employee: def __init__(self, name, type): self.name = name self.type = type
def monthly_bonus(self): return 1000 if self.type == "manager" else 200
e = Employee("Sara", "manager")
# Afterfrom abc import ABC, abstractmethod
class Employee(ABC): def __init__(self, name): self.name = name
@abstractmethod def monthly_bonus(self): ...
class Manager(Employee): def monthly_bonus(self): return 1000
class Engineer(Employee): def monthly_bonus(self): return 200
def hire(name, role): builders = {"manager": Manager, "engineer": Engineer} if role not in builders: raise ValueError(f"Unknown role: {role}") return builders[role](name)
e = hire("Sara", "manager")// Beforetype Employee struct { Name string Type string}
func (e Employee) MonthlyBonus() float64 { if e.Type == "manager" { return 1000 } return 200}
e := Employee{Name: "Sara", Type: "manager"}
// Aftertype Employee interface { MonthlyBonus() float64}
type Manager struct{ Name string }
func (m Manager) MonthlyBonus() float64 { return 1000 }
type Engineer struct{ Name string }
func (e Engineer) MonthlyBonus() float64 { return 200 }
func Hire(name, role string) (Employee, error) { switch role { case "manager": return Manager{Name: name}, nil case "engineer": return Engineer{Name: name}, nil default: return nil, fmt.Errorf("unknown role: %s", role) }}
e, err := Hire("Sara", "manager")// Beforestruct Employee { name: String, kind: String,}
impl Employee { fn monthly_bonus(&self) -> u32 { if self.kind == "manager" { 1000 } else { 200 } }}
let e = Employee { name: "Sara".to_string(), kind: "manager".to_string() };
// Aftertrait Employee { fn monthly_bonus(&self) -> u32;}
struct Manager { name: String,}
impl Employee for Manager { fn monthly_bonus(&self) -> u32 { 1000 }}
struct Engineer { name: String,}
impl Employee for Engineer { fn monthly_bonus(&self) -> u32 { 200 }}
fn hire(name: &str, role: &str) -> Result<Box<dyn Employee>, String> { match role { "manager" => Ok(Box::new(Manager { name: name.to_string() })), "engineer" => Ok(Box::new(Engineer { name: name.to_string() })), other => Err(format!("unknown role: {other}")), }}
let e = hire("Sara", "manager")?;กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- เขียน factory function ที่ข้างในไม่ทำอะไรนอกจากเรียก constructor เดิมแล้วคืนผลลัพธ์ ตั้งชื่อให้บอกว่าสร้างอะไร เช่น
hireหรือdefaultConfig - เปลี่ยนเส้นทาง caller ไปยัง factory ทีละตัว แทนการเรียก constructor แต่ละครั้งด้วยการเรียก factory รัน test หลังแต่ละครั้ง
- เมื่อ caller ทุกตัวผ่าน factory แล้ว คุณก็ได้จุดคอขวดจุดเดียว ตอนนี้จะเสริมอะไรก็ได้ เช่น validate argument คืน instance ที่ cache ไว้ หรือแตก branch ไปยัง subtype
- ถ้ากำลังจะเพิ่ม subtype ก็สร้างขึ้นมา ย้าย behavior ของแต่ละ branch ไปไว้บน subtype ที่ตรงกัน แล้วให้ factory คืนตัวที่ถูกต้องตาม input
- รัน test หลังเปลี่ยนแต่ละครั้ง signature ของ factory คงที่ในสายตา caller ถึงแม้สิ่งที่สร้างข้างหลังจะเปลี่ยนไปเรื่อย ๆ
- หากภาษาอนุญาต ให้ทำ constructor ดิบเป็น private หรือ non-public เพื่อให้ factory เป็นทางเข้าที่รองรับเพียงทางเดียว
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”หยิบ factory มาใช้เมื่อการสร้างต้องการชื่อที่ชัดกว่าที่ชื่อ class จะให้ได้ เมื่อ type จริงที่ต้องสร้างขึ้นกับ input เมื่อคุณอยากซ่อน instance หรือรวมไว้เป็น pool หรือเมื่อต้องการ validate ก่อน object จะเกิดขึ้นจริง factory จะกลายเป็นประตูบานเดียวที่มีชื่อดี ๆ ให้ object เดินเข้าระบบ
ข้อแลกเปลี่ยนคือชั้นของการอ้อม: caller ไม่เห็น new และ type ที่เป็นรูปธรรมโดยตรงอีกต่อไป ซึ่งอาจทำให้การไล่ตาม code ชัดเจนน้อยลงเล็กน้อย สำหรับ type ค่าธรรมดาที่มีรูปทรงเดียวและไม่มีตัวแปร constructor โดยตรงเรียบง่ายกว่า และ factory ก็เป็นพิธีรีตองที่ไม่จำเป็น เพิ่ม factory เมื่อความแข็งทื่อของ constructor เริ่มทำให้คุณต้องเสีย — ไม่ใช่ก่อนหน้านั้น
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Replace Constructor with Factory เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| ต้องการ subtype ที่แตกต่างกันตาม input | type เดียว รูปทรงเดียว ไม่มี variant |
| ชื่อ constructor ไม่สื่อความหมายของ use case | เพิ่ม indirection โดยไม่ได้รับประโยชน์จริง |
| ต้องการ validate input ก่อน construct | factory ที่ซับซ้อนเกินไปจนเข้าใจยากกว่า constructor |
⚠️ ไม่ควร Replace Constructor with Factory เมื่อ:
- type ธรรมดา ไม่มี variant และไม่ต้องการชื่อพิเศษ — constructor ตรงไปตรงมากว่า
- factory ทำแค่
return new Thing(args)โดยไม่เพิ่มค่าอะไร- team ไม่คุ้นชินกับ pattern นี้และ codebase ไม่มี convention รองรับ