Review Patterns ที่ใช้บ่อยในไทย: Factory, Strategy, Observer, Repository

บทนำ: ทำไมต้อง Design Patterns?

เมื่อเขียนโปรแกรม คุณอาจเจอปัญหา “ยังไง” ซ้ำๆ:

❓ ปัญหา 1: "ไม่รู้ต้องสร้าง object ไหนตอน runtime"
❓ ปัญหา 2: "ต้องเปลี่ยน algorithm บ่อยตามเงื่อนไข"
❓ ปัญหา 3: "ต้องบอก objects อื่นว่ามีอะไรเปลี่ยน"
❓ ปัญหา 4: "Database access code ซ้ำกันทั่ว project"

Design Patterns = Proven solutions สำหรับปัญหา common ในโปรแกรมมิ่ง


Pattern 1: Factory Pattern

ปัญหา: Object Creation Complexity

java// ❌ ปัญหา: Tight coupling, hard to maintain
public class ReportGenerator {
    public void generateReport(String type) {
        if (type.equals("pdf")) {
            PDFReport report = new PDFReport();
            report.generate();
        } else if (type.equals("excel")) {
            ExcelReport report = new ExcelReport();
            report.generate();
        } else if (type.equals("csv")) {
            CSVReport report = new CSVReport();
            report.generate();
        }
        // ← ถ้าต้องเพิ่ม report type ต้อง modify method นี้
        // ← ทำให้ dependency ยุ่งมาก
    }
}

วิธีแก้: Factory Pattern

java// ==== Interface ====
public interface Report {
    void generate();
    void export(String filename);
}

// ==== Concrete Implementations ====
public class PDFReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating PDF report...");
    }
    
    @Override
    public void export(String filename) {
        System.out.println("Exporting to: " + filename + ".pdf");
    }
}

public class ExcelReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating Excel report...");
    }
    
    @Override
    public void export(String filename) {
        System.out.println("Exporting to: " + filename + ".xlsx");
    }
}

public class CSVReport implements Report {
    @Override
    public void generate() {
        System.out.println("Generating CSV report...");
    }
    
    @Override
    public void export(String filename) {
        System.out.println("Exporting to: " + filename + ".csv");
    }
}

// ==== Factory ====
/**
 * ReportFactory: centralized object creation
 * ← Single Responsibility: การสร้าง objects
 */
public class ReportFactory {
    
    /**
     * Create report based on type
     * @param type report type (pdf, excel, csv)
     * @return appropriate Report implementation
     */
    public static Report createReport(String type) {
        switch (type.toLowerCase()) {
            case "pdf":
                return new PDFReport();
            case "excel":
                return new ExcelReport();
            case "csv":
                return new CSVReport();
            default:
                throw new IllegalArgumentException("Unknown report type: " + type);
        }
    }
}

// ==== Usage ====
public class ReportGeneratorImproved {
    
    public void generateReport(String type) {
        // ← ใช้ factory เพื่อ create object
        Report report = ReportFactory.createReport(type);
        report.generate();
        report.export("output_report");
    }
}

public class FactoryPatternExample {
    public static void main(String[] args) {
        ReportGeneratorImproved generator = new ReportGeneratorImproved();
        
        System.out.println("=== Generate Reports ===");
        generator.generateReport("pdf");
        generator.generateReport("excel");
        generator.generateReport("csv");
        
        // ← หากต้องเพิ่ม report type ใหม่:
        // 1. Create new class implement Report
        // 2. Add case ใน ReportFactory เท่านั้น
        // 3. Code อื่นไม่ต้องแก้ไข (Open/Closed Principle)
    }
}

// Output:
// === Generate Reports ===
// Generating PDF report...
// Exporting to: output_report.pdf
// Generating Excel report...
// Exporting to: output_report.xlsx
// Generating CSV report...
// Exporting to: output_report.csv

ประโยชน์ของ Factory Pattern:

  • Decoupling – ระหว่าง creator กับ concrete classes
  • Flexibility – เพิ่ม types ได้โดยไม่ modify existing code
  • Single Responsibility – factory ดูแล creation logic เท่านั้น

Pattern 2: Strategy Pattern

ปัญหา: Algorithm ต้องเปลี่ยนตามเงื่อนไข

java// ❌ ปัญหา: Many if-else, hard to test
public class PaymentProcessor {
    
