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 ในตัวมันเอง
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”- Composition root (จุดเดียวใน app ที่ผูก dependency เข้าด้วยกัน) ตัดสินใจว่าจะใช้ variant ไหน แล้วสร้าง concrete factory ตัวนั้นขึ้นมาหนึ่งตัว
- Concrete factory ตัวนั้นถูก inject เข้า client ผ่าน constructor โดยประกาศ type เป็น
IUiFactory(abstract) - เมื่อ client ต้องการ product ใด ๆ ก็เรียก method บน factory เช่น
CreateButton()— factory จะคืน concrete instance ของ variant ที่ตัวเองรับผิดชอบ แต่ client เห็นแค่IButton - เพราะ factory ตัวเดียวสร้างทุก product ใน family ตัวมันจึงการันตีได้เองว่า product ที่คืนออกไปจะ “เข้าชุดกัน” เสมอ ไม่มีทางได้
LightButtonปนกับDarkCheckbox - ถ้าจะเพิ่ม variant ใหม่ (เช่น
HighContrastFactory) แค่ implementIUiFactoryเพิ่มอีกตัว โดยไม่ต้องแก้ 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 เท่านั้น
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”// Abstract Product แต่ละชนิดใน familypublic 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 — จุดเดียวที่เลือก variantIUiFactory 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 เริ่มบวมเกินไป
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/design-patterns/abstract-factory-pattern
- Abstract Factory — Refactoring.Guru
- Abstract factory pattern — Wikipedia
- Abstract Factory Design Pattern — dofactory.com
- Design Patterns: Elements of Reusable Object-Oriented Software — Gang of Four (Gamma, Helm, Johnson, Vlissides)