Design Patterns เบื้องต้น

บทนำ: ปัญหาซ้ำๆ ต้องแก้วิธีเดิมๆ

ในระหว่างการเขียนโปรแกรม เราต่างหน้าสำหรับ ปัญหาบางอย่างที่เกิดขึ้นบ่อย เช่น:

  • “จะสร้าง object ประเภทต่างๆ อย่างไร?”
  • “จะให้มี object เดียวทั่วทั้งระบบได้อย่างไร?”
  • “จะเปลี่ยนพฤติกรรมของ program ได้ง่ายๆ ได้อย่างไร?”

Design Patterns คือ สูตรแบบทั่วไป (templates) ที่มีคนค้นพบแล้วว่าแก้ปัญหาเหล่านี้ได้ดี ในส่วนนี้เราจะเรียนรู้ 3 pattern ที่พื้นฐานและใช้บ่อยที่สุด


1. Factory Pattern: สร้าง Object หลายชนิด

ปัญหา: จะสร้าง Object แบบเลือกได้ยังไง?

ลองนึกว่า คุณมี interface Transport และหลาย implementation (CarBikeTruck) คุณต้องการให้ code ที่ใช้ Transport ไม่ต้องรู้จักว่า “วันนี้จะใช้ Car หรือ Truck”

textปัญหา:
└─ ถ้า client หลายๆ ตัวต้องการ interface เดิมแต่ implementation ต่างกัน
└─ ทำยังไง?

✓ วิธีแก้: Factory Pattern

Factory Pattern = สร้าง “โรงงาน (factory)” ที่รับคำขอว่า “ผมต้อง object ประเภท X” แล้ว return object นั้นให้

ตัวอย่างที่ 1: Transport Factory

java// ==== STEP 1: Interface/Abstraction ====
public interface Transport {
    void deliver();
}

// ==== STEP 2: Implementations ====
public class Car implements Transport {
    @Override
    public void deliver() {
        System.out.println("🚗 ส่งสินค้าโดยรถยนต์");
    }
}

public class Bike implements Transport {
    @Override
    public void deliver() {
        System.out.println("🏍️ ส่งสินค้าโดยมอเตอร์ไซค์");
    }
}

public class Truck implements Transport {
    @Override
    public void deliver() {
        System.out.println("🚚 ส่งสินค้าโดยรถบรรทุก");
    }
}

// ==== STEP 3: Factory ====
public class TransportFactory {
    public static Transport createTransport(String type) {
        switch(type.toLowerCase()) {
            case "car":
                return new Car();
            case "bike":
                return new Bike();
            case "truck":
                return new Truck();
            default:
                throw new IllegalArgumentException("ประเภท: " + type + " ไม่รู้จัก");
        }
    }
}

// ==== STEP 4: Client ====
public class DeliveryService {
    public void shipOrder(String transportType) {
        Transport transport = TransportFactory.createTransport(transportType);
        transport.deliver();
    }
}

// ==== การใช้งาน ====
public class Main {
    public static void main(String[] args) {
        DeliveryService service = new DeliveryService();
        
        service.shipOrder("car");
        service.shipOrder("bike");
        service.shipOrder("truck");
    }
}

Output:

text🚗 ส่งสินค้าโดยรถยนต์
🏍️ ส่งสินค้าโดยมอเตอร์ไซค์
🚚 ส่งสินค้าโดยรถบรรทุก

คำอธิบาย:

  • TransportFactory.createTransport() เป็น “โรงงาน” ที่รับคำขอ string แล้ว return object ที่เหมาะสม
  • DeliveryService ไม่ต้องรู้จักว่า Car, Bike, Truck เป็นอะไร มันเพียงขอจาก factory
  • เพิ่ม transport type ใหม่ (เช่น Airplane) แค่เพิ่ม case ใหม่ใน factory

ตัวอย่างที่ 2: Database Connection Factory

java// ==== Interface ====
public interface DatabaseConnection {
    void connect();
    void executeQuery(String sql);
}

// ==== Implementations ====
public class MySQLConnection implements DatabaseConnection {
    @Override
    public void connect() {
        System.out.println("🔗 เชื่อมต่อ MySQL");
    }
    
    @Override
    public void executeQuery(String sql) {
        System.out.println("📊 รัน MySQL query: " + sql);
    }
}

public class PostgresConnection implements DatabaseConnection {
    @Override
    public void connect() {
        System.out.println("🔗 เชื่อมต่อ PostgreSQL");
    }
    
