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

Abstract Factory

interface สำหรับ​สร้าง “ครอบครัว” ของ object ที่​เกี่ยวข้อง​กัน

เวลา code ต้อง​สร้าง object หลาย​ตัว​ที่ “ต้อง​เข้า​ชุด​กัน” — เช่น component ของ UI ทั้ง​ชุด​ต้อง​เป็น​ธีม​เดียวกัน หรือ provider ของ​ฐาน​ข้อมูล​ทั้ง​ชุด​ต้อง​เป็น driver เดียวกัน — การ​ใช้ new กระจัดกระจาย​ทั่ว code จะ​ทำให้​เกิด​ปัญหา​สอง​อย่าง หนึ่ง​คือ code client ผูก​ติด​กับ concrete class โดยตรง (ผิด Dependency Inversion) สอง​คือ​ไม่มี​อะไร​การันตี​ว่า object ที่​สร้าง​มา​จะ “เข้า​กัน​ได้” กับ object อื่น​ใน​ชุด​เดียวกัน สมมติ​สร้าง LightButton แต่​ดัน​ไป​จับ​คู่​กับ DarkCheckbox โดย​ไม่​ตั้งใจ UI ก็​จะ​ดู​แปลก​ทันที

Abstract Factory Pattern แก้​ปัญหา​นี้​ด้วย​การ​ให้ interface หนึ่ง​ตัว​ประกาศ method สำหรับ​สร้าง product ทุก​ตัว​ใน “ครอบครัว” (family) นั้น แล้ว​ให้​แต่ละ concrete factory implement method เหล่า​นั้น​เพื่อ​สร้าง product ทั้ง​ชุด​ที่ variant เดียวกัน​เสมอ client จึง​เรียก​ผ่าน abstract factory และ abstract product เท่านั้น ไม่รู้จัก​และ​ไม่​ผูก​กับ concrete class ใด ๆ เลย การ​สลับ​ทั้ง​ชุด (เช่น สลับ​ธีม หรือ​สลับ database provider) ทำได้​แค่​เปลี่ยนตัว factory ที่ inject เข้าไป​ตัว​เดียว

แนวคิด​นี้​มา​จาก​หนังสือ Design Patterns ของ Gang of Four (Gamma, Helm, Johnson, Vlissides) ซึ่ง​นิยาม​ไว้​ว่า “Provide an interface for creating families of related or dependent objects without specifying their concrete classes”

classDiagram
    class IUiFactory {
      <<interface>>
      +CreateButton() IButton
      +CreateCheckbox() ICheckbox
    }
    class LightFactory {
      +CreateButton() IButton
      +CreateCheckbox() ICheckbox
    }
    class DarkFactory {
      +CreateButton() IButton
      +CreateCheckbox() ICheckbox
    }
    class IButton {
      <<interface>>
      +Render()
    }
    class ICheckbox {
      <<interface>>
      +Render()
    }
    class LightButton
    class DarkButton
    class LightCheckbox
    class DarkCheckbox
    class Client {
      -IUiFactory factory
      +BuildToolbar()
    }

    IUiFactory <|.. LightFactory
    IUiFactory <|.. DarkFactory
    IButton <|.. LightButton
    IButton <|.. DarkButton
    ICheckbox <|.. LightCheckbox
    ICheckbox <|.. DarkCheckbox
    LightFactory ..> LightButton
    LightFactory ..> LightCheckbox
    DarkFactory ..> DarkButton
    DarkFactory ..> DarkCheckbox
    Client o-- IUiFactory

ผู้​เกี่ยวข้อง (participants)

  • Abstract Factory (IUiFactory) — ประกาศ method สร้าง product แต่ละ​ชนิด​ใน family
  • Concrete Factory (LightFactory, DarkFactory) — implement การ​สร้าง product ทั้ง​ชุด​ของ variant หนึ่ง ๆ
  • Abstract Product (IButton, ICheckbox) — interface ของ product แต่ละ​ชนิด
  • Concrete Product (LightButton, DarkCheckbox ฯลฯ) — implementation จริง​ของ​แต่ละ variant
  • Client — เรียก​ใช้​ผ่าน IUiFactory และ interface ของ product เท่านั้น ไม่รู้จัก concrete class เลย