    public void processPayment(String method, double amount) {
        if (method.equals("creditCard")) {
            // Validate credit card
            // Charge credit card
            // Record transaction
            System.out.println("Processing credit card payment...");
        } else if (method.equals("bankTransfer")) {
            // Validate bank account
            // Initiate transfer
            // Wait for confirmation
            System.out.println("Processing bank transfer...");
        } else if (method.equals("eWallet")) {
            // Validate e-wallet
            // Deduct balance
            // Update wallet
            System.out.println("Processing e-wallet payment...");
        }
        // ← ยาว ยุ่ง ยากต่อการ test
    }
}

วิธีแก้: Strategy Pattern

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

// ==== Concrete Strategies ====
public class CreditCardStrategy implements PaymentStrategy {
    private String cardNumber;
    
    public CreditCardStrategy(String cardNumber) {
        this.cardNumber = cardNumber;
    }
    
    @Override
    public void validate() {
        System.out.println("✓ Validating credit card: " + cardNumber);
    }
    
    @Override
    public void pay(double amount) {
        System.out.println("✓ Charging card: $" + amount);
    }
    
    @Override
    public void recordTransaction() {
        System.out.println("✓ Recording credit card transaction");
    }
}

public class BankTransferStrategy implements PaymentStrategy {
    private String bankAccount;
    
    public BankTransferStrategy(String bankAccount) {
        this.bankAccount = bankAccount;
    }
    
    @Override
    public void validate() {
        System.out.println("✓ Validating bank account: " + bankAccount);
    }
    
    @Override
    public void pay(double amount) {
        System.out.println("✓ Initiating transfer: $" + amount);
    }
    
    @Override
    public void recordTransaction() {
        System.out.println("✓ Recording bank transfer");
    }
}

public class EWalletStrategy implements PaymentStrategy {
    private String walletId;
    
    public EWalletStrategy(String walletId) {
        this.walletId = walletId;
    }
    
    @Override
    public void validate() {
        System.out.println("✓ Validating e-wallet: " + walletId);
    }
    
    @Override
    public void pay(double amount) {
        System.out.println("✓ Deducting from wallet: $" + amount);
    }
    
    @Override
    public void recordTransaction() {
        System.out.println("✓ Recording e-wallet transaction");
    }
}

// ==== Context ====
/**
 * PaymentProcessor: Uses strategy
 * ← No if-else! Strategy is pluggable
 */
public class PaymentProcessorImproved {
    private PaymentStrategy strategy;
    
    // Setter for strategy (Dependency Injection)
    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }
    
    public void processPayment(double amount) {
        if (strategy == null) {
            throw new IllegalStateException("Payment strategy not set");
        }
        
        strategy.validate();
        strategy.pay(amount);
        strategy.recordTransaction();
    }
}

// ==== Usage ====
public class StrategyPatternExample {
    public static void main(String[] args) {
        PaymentProcessorImproved processor = new PaymentProcessorImproved();
        
        System.out.println("=== Credit Card Payment ===");
        processor.setPaymentStrategy(new CreditCardStrategy("1234-5678-9012-3456"));
        processor.processPayment(99.99);
        
        System.out.println("\n=== Bank Transfer ===");
        processor.setPaymentStrategy(new BankTransferStrategy("ACC-123456"));
        processor.processPayment(500.00);
        
        System.out.println("\n=== E-Wallet Payment ===");
        processor.setPaymentStrategy(new EWalletStrategy("wallet_12345"));
        processor.processPayment(49.99);
    }
}

// Output:
// === Credit Card Payment ===
// ✓ Validating credit card: 1234-5678-9012-3456
// ✓ Charging card: $99.99
// ✓ Recording credit card transaction
// 
// === Bank Transfer ===
// ✓ Validating bank account: ACC-123456
// ✓ Initiating transfer: $500.0
// ✓ Recording bank transfer
// 
// === E-Wallet Payment ===
// ✓ Validating e-wallet: wallet_12345
// ✓ Deducting from wallet: $49.99
// ✓ Recording e-wallet transaction

ประโยชน์ของ Strategy Pattern:

  • No if-else – Algorithm ต่างกันขาดจากกัน
  • Runtime Flexibility – เปลี่ยน strategy ได้ทันที
  • Testability – Test แต่ละ strategy แยกกัน
  • Easy to Extend – เพิ่ม strategies ใหม่ง่าย