    @Override
    public void executeQuery(String sql) {
        System.out.println("📊 รัน PostgreSQL query: " + sql);
    }
}

public class MongoDBConnection implements DatabaseConnection {
    @Override
    public void connect() {
        System.out.println("🔗 เชื่อมต่อ MongoDB");
    }
    
    @Override
    public void executeQuery(String sql) {
        System.out.println("📊 รัน MongoDB query: " + sql);
    }
}

// ==== Factory ====
public class DatabaseFactory {
    public static DatabaseConnection create(String databaseType) {
        switch(databaseType.toLowerCase()) {
            case "mysql":
                return new MySQLConnection();
            case "postgres":
                return new PostgresConnection();
            case "mongodb":
                return new MongoDBConnection();
            default:
                throw new IllegalArgumentException("Database ไม่รู้จัก: " + databaseType);
        }
    }
}

// ==== การใช้งาน ====
public class UserRepository {
    private DatabaseConnection db;
    
    public UserRepository(String databaseType) {
        this.db = DatabaseFactory.create(databaseType);
    }
    
    public void saveUser(String userName) {
        db.connect();
        db.executeQuery("INSERT INTO users VALUES ('" + userName + "')");
    }
}

public class Main {
    public static void main(String[] args) {
        UserRepository repo1 = new UserRepository("mysql");
        repo1.saveUser("john");
        
        System.out.println();
        
        UserRepository repo2 = new UserRepository("mongodb");
        repo2.saveUser("jane");
    }
}

Output:

text🔗 เชื่อมต่อ MySQL
📊 รัน MySQL query: INSERT INTO users VALUES ('john')

🔗 เชื่อมต่อ MongoDB
📊 รัน MongoDB query: INSERT INTO users VALUES ('jane')

2. Singleton Pattern: Object เดียวทั่วระบบ

ปัญหา: ต้องการ object เดียวแค่ตัวเดียว

บางครั้ง เราต้องการให้มี object เพียงตัวเดียว ทั่วทั้งระบบ เช่น:

  • Database connection (ต้องเดียวตัว)
  • Logger (ต้องเดียวตัว)
  • Configuration manager (ต้องเดียวตัว)

✓ วิธีแก้: Singleton Pattern

Singleton Pattern = สร้าง class ที่ สามารถสร้าง object ได้เพียงตัวเดียว ไม่ว่าจะ new กี่ครั้ง

ตัวอย่างที่ 3: Logger Singleton

java// ==== Singleton Logger ====
public class Logger {
    // Static instance
    private static Logger instance;
    
    // Private constructor เพื่อไม่ให้ new ได้ตรงๆ
    private Logger() {
    }
    
    // Static method เพื่อรับ instance เดียวตัว
    public static Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
    
    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

// ==== การใช้งาน ====
public class ServiceA {
    public void doSomething() {
        Logger logger = Logger.getInstance();
        logger.log("ServiceA ทำงาน");
    }
}

public class ServiceB {
    public void doSomething() {
        Logger logger = Logger.getInstance();
        logger.log("ServiceB ทำงาน");
    }
}

public class Main {
    public static void main(String[] args) {
        ServiceA serviceA = new ServiceA();
        ServiceB serviceB = new ServiceB();
        
        serviceA.doSomething();
        serviceB.doSomething();
        
        // ✓ ตรวจสอบว่า instance เดียวตัว
        Logger log1 = Logger.getInstance();
        Logger log2 = Logger.getInstance();
        System.out.println("log1 == log2? " + (log1 == log2));  // true
    }
}

Output:

text[LOG] ServiceA ทำงาน
[LOG] ServiceB ทำงาน
log1 == log2? true

คำอธิบาย:

  • getInstance() ตรวจสอบว่า instance มีอยู่แล้วหรือไม่
  • ถ้าไม่มี สร้างตัวเดียว
  • ถ้ามีแล้ว return ตัวที่มีอยู่
  • ไม่ว่าจะเรียกจากไหน ได้ object เดียวตัว

ตัวอย่างที่ 4: Database Connection Singleton

java// ==== Singleton Database Connection ====
public class DatabaseConnection {
    private static DatabaseConnection instance;
    private String connectionString;
    
    private DatabaseConnection() {
        this.connectionString = "mysql://localhost/myapp";
        System.out.println("🔗 เชื่อมต่อฐานข้อมูล: " + connectionString);
    }
    
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
        }
        return instance;
    }
    
    public void query(String sql) {
        System.out.println("📊 รัน: " + sql);
    }
}