สังเกต​ว่า​นี่​คือ​การนำ Factory Method หลาย​ตัว​มา​รวม​ไว้​ใน interface เดียว — แต่ละ method ของ Abstract Factory มัก​เป็น Factory Method ใน​ตัว​มัน​เอง

  1. Composition root (จุด​เดียว​ใน app ที่​ผูก dependency เข้า​ด้วย​กัน) ตัดสิน​ใจ​ว่า​จะ​ใช้ variant ไหน แล้ว​สร้าง concrete factory ตัว​นั้น​ขึ้น​มา​หนึ่ง​ตัว
  2. Concrete factory ตัว​นั้น​ถูก inject เข้า client ผ่าน constructor โดย​ประกาศ type เป็น IUiFactory (abstract)
  3. เมื่อ client ต้องการ product ใด ๆ ก็​เรียก method บน factory เช่น CreateButton() — factory จะ​คืน concrete instance ของ variant ที่​ตัวเอง​รับผิดชอบ แต่ client เห็น​แค่ IButton
  4. เพราะ factory ตัว​เดียว​สร้าง​ทุก product ใน family ตัว​มัน​จึง​การันตี​ได้​เอง​ว่า product ที่​คืน​ออก​ไป​จะ “เข้า​ชุด​กัน” เสมอ ไม่มี​ทาง​ได้ LightButton ปน​กับ DarkCheckbox
  5. ถ้า​จะ​เพิ่ม variant ใหม่ (เช่น HighContrastFactory) แค่ implement IUiFactory เพิ่ม​อีก​ตัว โดย​ไม่​ต้อง​แก้ code client เลย — เข้า​หลัก Open-Closed
sequenceDiagram
    participant Root as Composition Root
    participant Client
    participant Factory as DarkFactory
    participant Button as DarkButton

    Root->>Client: new Client(new DarkFactory())
    Client->>Factory: CreateButton()
    Factory->>Button: new DarkButton()
    Factory-->>Client: IButton
    Client->>Client: ใช้งานผ่าน IButton เท่านั้น