Pattern 3: Observer Pattern

ปัญหา: Tight Coupling เมื่อ Notify Objects

java// ❌ ปัญหา: Order ต้อง know ทุก classes ที่ต้องอัพเดท
public class Order {
    private String status;
    
    public void changeStatus(String newStatus) {
        this.status = newStatus;
        
        // ← Order ต้อง notify ทุก related objects
        // Tight coupling!
        EmailService emailService = new EmailService();
        emailService.sendNotification("Order status: " + status);
        
        InventoryService inventoryService = new InventoryService();
        inventoryService.updateStock(this);
        
        LogService logService = new LogService();
        logService.logOrderChange(this);
        
        // ← ถ้าต้องเพิ่ม observer ต้อง modify Order class
    }
}

วิธีแก้: Observer Pattern

java// ==== Observer Interface ====
public interface OrderObserver {
    void update(Order order);
}

// ==== Concrete Observers ====
public class EmailObserver implements OrderObserver {
    @Override
    public void update(Order order) {
        System.out.println("📧 Email: Sending notification for order " + 
            order.getId() + " - Status: " + order.getStatus());
    }
}

public class InventoryObserver implements OrderObserver {
    @Override
    public void update(Order order) {
        System.out.println("📦 Inventory: Updating stock for order " + 
            order.getId());
    }
}

public class LogObserver implements OrderObserver {
    @Override
    public void update(Order order) {
        System.out.println("📝 Log: Recording order change - ID: " + 
            order.getId() + ", Status: " + order.getStatus());
    }
}

public class AnalyticsObserver implements OrderObserver {
    @Override
    public void update(Order order) {
        System.out.println("📊 Analytics: Tracking order " + 
            order.getId() + " status change");
    }
}

// ==== Subject (Observable) ====
/**
 * Order: maintains list of observers
 * ← Order ไม่รู้ใครคือ observers
 */
public class Order {
    private String id;
    private String status;
    private List<OrderObserver> observers = new ArrayList<>();
    
    public Order(String id) {
        this.id = id;
        this.status = "CREATED";
    }
    
    public String getId() { return id; }
    public String getStatus() { return status; }
    
    // Observer management
    public void attach(OrderObserver observer) {
        observers.add(observer);
        System.out.println("✓ Observer attached");
    }
    
    public void detach(OrderObserver observer) {
        observers.remove(observer);
        System.out.println("✓ Observer detached");
    }
    
    public void notifyObservers() {
        for (OrderObserver observer : observers) {
            observer.update(this);
        }
    }
    
    // Business logic
    public void changeStatus(String newStatus) {
        this.status = newStatus;
        System.out.println("➡️ Order status changed to: " + status);
        notifyObservers();  // ← Notify all observers
    }
}

// ==== Usage ====
public class ObserverPatternExample {
    public static void main(String[] args) {
        Order order = new Order("ORD-001");
        
        // Attach observers
        System.out.println("=== Attaching Observers ===");
        order.attach(new EmailObserver());
        order.attach(new InventoryObserver());
        order.attach(new LogObserver());
        order.attach(new AnalyticsObserver());
        
        // Change order status (notifies all observers)
        System.out.println("\n=== Order Status Change ===");
        order.changeStatus("PROCESSING");
        
        System.out.println("\n=== Another Status Change ===");
        order.changeStatus("SHIPPED");
    }
}

// Output:
// === Attaching Observers ===
// ✓ Observer attached
// ✓ Observer attached
// ✓ Observer attached
// ✓ Observer attached
// 
// === Order Status Change ===
// ➡️ Order status changed to: PROCESSING
// 📧 Email: Sending notification for order ORD-001 - Status: PROCESSING
// 📦 Inventory: Updating stock for order ORD-001
// 📝 Log: Recording order change - ID: ORD-001, Status: PROCESSING
// 📊 Analytics: Tracking order ORD-001 status change
// 
// === Another Status Change ===
// ➡️ Order status changed to: SHIPPED
// 📧 Email: Sending notification for order ORD-001 - Status: SHIPPED
// 📦 Inventory: Updating stock for order ORD-001
// 📝 Log: Recording order change - ID: ORD-001, Status: SHIPPED
// 📊 Analytics: Tracking order ORD-001 status change

