Factory Method
นิยาม interface สำหรับสร้าง object แล้วปล่อยรายละเอียดให้ implementation
เวลา code เรียก new ConcreteClass() ตรง ๆ กระจายอยู่ทั่วโปรแกรม code นั้นจะผูกติด (coupled) กับ concrete type นั้นทันที ถ้าวันหนึ่งต้องเพิ่ม product ชนิดใหม่ หรือเปลี่ยนว่าจะสร้าง object แบบไหนตามเงื่อนไข (เช่น ตาม config, ตาม environment, ตาม request) เราต้องไล่แก้ทุกจุดที่มี new ฝังอยู่ — นี่คืออาการของ tight coupling ที่ทำให้ระบบเปราะและขยายยาก
Factory Method เป็นหนึ่งใน 23 pattern จากหนังสือ Design Patterns ของ Gang of Four (GoF) แก้ปัญหานี้ด้วยแนวคิดง่าย ๆ: แทนที่จะเรียก constructor ตรง ๆ ให้นิยาม method สำหรับสร้าง object ไว้ใน abstract class หรือ interface แล้วปล่อยให้ subclass เป็นผู้ตัดสินใจว่าจะสร้าง concrete type ไหน นิยามฉบับ GoF คือ
“Define an interface for creating an object, but let subclasses decide which class to instantiate.”
ผลลัพธ์คือ loose coupling ระหว่าง code ที่ ใช้ object กับ code ที่ สร้าง object code ฝั่งผู้ใช้รู้จักแค่ abstract product กับ abstract creator เท่านั้น ส่วนการตัดสินใจว่าจะ instantiate class ไหนถูกห่อหุ้ม (encapsulate) ไว้ใน concrete creator ทำให้เพิ่ม product ชนิดใหม่ได้โดยไม่ต้องแก้ code เดิม สอดคล้องกับ Open/Closed Principle และมักใช้ร่วมกับ dependency injection โดยส่ง factory ที่เจาะจงเข้าไปตอน composition root
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”Creator ประกาศ factory method ไว้ (จะเป็น abstract หรือมี default implementation ก็ได้) ConcreteCreator แต่ละตัวจะ override factory method นี้เพื่อคืนค่า ConcreteProduct ของตัวเอง — Creator ไม่รู้จัก ConcreteProduct เลยแม้แต่น้อย รู้จักแค่ Product interface
classDiagram
class Creator {
+FactoryMethod() Product
+SomeOperation()
}
class ConcreteCreatorA {
+FactoryMethod() Product
}
class ConcreteCreatorB {
+FactoryMethod() Product
}
class Product {
+Operation()
}
class ConcreteProductA {
+Operation()
}
class ConcreteProductB {
+Operation()
}
Creator <|-- ConcreteCreatorA
Creator <|-- ConcreteCreatorB
Product <|.. ConcreteProductA
Product <|.. ConcreteProductB
ConcreteCreatorA ..> ConcreteProductA
ConcreteCreatorB ..> ConcreteProductB
(Creator เป็น abstract class, Product เป็น interface — ConcreteCreator แต่ละตัว override factory method เพื่อคืนค่า ConcreteProduct ของตัวเอง)
จุดสำคัญคือ Creator มักไม่ใช่แค่ “โรงงานเปล่า ๆ” — มันมักมี business logic ของตัวเอง (SomeOperation ในแผนภาพ) ที่ ใช้ product ซึ่งได้จาก factory method นี่คือความต่างสำคัญจาก Abstract Factory ที่ทั้ง class มีหน้าที่สร้าง object ล้วน ๆ ไม่มี business logic อื่นปน
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”- Client เรียก business method บน concrete creator (เช่น
SendAlert) - ภายใน method นั้น creator เรียก factory method ของตัวเอง (
CreateNotification) เพื่อขอ product - เพราะเป็น virtual/abstract method แต่ละ ConcreteCreator จึงคืนค่า ConcreteProduct ที่ตัวเองผูกไว้
- Creator ใช้งาน product ผ่าน Product interface เท่านั้น — ไม่รู้เลยว่าได้ ConcreteProduct ตัวไหนมา
sequenceDiagram
participant Client
participant ConcreteCreatorA
participant ProductA as ConcreteProductA
Client->>ConcreteCreatorA: SomeOperation
ConcreteCreatorA->>ConcreteCreatorA: FactoryMethod
ConcreteCreatorA->>ProductA: new ConcreteProductA
ConcreteCreatorA->>ProductA: Operation
ProductA-->>ConcreteCreatorA: result
ConcreteCreatorA-->>Client: result
ถ้าจะสลับ product ที่ใช้ทั้งระบบ ก็แค่สลับว่า client ถืออ้างอิง ConcreteCreator ตัวไหน — จุดตัดสินใจนี้มักอยู่ที่ composition root หรือ DI container จุดเดียว ไม่กระจายอยู่ทั่ว code
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”ตัวอย่างระบบแจ้งเตือนที่ต้องรองรับหลายช่องทาง (email, SMS) โดยตัวเรียกใช้งาน (AlertService) ไม่ต้องรู้จัก concrete notification class เลย
// Product interface — สัญญาที่ product ทุกชนิดต้องทำตามpublic interface INotification{ void Send(string recipient, string message);}
// ConcreteProduct — การส่งจริงของแต่ละช่องทางpublic class EmailNotification : INotification{ public void Send(string recipient, string message) => Console.WriteLine($"[Email] ถึง {recipient}: {message}");}
public class SmsNotification : INotification{ public void Send(string recipient, string message) => Console.WriteLine($"[SMS] ถึง {recipient}: {message}");}
// Creator — ประกาศ factory method ไว้เป็น abstract// และมี business logic ของตัวเอง (SendAlert) ที่ "ใช้" product โดยไม่รู้ว่าเป็นชนิดไหนpublic abstract class NotifierFactory{ // factory method — ให้ subclass ตัดสินใจว่าจะสร้าง product ชนิดไหน protected abstract INotification CreateNotification();
// template method ง่าย ๆ ที่ใช้ product ซึ่งได้จาก factory method // นี่คือเหตุผลที่ Creator ไม่ใช่แค่ "โรงงานเปล่า ๆ" แต่มี logic ที่คุ้มค่าจะ share ผ่าน inheritance public void SendAlert(string recipient, string message) { INotification notification = CreateNotification(); notification.Send(recipient, message); }}
// ConcreteCreator — ผูกกับ product หนึ่งชนิดต่อ1 classpublic class EmailFactory : NotifierFactory{ protected override INotification CreateNotification() => new EmailNotification();}
public class SmsFactory : NotifierFactory{ protected override INotification CreateNotification() => new SmsNotification();}
// Client code — รู้จักแค่ NotifierFactory ไม่รู้จัก EmailNotification หรือ SmsNotification เลยpublic class AlertService{ private readonly NotifierFactory _factory;
public AlertService(NotifierFactory factory) => _factory = factory;
public void NotifyUser(string recipient, string message) => _factory.SendAlert(recipient, message);}
// การใช้งานจริง — เลือก concrete factory ตอน composition root เช่นตอน register DI containervar service = new AlertService(new EmailFactory());service.NotifyUser("user@example.com", "คำสั่งซื้อของคุณถูกส่งแล้ว");สังเกตว่า AlertService ไม่มี if (channel == "email") หรือ switch ใด ๆ เลย — การตัดสินใจว่าจะส่งช่องทางไหนถูกย้ายไปอยู่ที่ “จะฉีด EmailFactory หรือ SmsFactory เข้ามา” ซึ่งมักตัดสินครั้งเดียวตอน startup
เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- ตอน compile-time ยังไม่รู้ชนิด object ที่ต้องสร้างแน่ชัด — รู้แค่ว่าต้องเป็นไปตาม interface ไหน
- ต้องการให้ codebase (โดยเฉพาะ framework/library) เปิดช่องให้ผู้ใช้ขยาย product ชนิดใหม่ได้ โดยไม่ต้องแก้ source เดิม
- Class มี logic ที่ต้อง share ระหว่าง product หลายชนิด แต่ส่วนสร้าง object ต่างกันไปตาม subclass (เข้ากับ template method)
- ต้องการรวมจุดตัดสินใจว่าจะสร้าง object แบบไหนไว้ที่เดียว แทนที่จะกระจาย
newไปทั่ว code - ต้องการ reuse หรือ pool object แทนที่จะสร้างใหม่ทุกครั้ง (แอบ logic ไว้หลัง factory method ได้)
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- มี product แค่ชนิดเดียว และไม่มีสัญญาณว่าจะเพิ่มในอนาคต —
newตรง ๆ ชัดเจนและอ่านง่ายกว่า ยึด YAGNI - ต้องการสร้าง “กลุ่ม” ของ object ที่สัมพันธ์กันหลายชนิดพร้อมกัน (เช่น UI theme ที่ต้องมีทั้งปุ่ม, ช่อง input, สกอลบาร์ที่เข้าชุดกัน) — กรณีนี้ Abstract Factory เหมาะกว่า
- การสร้าง object ซับซ้อนจาก parameter หลายตัวที่ต้องประกอบทีละขั้น — ใช้ Builder แทน
- ทีมยังไม่คุ้นกับ inheritance hierarchy ที่เพิ่มขึ้น การเพิ่ม abstraction ตอนนี้อาจทำให้ code อ่านยากกว่าเดิมโดยไม่ได้ประโยชน์จริง
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | ตัด coupling ระหว่าง code ที่ใช้ object กับ concrete class ที่ถูกสร้าง |
| ข้อดี | เพิ่ม product ชนิดใหม่ได้โดยไม่ต้องแก้ code เดิม (Open/Closed) |
| ข้อดี | รวม logic การสร้าง object ไว้ที่เดียว ง่ายต่อการทดสอบและดูแล (Single Responsibility) |
| ข้อดี | เปิดช่องให้ subclass ผสาน creation logic เข้ากับ business logic อื่นผ่าน template method |
| ข้อเสีย | เพิ่มจำนวน class และ hierarchy — ถ้ามี product แค่ชนิดเดียวจะดูเกินความจำเป็น |
| ข้อเสีย | ต้องสร้าง ConcreteCreator ใหม่ทุกครั้งที่เพิ่ม ConcreteProduct หนึ่งชนิด แม้ว่าตัว product เองจะไม่ซับซ้อน |
| ข้อเสีย | ถ้าใช้พร่ำเพรื่อในที่ที่ constructor ธรรมดาก็เพียงพอ จะกลายเป็น over-engineering |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Abstract Factory — สร้าง “กลุ่ม” ของ product ที่สัมพันธ์กัน ไม่ใช่ product เดี่ยว
- Builder — ใช้เมื่อการสร้าง object ต้องประกอบทีละขั้นตอนจาก parameter ที่ซับซ้อน
- Strategy — ทั้งคู่สลับ implementation ผ่าน interface แต่ Strategy เน้นสลับ พฤติกรรม ไม่ใช่การสร้าง object
- Dependency Inversion Principle — หลักการเบื้องหลังที่ทำให้ Creator พึ่งพา abstraction แทน concrete class
- Dependency Injection — วิธีที่มักใช้เลือกฉีด concrete factory เข้าไปตอน composition root
- Open/Closed Principle — เหตุผลเชิงหลักการที่ทำให้เพิ่ม product ใหม่ได้โดยไม่แก้ code เดิม