// Abstract Product แต่ละชนิดใน family
public interface IButton
{
void Render();
}
public interface ICheckbox
{
void Render();
}
// Concrete Product ของ variant "Light"
public class LightButton : IButton
{
public void Render() => Console.WriteLine("[ปุ่มธีมสว่าง]");
}
public class LightCheckbox : ICheckbox
{
public void Render() => Console.WriteLine("[checkbox ธีมสว่าง]");
}
// Concrete Product ของ variant "Dark"
public class DarkButton : IButton
{
public void Render() => Console.WriteLine("[ปุ่มธีมมืด]");
}
public class DarkCheckbox : ICheckbox
{
public void Render() => Console.WriteLine("[checkbox ธีมมืด]");
}
// Abstract Factory — สร้างทั้ง family ในที่เดียว
public interface IUiFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
}
// Concrete Factory หนึ่งตัวต่อ1 variant การันตีว่า product เข้าชุดกันเสมอ
public class LightFactory : IUiFactory
{
public IButton CreateButton() => new LightButton();
public ICheckbox CreateCheckbox() => new LightCheckbox();
}
public class DarkFactory : IUiFactory
{
public IButton CreateButton() => new DarkButton();
public ICheckbox CreateCheckbox() => new DarkCheckbox();
}
// Client — รู้จักแค่ IUiFactory และ abstract product เท่านั้น
public class Toolbar
{
private readonly IUiFactory _factory;
public Toolbar(IUiFactory factory)
{
_factory = factory; // รับ factory ผ่าน constructor (ไม่ new เอง)
}
public void Build()
{
var button = _factory.CreateButton();
var checkbox = _factory.CreateCheckbox();
button.Render();
checkbox.Render();
}
}
// Composition root — จุดเดียวที่เลือก variant
IUiFactory factory = userPrefersDarkMode
? new DarkFactory()
: new LightFactory();
var toolbar = new Toolbar(factory);
toolbar.Build(); // ได้ component ธีมเดียวกันทั้งชุดเสมอ
  • เมื่อ code ต้อง​ทำงาน​กับ หลาย variant ของ product family ที่​ต้อง​สลับ​กัน​ได้​ทั้ง​ชุด (ธีม UI, database provider, cross-platform toolkit)
  • เมื่อ​ต้องการ​การันตี​ว่า product ที่​สร้าง​จาก factory เดียวกัน “เข้า​กัน​ได้” เสมอ ไม่​ปน​กัน​ข้าม variant
  • เมื่อ​ระบบ​มี Factory Method หลาย​ตัว​กระจาย​อยู่ และ​เริ่ม​เห็น pattern ว่า​มัน​ควร​จะ​ถูก​จัด​กลุ่ม​เป็น family เดียวกัน
  • เมื่อ​ต้องการ​เปิด​ทาง​ให้​เพิ่ม variant ใหม่​ใน​อนาคต​โดย​ไม่​แก้ code client (Open-Closed)
  • เมื่อ​มี product แค่​ชนิด​เดียว​หรือ​ไม่มี family ที่​ต้อง​เข้า​ชุด​กัน​จริง ๆ — ใช้ Factory Method ธรรมดา หรือ​แม้แต่ constructor ตรง ๆ ก็พอ (ดู YAGNI)
  • เมื่อ​จำนวน variant คงที่​และ​แทบ​ไม่​เปลี่ยน การ​เพิ่ม interface และ class จำนวน​มาก​อาจ​เป็นการ​เพิ่ม complexity โดย​ไม่​จำเป็น
  • เมื่อ Dependency Injection container สามารถ​แก้​ปัญหา​การ​ประกอบ object ได้​ตรง​ไป​ตรง​มากว่า — Abstract Factory เหมาะ​กับ​การ​สลับ “ทั้ง​ชุด” พร้อม​กัน ส่วน DI เหมาะ​กับ​การ​จัดการ dependency แบบ​ละเอียด​ที​ละ​ตัว
  • เมื่อ family ของ product มี​แนวโน้ม​จะ​เพิ่ม method ใหม่​บ่อย ๆ เพราะ​ทุก concrete factory ต้อง implement method ใหม่​นั้น​หมด (ผิด​หลัก Interface Segregation ถ้า interface บวม​เกิน​ไป)
ข้อดีข้อ​เสีย
การันตี​ว่า product ใน​ชุด​เดียวกัน​เข้า​กัน​ได้​เสมอเพิ่ม​จำนวน interface และ class ค่อน​ข้าง​มาก
Client ไม่​ผูก​กับ concrete class เลย ทดสอบ​และ​สลับ implementation ง่ายเพิ่ม variant ใหม่​ทำ​ง่าย แต่​เพิ่ม product ชนิด​ใหม่​ใน​ทุก factory ต้อง​แก้​ทุก concrete factory
เพิ่ม variant ใหม่​ได้​โดย​ไม่​แตะ code client (Open-Closed)อาจ over-engineer ถ้า​จริง ๆ มี variant เดียว​หรือ family เล็ก​มาก
รวม logic การ​สร้าง object ไว้​ที่​เดียว (Single Responsibility)ทำให้ debug ยาก​ขึ้น​เล็กน้อย​เพราะ​มี indirection เพิ่ม​ขึ้น​หนึ่ง​ชั้น
  • Factory Method — Abstract Factory มัก​ประกอบ​ขึ้น​จาก Factory Method หลาย​ตัว​รวม​กัน
  • Dependency Inversion — หลักการ​ที่ Abstract Factory ใช้​เพื่อ​ให้ client ไม่​ผูก​กับ concrete class
  • Open-Closed — เพิ่ม variant ใหม่​ได้​โดย​ไม่​แก้ code เดิม
  • Dependency Injection — แนวทาง​จัดการ dependency แบบ​รวม​ศูนย์​ที่​มัก​ใช้​คู่​กับ Abstract Factory
  • Builder — อีก creational pattern ที่​เน้น​สร้าง object ซับซ้อน​ที​ละ​ขั้น ต่าง​จาก Abstract Factory ที่​เน้น​สร้าง family ของ object ที่​เข้า​ชุด​กัน
  • Interface Segregation — ข้อ​ควร​ระวัง​เมื่อ abstract factory interface เริ่ม​บวม​เกิน​ไป