ในโลกของการพัฒนาซอฟต์แวร์ ปัญหาหลายอย่างเกิดซ้ำแล้วซ้ำเล่า ไม่ว่าจะเป็นการสร้าง Object ที่ซับซ้อน การจัดการ Dependencies ระหว่าง Module หรือการออกแบบระบบที่ต้องรองรับการเปลี่ยนแปลงในอนาคต Design Patterns คือชุดของวิธีแก้ปัญหาที่ถูกพิสูจน์แล้วว่าใช้งานได้จริง ถูกรวบรวมและจัดหมวดหมู่เพื่อให้นักพัฒนาสามารถนำไปใช้ได้ทันทีโดยไม่ต้องเริ่มต้นจากศูนย์
บทความนี้จะพาคุณทำความรู้จัก Design Patterns ตั้งแต่ที่มาประวัติศาสตร์ หลักการแบ่งหมวดหมู่ทั้ง 3 กลุ่ม และสอน 10 Design Patterns ที่สำคัญที่สุดพร้อมตัวอย่างโค้ดทั้ง Python และ TypeScript รวมถึงกรณีที่ควรใช้และไม่ควรใช้ เพื่อให้คุณสามารถนำไปประยุกต์ใช้ในงานจริงได้อย่างมั่นใจ
Design Patterns คืออะไร? ทำไมต้องรู้?
Design Patterns คือแนวทางการแก้ปัญหาที่เกิดขึ้นบ่อยในการออกแบบซอฟต์แวร์ ไม่ใช่โค้ดสำเร็จรูปที่ Copy-Paste ได้ แต่เป็น Template หรือ Blueprint ที่บอกว่า "เมื่อเจอปัญหาแบบนี้ ให้ออกแบบโครงสร้างโค้ดแบบนี้" คล้ายกับสูตรอาหารที่บอกขั้นตอนและส่วนผสม แต่คุณสามารถปรับเปลี่ยนรสชาติตามความชอบได้
เหตุผลที่ Developer ต้องรู้ Design Patterns ได้แก่:
- ภาษากลางในทีม: เมื่อพูดว่า "ใช้ Observer Pattern" ทุกคนเข้าใจตรงกันทันที ไม่ต้องอธิบายยาว
- ลดเวลาออกแบบ: ไม่ต้องคิดวิธีแก้ปัญหาใหม่จากศูนย์ เพราะมีรูปแบบที่พิสูจน์แล้ว
- Code ที่ดูแลง่าย: โค้ดที่ใช้ Pattern ที่ถูกต้องจะอ่านง่าย แก้ไขง่าย และขยายได้ง่าย
- สัมภาษณ์งาน: บริษัท Tech ชั้นนำมักถามเรื่อง Design Patterns ในการสัมภาษณ์ Senior Developer
- เข้าใจ Framework: Framework ยอดนิยมอย่าง React, Django, Spring ล้วนใช้ Design Patterns เป็นพื้นฐาน เมื่อเข้าใจ Pattern คุณจะเข้าใจ Framework ลึกขึ้น
ประวัติ Gang of Four (GoF)
แนวคิด Design Patterns ในซอฟต์แวร์ได้รับแรงบันดาลใจจากสถาปัตยกรรมศาสตร์ โดย Christopher Alexander ได้เขียนหนังสือเกี่ยวกับ Pattern Language ในการออกแบบอาคารและเมืองในปี 1977 แนวคิดนี้ถูกนำมาประยุกต์ใช้กับซอฟต์แวร์ จนในปี 1994 นักวิทยาศาสตร์คอมพิวเตอร์ 4 คน ได้แก่ Erich Gamma, Richard Helm, Ralph Johnson และ John Vlissides ได้ร่วมกันเขียนหนังสือชื่อ "Design Patterns: Elements of Reusable Object-Oriented Software" หนังสือเล่มนี้กลายเป็นตำราคลาสสิกที่ Developer ทั่วโลกอ้างอิง และผู้เขียนทั้ง 4 คนถูกเรียกว่า "Gang of Four" หรือ GoF
หนังสือ GoF ได้จัดหมวดหมู่ Design Patterns เป็น 23 รูปแบบ แบ่งออกเป็น 3 กลุ่มหลัก ซึ่งเป็นรากฐานที่ใช้มาจนถึงทุกวันนี้
3 หมวดหมู่ของ Design Patterns
1. Creational Patterns (สร้าง Object)
เกี่ยวกับกระบวนการสร้าง Object ช่วยให้ระบบไม่ผูกติดกับวิธีการสร้าง Object โดยตรง ตัวอย่าง: Singleton, Factory Method, Abstract Factory, Builder, Prototype
2. Structural Patterns (จัดโครงสร้าง)
เกี่ยวกับการจัดระเบียบความสัมพันธ์ระหว่าง Class และ Object เพื่อสร้างโครงสร้างที่ใหญ่ขึ้นอย่างยืดหยุ่น ตัวอย่าง: Adapter, Decorator, Facade, Proxy, Composite, Bridge, Flyweight
3. Behavioral Patterns (จัดการพฤติกรรม)
เกี่ยวกับการสื่อสารและแบ่งหน้าที่ระหว่าง Object ช่วยให้ Object ทำงานร่วมกันอย่างมีระเบียบ ตัวอย่าง: Observer, Strategy, Command, Template Method, Iterator, State, Mediator, Chain of Responsibility
| หมวดหมู่ | วัตถุประสงค์ | Pattern ที่จะสอน |
|---|---|---|
| Creational | สร้าง Object อย่างยืดหยุ่น | Singleton, Factory Method, Abstract Factory, Builder |
| Structural | จัดโครงสร้าง Class | Adapter, Decorator |
| Behavioral | จัดการพฤติกรรมการทำงาน | Observer, Strategy, Command, Template Method |
1. Singleton Pattern
เปรียบเทียบง่ายๆ: เหมือนประธานาธิบดีของประเทศ — มีได้แค่คนเดียวเท่านั้นในเวลาเดียวกัน ใครจะติดต่อก็ได้คนเดียวกัน
Singleton ทำให้มั่นใจว่า Class จะมี Instance เพียงหนึ่งเดียวตลอดทั้งโปรแกรม และมีจุดเข้าถึงแบบ Global สำหรับ Instance นั้น ใช้บ่อยกับ Database Connection Pool, Logger, Configuration Manager หรือ Cache Manager ที่ต้องการให้ทั้งระบบใช้ Resource เดียวกัน
Python:
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.connection = cls._connect()
return cls._instance
@staticmethod
def _connect():
print("Creating new database connection...")
return "db_connection_object"
def query(self, sql: str):
return f"Executing: {sql} on {self.connection}"
# ใช้งาน
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # True — เป็น Instance เดียวกัน
print(db1.query("SELECT * FROM users"))
TypeScript:
class Logger {
private static instance: Logger;
private logs: string[] = [];
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
log(message: string): void {
const timestamp = new Date().toISOString();
this.logs.push(`[${timestamp}] ${message}`);
console.log(`[${timestamp}] ${message}`);
}
getLogs(): string[] {
return [...this.logs];
}
}
// ใช้งาน
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // true
logger1.log("Application started");
เมื่อไหร่ไม่ควรใช้: เมื่อ Class ต้องมีหลาย Instance ที่แตกต่างกัน หรือเมื่อต้องการ Test แบบ Isolated — Singleton ทำให้ Unit Testing ยากขึ้น
2. Factory Method Pattern
เปรียบเทียบง่ายๆ: เหมือนร้านพิซซ่าที่มีสาขาหลายแห่ง — สาขา New York ทำพิซซ่าแบบบาง สาขา Chicago ทำแบบหนา แต่ทุกสาขาใช้กระบวนการสั่งซื้อ-จัดทำ-ส่งมอบเหมือนกัน ลูกค้าไม่ต้องรู้ว่าครัวทำพิซซ่าอย่างไร แค่สั่งแล้วได้รับพิซซ่า
Factory Method กำหนด Interface สำหรับการสร้าง Object แต่ให้ Subclass เป็นคนตัดสินใจว่าจะสร้าง Class ไหน ช่วยแยกโค้ดที่สร้าง Object ออกจากโค้ดที่ใช้ Object ทำให้เพิ่มประเภทใหม่ได้ง่ายโดยไม่ต้องแก้โค้ดเดิม
Python:
from abc import ABC, abstractmethod
class Notification(ABC):
@abstractmethod
def send(self, message: str) -> str:
pass
class EmailNotification(Notification):
def send(self, message: str) -> str:
return f"Sending EMAIL: {message}"
class SMSNotification(Notification):
def send(self, message: str) -> str:
return f"Sending SMS: {message}"
class PushNotification(Notification):
def send(self, message: str) -> str:
return f"Sending PUSH: {message}"
class NotificationFactory:
@staticmethod
def create(channel: str) -> Notification:
factories = {
"email": EmailNotification,
"sms": SMSNotification,
"push": PushNotification,
}
if channel not in factories:
raise ValueError(f"Unknown channel: {channel}")
return factories[channel]()
# ใช้งาน
notification = NotificationFactory.create("email")
print(notification.send("Hello World"))
notification = NotificationFactory.create("sms")
print(notification.send("Your OTP is 123456"))
TypeScript:
interface Payment {
process(amount: number): string;
}
class CreditCardPayment implements Payment {
process(amount: number): string {
return `Processing credit card payment: ${amount} THB`;
}
}
class BankTransferPayment implements Payment {
process(amount: number): string {
return `Processing bank transfer: ${amount} THB`;
}
}
class PromptPayPayment implements Payment {
process(amount: number): string {
return `Processing PromptPay: ${amount} THB`;
}
}
class PaymentFactory {
static create(method: string): Payment {
switch (method) {
case "credit_card": return new CreditCardPayment();
case "bank_transfer": return new BankTransferPayment();
case "promptpay": return new PromptPayPayment();
default: throw new Error(`Unknown payment: ${method}`);
}
}
}
// ใช้งาน
const payment = PaymentFactory.create("promptpay");
console.log(payment.process(1500)); // Processing PromptPay: 1500 THB
ไม่ควรใช้เมื่อ: มีแค่ 1-2 ประเภทที่ไม่น่าจะเพิ่มอีก — การสร้าง Factory จะเป็น Over-engineering
3. Abstract Factory Pattern
เปรียบเทียบง่ายๆ: เหมือนโรงงานผลิตเฟอร์นิเจอร์ที่มีหลายสไตล์ — สั่งแบบ Modern ได้โต๊ะ Modern + เก้าอี้ Modern + ตู้ Modern สั่งแบบ Vintage ได้โต๊ะ Vintage + เก้าอี้ Vintage + ตู้ Vintage ทุกอย่างในชุดเดียวกันเข้ากัน
Abstract Factory สร้างกลุ่มของ Object ที่เกี่ยวข้องกัน (Product Family) โดยไม่ต้องระบุ Concrete Class เหมาะสำหรับระบบที่ต้องรองรับหลาย Platform หรือหลาย Theme
Python:
from abc import ABC, abstractmethod
class Button(ABC):
@abstractmethod
def render(self) -> str: pass
class Input(ABC):
@abstractmethod
def render(self) -> str: pass
class LightButton(Button):
def render(self) -> str:
return "<button class='light-btn'>Click</button>"
class DarkButton(Button):
def render(self) -> str:
return "<button class='dark-btn'>Click</button>"
class LightInput(Input):
def render(self) -> str:
return "<input class='light-input'/>"
class DarkInput(Input):
def render(self) -> str:
return "<input class='dark-input'/>"
class UIFactory(ABC):
@abstractmethod
def create_button(self) -> Button: pass
@abstractmethod
def create_input(self) -> Input: pass
class LightThemeFactory(UIFactory):
def create_button(self) -> Button:
return LightButton()
def create_input(self) -> Input:
return LightInput()
class DarkThemeFactory(UIFactory):
def create_button(self) -> Button:
return DarkButton()
def create_input(self) -> Input:
return DarkInput()
# ใช้งาน — เปลี่ยน Theme ได้โดยไม่แก้โค้ดที่เหลือ
def build_ui(factory: UIFactory):
btn = factory.create_button()
inp = factory.create_input()
print(btn.render())
print(inp.render())
build_ui(DarkThemeFactory())
# <button class='dark-btn'>Click</button>
# <input class='dark-input'/>
ไม่ควรใช้เมื่อ: มีแค่ Object ประเภทเดียว — ใช้ Factory Method ธรรมดาก็พอ
4. Builder Pattern
เปรียบเทียบง่ายๆ: เหมือนสั่งเบอร์เกอร์ที่ Subway — คุณเลือกขนมปัง เลือกเนื้อ เลือกผัก เลือกซอส ทีละขั้น สุดท้ายได้เบอร์เกอร์ตามที่คุณต้องการ ไม่ใช่รับแบบสำเร็จรูปที่เปลี่ยนอะไรไม่ได้
Builder Pattern แยกกระบวนการสร้าง Object ที่ซับซ้อนออกจากตัว Object เอง ทำให้สร้าง Object ที่มี Configuration หลากหลายได้โดยใช้กระบวนการเดียวกัน เหมาะกับ Object ที่มี Constructor Parameter มากเกิน 4-5 ตัว
Python:
class QueryBuilder:
def __init__(self):
self._table = ""
self._conditions = []
self._columns = ["*"]
self._order_by = None
self._limit = None
def table(self, name: str) -> "QueryBuilder":
self._table = name
return self
def select(self, *columns: str) -> "QueryBuilder":
self._columns = list(columns)
return self
def where(self, condition: str) -> "QueryBuilder":
self._conditions.append(condition)
return self
def order_by(self, column: str, direction: str = "ASC") -> "QueryBuilder":
self._order_by = f"{column} {direction}"
return self
def limit(self, n: int) -> "QueryBuilder":
self._limit = n
return self
def build(self) -> str:
cols = ", ".join(self._columns)
sql = f"SELECT {cols} FROM {self._table}"
if self._conditions:
sql += " WHERE " + " AND ".join(self._conditions)
if self._order_by:
sql += f" ORDER BY {self._order_by}"
if self._limit:
sql += f" LIMIT {self._limit}"
return sql
# ใช้งาน — อ่านง่ายมาก
query = (
QueryBuilder()
.table("users")
.select("id", "name", "email")
.where("age > 18")
.where("status = 'active'")
.order_by("name")
.limit(10)
.build()
)
print(query)
# SELECT id, name, email FROM users WHERE age > 18 AND status = 'active' ORDER BY name ASC LIMIT 10
TypeScript:
class HttpRequest {
method: string = "GET";
url: string = "";
headers: Record<string, string> = {};
body: string | null = null;
timeout: number = 30000;
}
class HttpRequestBuilder {
private request: HttpRequest;
constructor(url: string) {
this.request = new HttpRequest();
this.request.url = url;
}
method(m: string): this {
this.request.method = m;
return this;
}
header(key: string, value: string): this {
this.request.headers[key] = value;
return this;
}
body(data: object): this {
this.request.body = JSON.stringify(data);
this.request.headers["Content-Type"] = "application/json";
return this;
}
timeout(ms: number): this {
this.request.timeout = ms;
return this;
}
build(): HttpRequest {
return { ...this.request };
}
}
// ใช้งาน
const req = new HttpRequestBuilder("https://api.example.com/users")
.method("POST")
.header("Authorization", "Bearer token123")
.body({ name: "John", email: "john@example.com" })
.timeout(5000)
.build();
console.log(req);
ไม่ควรใช้เมื่อ: Object มี Parameter แค่ 2-3 ตัว — Constructor ธรรมดาก็เพียงพอ
5. Observer Pattern
เปรียบเทียบง่ายๆ: เหมือนช่อง YouTube — เมื่อคุณกด Subscribe ทุกครั้งที่ช่องลงวิดีโอใหม่ คุณจะได้รับแจ้งเตือนอัตโนมัติ ถ้ายกเลิก Subscribe ก็หยุดรับแจ้งเตือน โดยช่องไม่ต้องรู้จักผู้ Subscribe แต่ละคน
Observer Pattern สร้างกลไก Subscription ที่ให้หลาย Object (Observers) ติดตามการเปลี่ยนแปลงของ Object อื่น (Subject) เมื่อ Subject เปลี่ยนสถานะ Observer ทุกตัวจะได้รับการแจ้งเตือนอัตโนมัติ ใช้บ่อยมากใน Event System, UI Framework และ Reactive Programming
Python:
from abc import ABC, abstractmethod
from typing import List
class EventListener(ABC):
@abstractmethod
def update(self, event: str, data: dict) -> None:
pass
class EventEmitter:
def __init__(self):
self._listeners: dict[str, List[EventListener]] = {}
def on(self, event: str, listener: EventListener):
if event not in self._listeners:
self._listeners[event] = []
self._listeners[event].append(listener)
def off(self, event: str, listener: EventListener):
if event in self._listeners:
self._listeners[event].remove(listener)
def emit(self, event: str, data: dict = None):
for listener in self._listeners.get(event, []):
listener.update(event, data or {})
class EmailAlert(EventListener):
def update(self, event: str, data: dict):
print(f"Email Alert: {event} -> {data}")
class SlackNotifier(EventListener):
def update(self, event: str, data: dict):
print(f"Slack: {event} -> {data}")
class AuditLogger(EventListener):
def update(self, event: str, data: dict):
print(f"Audit Log: {event} -> {data}")
# ใช้งาน
order_system = EventEmitter()
order_system.on("order.created", EmailAlert())
order_system.on("order.created", SlackNotifier())
order_system.on("order.created", AuditLogger())
order_system.emit("order.created", {"order_id": 123, "amount": 1500})
# Email Alert: order.created -> {'order_id': 123, 'amount': 1500}
# Slack: order.created -> {'order_id': 123, 'amount': 1500}
# Audit Log: order.created -> {'order_id': 123, 'amount': 1500}
TypeScript:
type Listener = (data: any) => void;
class EventBus {
private events: Map<string, Listener[]> = new Map();
on(event: string, listener: Listener): void {
const listeners = this.events.get(event) || [];
listeners.push(listener);
this.events.set(event, listeners);
}
off(event: string, listener: Listener): void {
const listeners = this.events.get(event) || [];
this.events.set(event, listeners.filter(l => l !== listener));
}
emit(event: string, data?: any): void {
const listeners = this.events.get(event) || [];
listeners.forEach(listener => listener(data));
}
}
// ใช้งาน
const bus = new EventBus();
bus.on("user.login", (data) => console.log(`Welcome ${data.name}`));
bus.on("user.login", (data) => console.log(`Audit: ${data.name} logged in`));
bus.emit("user.login", { name: "Somchai" });
// Welcome Somchai
// Audit: Somchai logged in
ไม่ควรใช้เมื่อ: มี Observer แค่ตัวเดียวที่ไม่เปลี่ยน — เรียกตรงก็พอ ไม่ต้องสร้าง Event System
6. Strategy Pattern
เปรียบเทียบง่ายๆ: เหมือนแอป Google Maps ที่ให้เลือกวิธีเดินทาง — รถยนต์ รถไฟฟ้า จักรยาน หรือเดิน แต่ละวิธีมี Algorithm คำนวณเส้นทางต่างกัน แต่ผลลัพธ์คือ "บอกเส้นทางจาก A ไป B" เหมือนกัน
Strategy Pattern กำหนดกลุ่มของ Algorithm แยกแต่ละตัวเป็น Class ของตัวเอง และทำให้สามารถสลับ Algorithm ได้ตอน Runtime โดยไม่ต้องแก้โค้ดที่เรียกใช้ ช่วยกำจัด if-else ที่ยาวเหยียดและทำให้เพิ่ม Algorithm ใหม่ได้ง่าย
Python:
from abc import ABC, abstractmethod
class CompressionStrategy(ABC):
@abstractmethod
def compress(self, data: bytes) -> bytes:
pass
@abstractmethod
def name(self) -> str:
pass
class GzipCompression(CompressionStrategy):
def compress(self, data: bytes) -> bytes:
import gzip
return gzip.compress(data)
def name(self) -> str:
return "gzip"
class Bzip2Compression(CompressionStrategy):
def compress(self, data: bytes) -> bytes:
import bz2
return bz2.compress(data)
def name(self) -> str:
return "bzip2"
class NoCompression(CompressionStrategy):
def compress(self, data: bytes) -> bytes:
return data
def name(self) -> str:
return "none"
class FileProcessor:
def __init__(self, strategy: CompressionStrategy):
self._strategy = strategy
def set_strategy(self, strategy: CompressionStrategy):
self._strategy = strategy
def process(self, data: bytes) -> bytes:
print(f"Compressing with {self._strategy.name()}...")
result = self._strategy.compress(data)
ratio = len(result) / len(data) * 100
print(f" {len(data)} -> {len(result)} bytes ({ratio:.1f}%)")
return result
# ใช้งาน
processor = FileProcessor(GzipCompression())
data = b"Hello World! " * 1000
processor.process(data)
# เปลี่ยน Strategy ได้ตอน Runtime
processor.set_strategy(Bzip2Compression())
processor.process(data)
ไม่ควรใช้เมื่อ: Algorithm ไม่เคยเปลี่ยน หรือมีแค่วิธีเดียว — สร้าง Strategy Interface เป็น Over-engineering
7. Adapter Pattern
เปรียบเทียบง่ายๆ: เหมือนปลั๊กแปลงไฟ — เครื่องใช้ไฟฟ้าจากอเมริกาไม่สามารถเสียบกับปลั๊กไทยได้โดยตรง ต้องใช้ Adapter แปลงให้เข้ากันได้ โดยไม่ต้องเปลี่ยนเครื่องใช้ไฟฟ้าหรือปลั๊กผนัง
Adapter Pattern เชื่อมต่อ Interface ที่ไม่เข้ากัน ทำให้ Class ที่มี Interface ต่างกันสามารถทำงานร่วมกันได้ ใช้บ่อยเมื่อต้องใช้ Library ของคนอื่นที่มี Interface ไม่ตรงกับระบบเรา หรือเมื่อต้อง Integrate ระบบเก่ากับระบบใหม่
Python:
# Legacy XML system (ไม่สามารถแก้ได้)
class LegacyXMLParser:
def parse_xml(self, xml_string: str) -> dict:
# จำลองการ Parse XML
return {"format": "xml", "data": xml_string[:50]}
# ระบบใหม่ต้องการ JSON interface
class DataProcessor:
def process(self, parser, json_string: str):
result = parser.parse_json(json_string)
print(f"Processed: {result}")
# Adapter — ทำให้ XML Parser ใช้งานเหมือน JSON Parser ได้
class XMLtoJSONAdapter:
def __init__(self, xml_parser: LegacyXMLParser):
self._xml_parser = xml_parser
def parse_json(self, json_string: str) -> dict:
# แปลง JSON เป็น XML แล้วใช้ Legacy Parser
xml_result = self._xml_parser.parse_xml(json_string)
return {"format": "json_adapted", "original": xml_result}
# ใช้งาน
legacy = LegacyXMLParser()
adapter = XMLtoJSONAdapter(legacy)
processor = DataProcessor()
processor.process(adapter, '{"name": "test"}')
# Processed: {'format': 'json_adapted', 'original': {'format': 'xml', ...}}
TypeScript:
// Third-party payment library (แก้ไม่ได้)
class StripePaymentSDK {
createStripeCharge(amountInCents: number, currency: string): string {
return `Stripe charge: ${amountInCents} ${currency}`;
}
}
// ระบบเราใช้ Interface นี้
interface PaymentGateway {
charge(amountInBaht: number): string;
}
// Adapter
class StripeAdapter implements PaymentGateway {
private stripe: StripePaymentSDK;
constructor() {
this.stripe = new StripePaymentSDK();
}
charge(amountInBaht: number): string {
const cents = amountInBaht * 100;
return this.stripe.createStripeCharge(cents, "THB");
}
}
// ใช้งาน — ระบบเราไม่ต้องรู้จัก Stripe
const gateway: PaymentGateway = new StripeAdapter();
console.log(gateway.charge(1500)); // Stripe charge: 150000 THB
ไม่ควรใช้เมื่อ: สามารถแก้ไข Interface ของ Class ต้นทางได้โดยตรง — การแก้ตรงง่ายกว่าสร้าง Adapter
8. Decorator Pattern
เปรียบเทียบง่ายๆ: เหมือนการสั่งกาแฟที่ Starbucks — เริ่มจากกาแฟดำ แล้วเพิ่มนม เพิ่มน้ำตาล เพิ่มวิปครีม เพิ่มคาราเมล แต่ละอย่างเป็น "เลเยอร์" ที่ครอบกาแฟเดิม คุณเพิ่มหรือลดได้อย่างอิสระ
Decorator Pattern เพิ่มความสามารถให้ Object ที่มีอยู่แล้วโดยไม่ต้องแก้ไข Class เดิม ทำโดยการ "ห่อ" Object ด้วย Decorator ซ้อนทับกันเป็นชั้นๆ แต่ละ Decorator เพิ่มพฤติกรรมใหม่แล้วส่งต่อไปยัง Object ข้างใน
Python:
from abc import ABC, abstractmethod
class DataSource(ABC):
@abstractmethod
def write(self, data: str) -> str:
pass
@abstractmethod
def read(self) -> str:
pass
class FileDataSource(DataSource):
def __init__(self):
self._data = ""
def write(self, data: str) -> str:
self._data = data
return f"Written: {len(data)} chars"
def read(self) -> str:
return self._data
class DataSourceDecorator(DataSource):
def __init__(self, source: DataSource):
self._source = source
def write(self, data: str) -> str:
return self._source.write(data)
def read(self) -> str:
return self._source.read()
class EncryptionDecorator(DataSourceDecorator):
def write(self, data: str) -> str:
encrypted = data[::-1] # Simple reverse as encryption
print(f" Encrypting data...")
return super().write(encrypted)
def read(self) -> str:
data = super().read()
return data[::-1] # Decrypt
class CompressionDecorator(DataSourceDecorator):
def write(self, data: str) -> str:
compressed = data.replace(" ", " ")
print(f" Compressing data...")
return super().write(compressed)
class LoggingDecorator(DataSourceDecorator):
def write(self, data: str) -> str:
print(f" LOG: Writing {len(data)} chars at now")
return super().write(data)
# ใช้งาน — ซ้อน Decorator ได้ตามต้องการ
source = LoggingDecorator(
EncryptionDecorator(
CompressionDecorator(
FileDataSource()
)
)
)
source.write("Hello World! This is secret data.")
print(f"Read back: {source.read()}")
ไม่ควรใช้เมื่อ: ความสามารถเพิ่มเติมไม่เคยเปลี่ยน — สร้าง Subclass ธรรมดาง่ายกว่า
9. Command Pattern
เปรียบเทียบง่ายๆ: เหมือนการสั่งอาหารในร้านอาหาร — คุณไม่ได้ไปบอกพ่อครัวโดยตรง แต่เขียนออเดอร์ลงกระดาษ (Command Object) ส่งให้พนักงานเสิร์ฟ พนักงานเสิร์ฟนำออเดอร์ไปให้ครัว ออเดอร์สามารถเก็บไว้ ยกเลิก หรือทำซ้ำได้
Command Pattern ห่อ Request เป็น Object ทำให้สามารถส่งผ่าน Command เป็น Parameter จัดคิว ทำ Undo/Redo หรือบันทึก Log ได้ ใช้บ่อยในระบบ Undo ของ Text Editor, Transaction System และ Task Queue
Python:
from abc import ABC, abstractmethod
from typing import List
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
class TextEditor:
def __init__(self):
self.content = ""
def __str__(self):
return self.content or "(empty)"
class InsertTextCommand(Command):
def __init__(self, editor: TextEditor, text: str, position: int):
self.editor = editor
self.text = text
self.position = position
def execute(self) -> None:
self.editor.content = (
self.editor.content[:self.position]
+ self.text
+ self.editor.content[self.position:]
)
def undo(self) -> None:
self.editor.content = (
self.editor.content[:self.position]
+ self.editor.content[self.position + len(self.text):]
)
class DeleteTextCommand(Command):
def __init__(self, editor: TextEditor, position: int, length: int):
self.editor = editor
self.position = position
self.length = length
self.deleted_text = ""
def execute(self) -> None:
self.deleted_text = self.editor.content[self.position:self.position + self.length]
self.editor.content = (
self.editor.content[:self.position]
+ self.editor.content[self.position + self.length:]
)
def undo(self) -> None:
self.editor.content = (
self.editor.content[:self.position]
+ self.deleted_text
+ self.editor.content[self.position:]
)
class CommandHistory:
def __init__(self):
self._history: List[Command] = []
def execute(self, command: Command):
command.execute()
self._history.append(command)
def undo(self):
if self._history:
cmd = self._history.pop()
cmd.undo()
# ใช้งาน
editor = TextEditor()
history = CommandHistory()
history.execute(InsertTextCommand(editor, "Hello World", 0))
print(f"After insert: {editor}") # Hello World
history.execute(InsertTextCommand(editor, ", Developer", 11))
print(f"After insert: {editor}") # Hello World, Developer
history.undo()
print(f"After undo: {editor}") # Hello World
history.undo()
print(f"After undo: {editor}") # (empty)
ไม่ควรใช้เมื่อ: Action เป็นแบบ Fire-and-forget ที่ไม่ต้อง Undo และไม่ต้องจัดคิว
10. Template Method Pattern
เปรียบเทียบง่ายๆ: เหมือนกระบวนการสร้างบ้าน — ทุกบ้านต้องทำตามขั้นตอน: วางฐานราก สร้างโครงสร้าง ติดผนัง ติดหลังคา ตกแต่ง แต่วัสดุและรายละเอียดแต่ละขั้นตอนต่างกันตามแบบบ้าน กระบวนการโดยรวมเหมือนกัน แต่รายละเอียดแตกต่าง
Template Method Pattern กำหนดโครงร่างของ Algorithm ใน Base Class แล้วให้ Subclass Override เฉพาะบางขั้นตอนโดยไม่เปลี่ยนโครงสร้างรวม เหมาะกับกระบวนการที่มีขั้นตอนคงที่แต่รายละเอียดแต่ละขั้นต้องปรับเปลี่ยนได้
Python:
from abc import ABC, abstractmethod
class DataMiner(ABC):
# Template Method — กำหนดโครงร่างที่ไม่เปลี่ยน
def mine(self, path: str):
raw_data = self.extract(path)
parsed = self.parse(raw_data)
analyzed = self.analyze(parsed)
report = self.format_report(analyzed)
self.send_report(report)
@abstractmethod
def extract(self, path: str) -> str:
pass
@abstractmethod
def parse(self, data: str) -> list:
pass
def analyze(self, data: list) -> dict:
# Default implementation — Subclass override ได้
return {"count": len(data), "data": data}
def format_report(self, analysis: dict) -> str:
return f"Report: {analysis['count']} records processed"
def send_report(self, report: str):
print(f"Sending: {report}")
class CSVDataMiner(DataMiner):
def extract(self, path: str) -> str:
print(f"Reading CSV file: {path}")
return "name,age\nJohn,30\nJane,25"
def parse(self, data: str) -> list:
lines = data.strip().split("\n")
headers = lines[0].split(",")
return [dict(zip(headers, line.split(","))) for line in lines[1:]]
class JSONDataMiner(DataMiner):
def extract(self, path: str) -> str:
print(f"Reading JSON file: {path}")
return '[{"name":"John","age":30},{"name":"Jane","age":25}]'
def parse(self, data: str) -> list:
import json
return json.loads(data)
class APIDataMiner(DataMiner):
def extract(self, path: str) -> str:
print(f"Fetching API: {path}")
return '[{"name":"John","age":30}]'
def parse(self, data: str) -> list:
import json
return json.loads(data)
def analyze(self, data: list) -> dict:
result = super().analyze(data)
result["source"] = "api"
return result
# ใช้งาน
CSVDataMiner().mine("data.csv")
JSONDataMiner().mine("data.json")
APIDataMiner().mine("https://api.example.com/data")
ไม่ควรใช้เมื่อ: ขั้นตอนแต่ละ Class ต่างกันมากจนไม่มีโครงสร้างร่วม
Anti-Patterns — รูปแบบที่ควรหลีกเลี่ยง
การรู้จัก Anti-Patterns สำคัญพอๆ กับการรู้จัก Design Patterns เพราะช่วยให้คุณหลีกเลี่ยงกับดักที่พบบ่อยในการเขียนโค้ด
| Anti-Pattern | ปัญหา | วิธีแก้ |
|---|---|---|
| God Object | Class เดียวทำทุกอย่าง มี Method และ Property มากเกินไป | แยก Responsibility ตาม Single Responsibility Principle |
| Spaghetti Code | โค้ดพันกันยุ่งเหยิง ไม่มีโครงสร้าง ไม่มี Function แยก | ใช้ Design Patterns และ Refactor อย่างสม่ำเสมอ |
| Golden Hammer | ใช้ Pattern เดิมกับทุกปัญหา "เมื่อมีค้อน ทุกอย่างเป็นตะปู" | เลือก Pattern ตามปัญหา ไม่ใช่ตามความชอบ |
| Copy-Paste Programming | Copy โค้ดซ้ำแทนที่จะ Refactor เป็น Reusable Function | ใช้ Template Method, Strategy หรือ Abstract ออกมา |
| Premature Optimization | ปรับ Performance ก่อนที่จะรู้ว่ามีปัญหาจริง | Measure ก่อน Optimize |
| Singleton Abuse | ใช้ Singleton กับทุก Class ทำให้ Testing ยากและเกิด Hidden Dependencies | ใช้ Dependency Injection แทน ยกเว้นกรณีจำเป็นจริงๆ |
SOLID Principles กับ Design Patterns
SOLID Principles เป็นหลักการออกแบบ OOP ที่เป็นรากฐานของ Design Patterns ทั้งหมด การเข้าใจ SOLID จะทำให้เข้าใจว่าทำไม Pattern แต่ละตัวถึงออกแบบมาแบบนั้น
S — Single Responsibility Principle (SRP)
Class หนึ่งควรมีเหตุผลในการเปลี่ยนแปลงเพียงหนึ่งเดียว หรือพูดง่ายๆ คือ "ทำหน้าที่เดียว ทำให้ดีที่สุด" Pattern ที่เกี่ยว: Command, Observer ช่วยแยก Concern ออกจากกัน ทำให้แต่ละ Class มีหน้าที่ชัดเจน
O — Open/Closed Principle (OCP)
Class ควรเปิดสำหรับการขยาย (Extension) แต่ปิดสำหรับการแก้ไข (Modification) คือเพิ่มความสามารถใหม่ได้โดยไม่ต้องแก้โค้ดเดิม Pattern ที่เกี่ยว: Strategy, Decorator ช่วยเพิ่มพฤติกรรมใหม่โดยไม่แก้ Class เดิม
L — Liskov Substitution Principle (LSP)
Object ของ Subclass ต้องสามารถใช้แทน Object ของ Parent Class ได้โดยไม่ทำให้โปรแกรมพัง Pattern ที่เกี่ยว: Factory Method, Template Method ต้องมั่นใจว่า Subclass ทำงานได้เหมือน Parent
I — Interface Segregation Principle (ISP)
อย่าบังคับ Client ให้พึ่งพา Interface ที่ไม่ได้ใช้ ควรแยก Interface ใหญ่เป็น Interface เล็กๆ หลายตัว Pattern ที่เกี่ยว: Adapter ช่วยแปลง Interface ที่ใหญ่เกินให้เหลือเฉพาะที่ต้องใช้
D — Dependency Inversion Principle (DIP)
Module ระดับสูงไม่ควรพึ่งพา Module ระดับต่ำโดยตรง ทั้งคู่ควรพึ่งพา Abstraction เช่น Interface หรือ Abstract Class Pattern ที่เกี่ยว: Abstract Factory, Strategy ทำงานผ่าน Interface ไม่ใช่ Concrete Class
Design Patterns ใน Framework ยอดนิยม
Framework ที่ Developer ใช้ทุกวันล้วนสร้างบน Design Patterns การรู้ Pattern ช่วยให้เข้าใจ Framework ลึกขึ้น
React (Frontend)
- Observer Pattern: useState Hook และ State Management (Redux, Zustand) ใช้หลัก Observer — เมื่อ State เปลี่ยน Component ที่ Subscribe อยู่จะ Re-render อัตโนมัติ
- Decorator Pattern: Higher-Order Components (HOC) เช่น
withRouter(),connect()ห่อ Component เดิมด้วยความสามารถใหม่ - Strategy Pattern: Custom Hooks เช่น
useFetch,useAuthที่เปลี่ยน Logic ได้โดยไม่แก้ Component - Factory Pattern:
React.createElement()สร้าง Element จาก Type ที่ส่งเข้ามา
Django (Python Web Framework)
- Template Method: Class-Based Views เช่น
ListView,CreateViewกำหนดโครงสร้างไว้แล้ว ให้ Developer Override เฉพาะบางส่วน - Observer Pattern: Django Signals เช่น
post_save,pre_deleteแจ้งเตือนเมื่อ Model มีการเปลี่ยนแปลง - Decorator Pattern: Middleware ห่อ Request/Response Pipeline เพิ่มความสามารถเช่น Authentication, CORS, Logging
Spring (Java Framework)
- Singleton: Spring Container สร้าง Bean เป็น Singleton โดย Default
- Factory Method:
BeanFactoryสร้าง Object ตาม Configuration - Template Method:
JdbcTemplate,RestTemplateกำหนดโครงสร้างการเรียก Database/API ให้แล้ว - Observer:
ApplicationEventPublisherและ@EventListenerสำหรับ Event-driven Architecture
แนวทางเลือกใช้ Design Pattern
การเลือก Design Pattern ที่เหมาะสมเป็นทักษะที่ต้องฝึกฝน ไม่ใช่ท่องจำ ต่อไปนี้คือแนวทางที่จะช่วยให้ตัดสินใจได้ง่ายขึ้น:
| สถานการณ์ | Pattern ที่เหมาะ |
|---|---|
| ต้องการ Instance เดียวทั้งระบบ | Singleton |
| สร้าง Object หลายประเภทจาก Input | Factory Method |
| สร้างกลุ่ม Object ที่ต้องเข้ากัน | Abstract Factory |
| Object มี Configuration ซับซ้อน | Builder |
| แจ้งเตือนหลาย Object เมื่อเกิดเหตุการณ์ | Observer |
| เปลี่ยน Algorithm ตอน Runtime | Strategy |
| เชื่อมต่อ Interface ที่ไม่เข้ากัน | Adapter |
| เพิ่มความสามารถแบบยืดหยุ่น | Decorator |
| ต้องการ Undo/Redo หรือ Task Queue | Command |
| กระบวนการเดียวกัน รายละเอียดต่างกัน | Template Method |
สรุป
Design Patterns เป็นเครื่องมือสำคัญในคลังอาวุธของ Developer ทุกคน ไม่ใช่กฎตายตัวที่ต้องทำตาม แต่เป็นแนวทางที่พิสูจน์แล้วว่าใช้งานได้จริง การเข้าใจ 10 Pattern ที่สอนในบทความนี้ ได้แก่ Singleton, Factory Method, Abstract Factory, Builder, Observer, Strategy, Adapter, Decorator, Command และ Template Method จะช่วยให้คุณออกแบบซอฟต์แวร์ที่ดูแลง่าย ขยายได้ง่าย และทำงานร่วมกับทีมได้อย่างมีประสิทธิภาพ
สิ่งสำคัญที่สุดคือการฝึกใช้จริง ลองนำ Pattern ไปใช้ในโปรเจกต์ส่วนตัว อ่านโค้ดของ Open Source Framework และสังเกตว่าพวกเขาใช้ Pattern ไหนบ้าง เมื่อเวลาผ่านไป คุณจะเลือก Pattern ได้อย่างเป็นธรรมชาติโดยไม่ต้องคิดนาน เพราะมันจะกลายเป็นส่วนหนึ่งของวิธีคิดในการแก้ปัญหาซอฟต์แวร์ของคุณ