ประโยชน์ของ Observer Pattern:

  • Loose Coupling – Subject ไม่ต้อง know observers
  • Dynamic Subscriptions – Attach/detach observers ได้ anytime
  • Flexible – Add observers โดยไม่ modify Subject
  • Scalable – Order ไม่เพิ่มความซับซ้อน เพิ่ม observers

Pattern 4: Repository Pattern

ปัญหา: Data Access Logic ซ้ำกันทั่ว Project

java// ❌ ปัญหา: Data access logic ไปอยู่ทั่วหลาย classes
public class UserController {
    public void getUser(int id) {
        // ← Data access code mixed with business logic
        try {
            Connection conn = DriverManager.getConnection("...");
            PreparedStatement stmt = conn.prepareStatement(
                "SELECT * FROM users WHERE id = ?"
            );
            stmt.setInt(1, id);
            ResultSet rs = stmt.executeQuery();
            if (rs.next()) {
                User user = new User(rs.getInt("id"), rs.getString("name"));
                System.out.println(user);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

public class UserService {
    public void updateUser(User user) {
        // ← Same data access code repeated!
        try {
            Connection conn = DriverManager.getConnection("...");
            PreparedStatement stmt = conn.prepareStatement(
                "UPDATE users SET name = ? WHERE id = ?"
            );
            stmt.setString(1, user.getName());
            stmt.setInt(2, user.getId());
            stmt.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

วิธีแก้: Repository Pattern

java// ==== POJO ====
public class User {
    private int id;
    private String name;
    private String email;
    
    public User(int id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
    
    // Getters
    public int getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
    
    @Override
    public String toString() {
        return String.format(
            "User{id=%d, name='%s', email='%s'}",
            id, name, email
        );
    }
}

// ==== Repository Interface ====
/**
 * UserRepository: Abstraction for data access
 * ← Hides database implementation details
 */
public interface UserRepository {
    User findById(int id);
    List<User> findAll();
    void save(User user);
    void update(User user);
    void delete(int id);
}

// ==== Repository Implementation ====
/**
 * UserRepositoryImpl: Concrete implementation
 * ← All data access logic in ONE place
 */
public class UserRepositoryImpl implements UserRepository {
    
    @Override
    public User findById(int id) {
        String sql = "SELECT id, name, email FROM users WHERE id = ?";
        
        try (Connection conn = DatabaseConfig.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            pstmt.setInt(1, id);
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return new User(
                        rs.getInt("id"),
                        rs.getString("name"),
                        rs.getString("email")
                    );
                }
            }
        } catch (SQLException e) {
            System.err.println("Error: " + e.getMessage());
        }
        
        return null;
    }
    
    @Override
    public List<User> findAll() {
        List<User> users = new ArrayList<>();
        String sql = "SELECT id, name, email FROM users";
        
        try (Connection conn = DatabaseConfig.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            
            while (rs.next()) {
                User user = new User(
                    rs.getInt("id"),
                    rs.getString("name"),
                    rs.getString("email")
                );
                users.add(user);
            }
        } catch (SQLException e) {
            System.err.println("Error: " + e.getMessage());
        }
        
        return users;
    }
    
    @Override
    public void save(User user) {
        String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
        
        try (Connection conn = DatabaseConfig.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            pstmt.setString(1, user.getName());
            pstmt.setString(2, user.getEmail());
            pstmt.executeUpdate();
            
            System.out.println("✓ User saved: " + user.getName());
        } catch (SQLException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
    
    @Override
    public void update(User user) {
        String sql = "UPDATE users SET name = ?, email = ? WHERE id = ?";
        
        try (Connection conn = DatabaseConfig.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            pstmt.setString(1, user.getName());
            pstmt.setString(2, user.getEmail());
            pstmt.setInt(3, user.getId());
            pstmt.executeUpdate();
            
            System.out.println("✓ User updated: " + user.getName());
        } catch (SQLException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
    
    @Override
    public void delete(int id) {
        String sql = "DELETE FROM users WHERE id = ?";
        
        try (Connection conn = DatabaseConfig.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            pstmt.setInt(1, id);
            pstmt.executeUpdate();
            
            System.out.println("✓ User deleted");
        } catch (SQLException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
}

// ==== Service Layer (uses Repository) ====
public class UserService {
    private UserRepository userRepository;
    
    // Dependency Injection
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public void createUser(String name, String email) {
        User user = new User(0, name, email);
        userRepository.save(user);
    }
    
    public User getUser(int id) {
        return userRepository.findById(id);
    }
    
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
    
    public void updateUserInfo(int id, String newName, String newEmail) {
        User user = new User(id, newName, newEmail);
        userRepository.update(user);
    }
}

// ==== Controller Layer ====
public class UserController {
    private UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    public void handleGetUser(int id) {
        User user = userService.getUser(id);
        if (user != null) {
            System.out.println(user);
        }
    }
    
    public void handleCreateUser(String name, String email) {
        userService.createUser(name, email);
    }
}

// ==== Usage ====
public class RepositoryPatternExample {
    public static void main(String[] args) {
        // Setup dependency injection
        UserRepository userRepository = new UserRepositoryImpl();
        UserService userService = new UserService(userRepository);
        UserController userController = new UserController(userService);
        
        System.out.println("=== Create User ===");
        userController.handleCreateUser("John Doe", "[email protected]");
        userController.handleCreateUser("Jane Smith", "[email protected]");
        
        System.out.println("\n=== Get All Users ===");
        List<User> users = userService.getAllUsers();
        users.forEach(System.out::println);
        
        System.out.println("\n=== Get Specific User ===");
        userController.handleGetUser(1);
        
        System.out.println("\n=== Update User ===");
        userController.handleGetUser(1);
    }
}

// Output:
// === Create User ===
// ✓ User saved: John Doe
// ✓ User saved: Jane Smith
// 
// === Get All Users ===
// User{id=1, name='John Doe', email='[email protected]'}
// User{id=2, name='Jane Smith', email='[email protected]'}
// 
// === Get Specific User ===
// User{id=1, name='John Doe', email='[email protected]'}
// 
// === Update User ===
// User{id=1, name='John Doe', email='[email protected]'}

ประโยชน์ของ Repository Pattern:

  • Separation of Concerns – Data access ต่างจาก business logic
  • Testability – Mock repository สำหรับ testing
  • Maintainability – เปลี่ยน database ได้โดยไม่ modify business logic
  • Reusability – Repository ใช้ได้ในหลาย services

Comparison: 4 Patterns

Patternปัญหาวิธีแก้ใช้เมื่อ
FactoryTight coupling ในการสร้าง objectsCentralize creation logicNerd create different types dynamically
StrategyMany if-else for algorithmsEncapsulate algorithms ในclassesAlgorithm เปลี่ยนตามเงื่อนไข
ObserverTight coupling เมื่อ notifyLoose coupling via interfacesEvent-driven architecture
RepositoryData access ซ้ำกันทั่วที่Centralize data accessMulti-layer architecture

Best Practices: Pattern Selection

text✓ DO:
  ☑ Choose pattern to solve real problem
  ☑ Understand when to apply pattern
  ☑ Use patterns consistently
  ☑ Keep patterns simple (don't over-engineer)
  ☑ Document why pattern was chosen
  ☑ Refactor to patterns gradually

✗ DON'T:
  ☐ Use pattern just because it exists
  ☐ Mix patterns unnecessarily
  ☐ Apply patterns prematurely
  ☐ Make code overly complex
  ☐ Ignore existing code patterns
  ☐ Create patterns without understanding

สรุป

Design Patterns ไม่ใช่ “magic solution” แต่เป็น documented best practices ที่ help solve common problems:

4 Patterns ที่สำคัญ:

  • Factory → Simplify object creation
  • Strategy → Encapsulate algorithms
  • Observer → Decouple event handling
  • Repository → Centralize data access

เมื่อใช้ Patterns ถูกต้อง:

  • Code ของคุณ cleaner – clear responsibilities
  • Code ของคุณ more flexible – เปลี่ยนแปลงได้ง่าย
  • Code ของคุณ more testable – mock ได้ง่าย
  • Team ของคุณ understands faster – patterns เป็น common language

Design Patterns คือ “vocabulary” ของ software engineers – เมื่อคุณ master patterns คุณจะ communicate design ideas ได้ชัดเจนและ create systems ที่ยืดหยุ่นและยั่งยืน ยิ่งกว่านั้น pattern ยังช่วยให้คุณหลีกเลี่ยงปัญหา common ที่นักพัฒนาอื่นเคยประสบมา