// ==== การใช้งาน ====
public class UserDAO {
    public void getUser(int id) {
        DatabaseConnection db = DatabaseConnection.getInstance();
        db.query("SELECT * FROM users WHERE id = " + id);
    }
}

public class ProductDAO {
    public void getProduct(int id) {
        DatabaseConnection db = DatabaseConnection.getInstance();
        db.query("SELECT * FROM products WHERE id = " + id);
    }
}

public class Main {
    public static void main(String[] args) {
        UserDAO userDAO = new UserDAO();
        ProductDAO productDAO = new ProductDAO();
        
        userDAO.getUser(1);      // เชื่อมต่อครั้งเดียว (สร้าง instance)
        productDAO.getProduct(1); // ใช้ instance เดิม (ไม่สร้างใหม่)
    }
}

Output:

text🔗 เชื่อมต่อฐานข้อมูล: mysql://localhost/myapp
📊 รัน: SELECT * FROM users WHERE id = 1
📊 รัน: SELECT * FROM products WHERE id = 1

ข้อดี:

  • แม้ UserDAO และ ProductDAO ต่างกเรียก getInstance() แต่ได้ connection เดียวตัว
  • ประหยัด resource (connection)
  • ปลอดภัยมากขึ้น

3. Strategy Pattern: สลับพฤติกรรมได้ Runtime

ปัญหา: วิธีการต่างๆ ต้องขึ้นอยู่กับสถานการณ์

บางครั้ง algorithm หรือ strategy ต้อง “สลับได้” ตามสถานการณ์ เช่น:

  • วิธีการเรียงลำดับ (ขึ้นอยู่กับข้อมูล)
  • วิธีการส่ง (ขึ้นอยู่กับที่ปลายทาง)
  • วิธีการคิดราคา (ขึ้นอยู่กับประเภท customer)

✓ วิธีแก้: Strategy Pattern

Strategy Pattern = สร้าง “เทพพอลี่” (family) ของ algorithms และให้ client เลือกได้

ตัวอย่างที่ 5: Sorting Strategy

java// ==== Interface: Strategy ====
public interface SortStrategy {
    void sort(int[] array);
}

// ==== Implementation 1: Bubble Sort ====
public class BubbleSort implements SortStrategy {
    @Override
    public void sort(int[] array) {
        System.out.println("🫧 ใช้ Bubble Sort");
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array.length - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    int temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                }
            }
        }
        System.out.println("ผลลัพธ์: " + java.util.Arrays.toString(array));
    }
}

// ==== Implementation 2: Quick Sort ====
public class QuickSort implements SortStrategy {
    @Override
    public void sort(int[] array) {
        System.out.println("⚡ ใช้ Quick Sort");
        // Simplified
        java.util.Arrays.sort(array);
        System.out.println("ผลลัพธ์: " + java.util.Arrays.toString(array));
    }
}

// ==== Implementation 3: Merge Sort ====
public class MergeSort implements SortStrategy {
    @Override
    public void sort(int[] array) {
        System.out.println("🔀 ใช้ Merge Sort");
        // Simplified
        java.util.Arrays.sort(array);
        System.out.println("ผลลัพธ์: " + java.util.Arrays.toString(array));
    }
}

// ==== Context: ใช้ Strategy ====
public class Sorter {
    private SortStrategy strategy;
    
    public Sorter(SortStrategy strategy) {
        this.strategy = strategy;
    }
    
    // สามารถสลับ strategy ได้
    public void setStrategy(SortStrategy strategy) {
        this.strategy = strategy;
    }
    
    public void performSort(int[] array) {
        strategy.sort(array);
    }
}

// ==== การใช้งาน ====
public class Main {
    public static void main(String[] args) {
        int[] data = {64, 34, 25, 12, 22, 11, 90};
        
        // ใช้ Bubble Sort
        Sorter sorter = new Sorter(new BubbleSort());
        sorter.performSort(data);
        
        // เปลี่ยนเป็น Quick Sort ขณะ runtime
        sorter.setStrategy(new QuickSort());
        sorter.performSort(data);
        
        // เปลี่ยนเป็น Merge Sort
        sorter.setStrategy(new MergeSort());
        sorter.performSort(data);
    }
}

Output:

text🫧 ใช้ Bubble Sort
ผลลัพธ์: [11, 12, 22, 25, 34, 64, 90]
⚡ ใช้ Quick Sort
ผลลัพธ์: [11, 12, 22, 25, 34, 64, 90]
🔀 ใช้ Merge Sort
ผลลัพธ์: [11, 12, 22, 25, 34, 64, 90]

