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

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 อื่น​ปน

  1. Client เรียก business method บน concrete creator (เช่น SendAlert)
  2. ภายใน method นั้น creator เรียก factory method ของ​ตัวเอง (CreateNotification) เพื่อ​ขอ product
  3. เพราะ​เป็น virtual/abstract method แต่ละ ConcreteCreator จึง​คืน​ค่า ConcreteProduct ที่​ตัวเอง​ผูก​ไว้
  4. 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

ตัวอย่าง​ระบบ​แจ้ง​เตือน​ที่​ต้อง​รองรับ​หลาย​ช่อง​ทาง (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 class
public 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 container
var 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 เดิม