คำอธิบาย:

  • SortStrategy คือ interface ที่นิยาม strategy
  • BubbleSortQuickSortMergeSort คือ strategy ต่างๆ
  • Sorter เป็น context ที่ใช้ strategy
  • สามารถเปลี่ยน strategy ขณะ runtime ด้วย setStrategy()

ตัวอย่างที่ 6: Payment Strategy

java// ==== Interface ====
public interface PaymentStrategy {
    void pay(double amount);
}

// ==== Implementation 1: Credit Card ====
public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("💳 ชำระด้วยบัตรเครดิต: " + amount + " บาท");
    }
}

// ==== Implementation 2: Bank Transfer ====
public class BankTransferPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("🏦 โอนเงินธนาคาร: " + amount + " บาท");
    }
}

// ==== Implementation 3: E-Wallet ====
public class EWalletPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
        System.out.println("💰 ชำระ E-Wallet: " + amount + " บาท");
    }
}

// ==== Context ====
public class ShoppingCart {
    private PaymentStrategy paymentStrategy;
    private double totalPrice;
    
    public ShoppingCart(double price) {
        this.totalPrice = price;
    }
    
    public void setPaymentMethod(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }
    
    public void checkout() {
        if (paymentStrategy == null) {
            System.out.println("❌ ยังไม่เลือกวิธีชำระเงิน");
            return;
        }
        paymentStrategy.pay(totalPrice);
    }
}

// ==== การใช้งาน ====
public class Main {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart(1000);
        
        // ลูกค้าเลือกชำระด้วยบัตรเครดิต
        System.out.println("--- วิธีที่ 1 ---");
        cart.setPaymentMethod(new CreditCardPayment());
        cart.checkout();
        
        // เปลี่ยนใจ เลือกโอนเงินธนาคาร
        System.out.println("\n--- วิธีที่ 2 ---");
        cart.setPaymentMethod(new BankTransferPayment());
        cart.checkout();
        
        // เปลี่ยนใจอีก เลือก E-Wallet
        System.out.println("\n--- วิธีที่ 3 ---");
        cart.setPaymentMethod(new EWalletPayment());
        cart.checkout();
    }
}

Output:

text--- วิธีที่ 1 ---
💳 ชำระด้วยบัตรเครดิต: 1000.0 บาท

--- วิธีที่ 2 ---
🏦 โอนเงินธนาคาร: 1000.0 บาท

--- วิธีที่ 3 ---
💰 ชำระ E-Wallet: 1000.0 บาท

ข้อดี:

  • ShoppingCart ไม่ต้องรู้ว่า payment method ไหนทำงาน
  • ลูกค้าสามารถเปลี่ยนวิธีชำระเงินได้ทุกเวลา
  • เพิ่ม payment method ใหม่ง่ายๆ

ตารางเปรียบเทียบ 3 Patterns

Patternปัญหาวิธีแก้ตัวอย่าง
Factoryต้องสร้าง object ประเภทต่างๆสร้าง factory ที่ return objectTransport ต่างชนิด
Singletonต้องการ object เดียวตัวทำให้ constructor private ให้เพียง getInstance()Database connection
StrategyAlgorithm ต้องเปลี่ยนได้สร้าง family ของ algorithms ให้เลือกPayment method ต่างชนิด

สรุป

Design Patterns เป็นคำแนะนำที่เหมาะสมแล้วสำหรับ ปัญหาทั่วไปในการเขียน OOP

  1. Factory Pattern – เมื่อต้องการสร้าง object หลายชนิด โดยไม่ให้ client รู้ละเอียด
  2. Singleton Pattern – เมื่อต้องการให้มี object เพียงตัวเดียวทั่วทั้งระบบ
  3. Strategy Pattern – เมื่อต้องให้ client เลือก algorithm หรือวิธีการที่แตกต่างกันได้

การใช้ design patterns ให้เหมาะสมจะทำให้ code ของเรา:

  • ยืดหยุ่น – เปลี่ยนแปลงได้ง่าย
  • ปลอดภัย – ลดข้อผิดพลาด
  • ดูแลรักษาได้ง่าย – คนอื่นเข้าใจได้
  • scalable – ขยายระบบได้ง่าย

จำไว้ว่า patterns ไม่ใช่กฎข้อบังคับ แต่เป็น “ข้อแนะนำปกติ” ที่คนก่อนหน้าค้นพบแล้วว่าสัก effective ในการแก้ปัญหา ใช้ให้สมควร ไม่ใช้ฝ่ายบังคับ