PlusMagi's Blog By Pitt Phunsanit PHP PHP: การเลือกใช้ Design Patterns ที่เหมาะสม (เช่น Factory, Singleton, Strategy)

PHP: การเลือกใช้ Design Patterns ที่เหมาะสม (เช่น Factory, Singleton, Strategy)

ในฐานะนักพัฒนา PHP ระดับ Senior การทำความเข้าใจ Design Patterns ไม่ใช่แค่การท่องจำโค้ด แต่คือการเรียนรู้ ‘วิธีแก้ปัญหา’ ที่ผ่านการพิสูจน์แล้ว (Proven Solutions) มันเปรียบเสมือนชุดเครื่องมือทางความคิดที่ช่วยให้เราเขียนโค้ดที่มีโครงสร้างชัดเจน, ยืดหยุ่นสูง (Flexible), และง่ายต่อการบำรุงรักษา (Maintainable) โดยเฉพาะอย่างยิ่งเมื่อระบบขยายตัวและมีนักพัฒนาหลายคนเข้ามาเกี่ยวข้อง

  • Design Pattern คืออะไร? มันคือพิมพ์เขียว (Blueprint) ของการแก้ปัญหาทางซอฟต์แวร์ที่เกิดขึ้นบ่อยครั้ง ไม่ใช่โค้ดสำเร็จรูป แต่เป็นแนวคิดเชิงสถาปัตยกรรม
  • ทำไมต้องใช้ Patterns ใน PHP? เพื่อให้เราหลีกเลี่ยงการเขียนโค้ดแบบ “God Object” (คลาสที่รับผิดชอบทุกอย่าง) และส่งเสริมหลักการ SOLID โดยเฉพาะ Single Responsibility Principle ทำให้เมื่อคุณต้องการเพิ่มฟีเจอร์ใหม่ คุณไม่ต้องไปแตะโค้ดส่วนอื่น ๆ ที่ทำงานอยู่แล้ว

2. ตัวอย่างการใช้งานและรูปแบบโค้ด (Code Examples)

เราจะมาดูตัวอย่าง 3 รูปแบบหลักที่ใช้บ่อยที่สุดใน PHP โดยใช้สถานการณ์จำลองคือ “ระบบประมวลผลคำสั่งซื้อสินค้า” เพื่อให้เห็นภาพการทำงานร่วมกันของ Patterns เหล่านี้

  • Strategy Pattern (กลยุทธ์): ใช้เมื่อคุณมีอัลกอริทึมหลายแบบที่สามารถสลับใช้ได้ตามบริบท เช่น วิธีการคำนวณส่วนลด หรือวิธีการชำระเงินต่าง ๆ
  • Factory Pattern (โรงงาน): ใช้เพื่อแยกความรับผิดชอบในการสร้าง Object ออกจากโค้ดหลัก ทำให้เราไม่จำเป็นต้องรู้ว่าคลาสที่แท้จริงคืออะไร เพียงแค่บอก Factory ว่าต้องการ “Product Type” อะไร
  • Singleton Pattern (เอกเทศ): ใช้เมื่อคุณมั่นใจว่าในระบบนั้นจะต้องมี Instance ของ Class นั้น ๆ ได้เพียงตัวเดียวเท่านั้น เช่น การจัดการ Log File หรือการเชื่อมต่อฐานข้อมูลหลัก
<?php declare(strict_types=1);

// ========================================
// 1. STRATEGY PATTERN (การชำระเงิน)
// ========================================

/**
 * Interface กำหนดมาตรฐานของ Payment Strategy ทั้งหมด
 */
interface PaymentStrategy {
    public function pay(float $amount): bool;
}

/**
 * Concrete Strategy: การจ่ายด้วยบัตรเครดิต
 */
class CreditCardPayment implements PaymentStrategy {
    private string $cardNumber;
    public function __construct(string $card) {
        $this->cardNumber = $card;
    }
    public function pay(float $amount): bool {
        echo "💳 Processing payment of $" . number_format($amount, 2) . " using Credit Card ending in " . substr($this->cardNumber, -4) . ". Success!\n";
        return true;
    }
}

/**
 * Concrete Strategy: การจ่ายด้วย PayPal
 */
class PayPalPayment implements PaymentStrategy {
    private string $email;
    public function __construct(string $email) {
        $this->email = $email;
    }
    public function pay(float $amount): bool {
        echo "📧 Processing payment of $" . number_format($amount, 2) . " using PayPal account: {$this->email}. Success!\n";
        return true;
    }
}

/**
 * Context Class: ผู้ประมวลผลคำสั่งซื้อ (ใช้ Strategy)
 */
class ShoppingCart {
    private PaymentStrategy $paymentMethod;

    // Dependency Injection: รับ PaymentStrategy เข้ามาใน Constructor
    public function setPaymentMethod(PaymentStrategy $method): void {
        $this->paymentMethod = $method;
    }

    public function checkout(float $totalAmount): bool {
        echo "\n--- Starting Checkout Process ---\n";
        return $this->paymentMethod->pay($totalAmount);
    }
}


// ========================================
// 2. FACTORY PATTERN (การสร้าง Connection)
// ========================================

/**
 * Product Interface: กำหนดมาตรฐานของ Database Connection
 */
interface DatabaseConnection {
    public function connect(): void;
}

/**
 * Concrete Product A: MySQL Connection
 */
class MySqlConnection implements DatabaseConnection {
    private string $host;
    public function __construct(string $host) { $this->host = $host; }
    public function connect(): void {
        echo "✅ Connected to MySQL database at host: {$this->host}.\n";
    }
}

/**
 * Concrete Product B: PostgreSQL Connection
 */
class PgSqlConnection implements DatabaseConnection {
    private string $host;
    public function __construct(string $host) { $this->host = $host; }
    public function connect(): void {
        echo "✅ Connected to PostgreSQL database at host: {$this->host}.\n";
    }
}

/**
 * Factory Class: รับผิดชอบในการสร้าง Object ที่ซับซ้อน
 */
class DatabaseFactory {
    // เมธอดหลักที่ใช้ตัดสินใจว่าจะสร้างคลาสไหน
    public static function createConnection(string $type, string $host): ?DatabaseConnection {
        switch (strtolower($type)) {
            case 'mysql':
                return new MySqlConnection($host);
            case 'pgsql':
                return new PgSqlConnection($host);
            default:
                echo "❌ Error: Unsupported database type.\n";
                return null;
        }
    }
}


// ========================================
// 3. SINGLETON PATTERN (Logger)
// ========================================

/**
 * Singleton Class: รับประกันว่ามี Instance ได้เพียงตัวเดียว
 */
class Logger {
    private static ?Logger $instance = null;
    private array $logs = [];

    // ป้องกันการสร้าง Object จากภายนอกโดยตรง
    private function __construct() {} 

    // เมธอดหลักในการเข้าถึง Instance เดียวกัน
    public static function getInstance(): Logger {
        if (self::$instance === null) {
            self::$instance = new Logger();
        }
        return self::$instance;
    }

    // ป้องกันการ Clone Object
    private function __clone() {} 

    // เมธอดสำหรับบันทึก Log
    public function log(string $message): void {
        $this->logs[] = date('Y-m-d H:i:s') . " - " . $message;
        echo "[LOG] Successfully recorded message.\n";
    }

    // เมธอดสำหรับดู Log ทั้งหมด (เพื่อการสาธิต)
    public function getLogs(): array {
        return $this->logs;
    }
}


// ========================================
// EXECUTION FLOW (การเรียกใช้งานจริง)
// ========================================

echo "========================================\n";
echo "         DEMO 1: STRATEGY PATTERN\n";
echo "========================================\n";

$cart = new ShoppingCart();

// Case 1: ใช้ Credit Card Strategy
$ccPayment = new CreditCardPayment("4242-xxxx-xxxx-1234");
$cart->setPaymentMethod($ccPayment);
$cart->checkout(500.00);

echo "\n========================================\n";
echo "         DEMO 2: FACTORY PATTERN\n";
echo "========================================\n";

// ใช้ Factory ในการสร้าง Connection โดยไม่ต้องรู้รายละเอียดคลาส
$mysqlConnection = DatabaseFactory::createConnection('MySQL', 'localhost');
if ($mysqlConnection) {
    $mysqlConnection->connect();
}

$pgSqlConnection = DatabaseFactory::createConnection('PgSQL', 'db.example.com');
if ($pgSqlConnection) {
    $pgSqlConnection->connect();
}


echo "\n========================================\n";
echo "         DEMO 3: SINGLETON PATTERN\n";
echo "========================================\n";

// การเข้าถึง Logger Instance ครั้งแรก
$logger1 = Logger::getInstance();
$logger1->log("User logged in successfully.");

// การเข้าถึง Logger Instance ครั้งที่สอง (จะได้ Object เดียวกัน)
$logger2 = Logger::getInstance(); 

if ($logger1 === $logger2) {
    echo "✅ Success: Both variables point to the same single instance of Logger.\n";
}

// ตรวจสอบ Log ที่บันทึกไว้
print_r($logger2->getLogs());

?>

ข้อควรระวัง Security และ Best Practices

  • คำนึงถึงความปลอดภัย (Security): เมื่อใช้ Factory หรือ Strategy ควรมีการตรวจสอบ Input Type เสมอ เพื่อป้องกันการถูกโจมตีด้วยค่าที่ไม่คาดคิด (Type Juggling) และควรหลีกเลี่ยงการสร้าง Object จาก Input ของผู้ใช้งานโดยตรง
  • Performance & Singleton Critique: Singleton Pattern แม้จะดูง่าย แต่หากใช้ไม่ถูกที่อาจทำให้เกิด Bottleneck ได้ ควรพิจารณาใช้ Lazy Loading หรือ Cache Layer (เช่น Redis/Memcached) ก่อนที่จะตัดสินใจใช้ Singleton กับทรัพยากรที่มีการเข้าถึงสูงมาก ๆ
  • Error Handling: ในทุก Pattern ที่มีการสร้าง Object (Factory) ต้องมีการจัดการ Exception อย่างรอบคอบ หากการเชื่อมต่อล้มเหลว ควรโยน `ConnectionException` แทนที่จะปล่อยให้โค้ดทำงานต่อไปด้วย Object ที่ไม่สมบูรณ์

สรุปและการนำไปประยุกต์ใช้งาน

การเลือกใช้ Design Pattern ที่เหมาะสมคือหัวใจสำคัญของการเป็น Senior Developer สิ่งที่ต้องจำไว้เสมอคือ “อย่าใช้ Pattern เพียงเพราะมันดูเท่” (Don’t use a pattern just because it exists) การออกแบบที่ดีต้องตอบโจทย์ปัญหาทางธุรกิจอย่างแท้จริง

  • เมื่อไหร่ควรใช้ Strategy? เมื่อคุณมีพฤติกรรมที่แตกต่างกันหลายแบบ (Behavior) และต้องการให้โค้ดหลัก (Context) ไม่ต้องรู้รายละเอียดของแต่ละพฤติกรรม ทำให้ระบบมีความยืดหยุ่นในการเพิ่มวิธีการใหม่ ๆ
  • เมื่อไหร่ควรใช้ Factory? เมื่อการสร้าง Object นั้นมีความซับซ้อน มีเงื่อนไขในการเลือกคลาส หรือมีการเปลี่ยนแปลงโครงสร้างของระบบบ่อยครั้ง (เช่น การเปลี่ยนจาก MySQL เป็น NoSQL)
  • เมื่อไหร่ควรใช้ Singleton? เมื่อคุณมั่นใจ 100% ว่าทรัพยากรนั้นต้องมี Instance เพียงตัวเดียวตลอดอายุการทำงานของแอปพลิเคชัน (เช่น Configuration Manager, Logger) และไม่มีความจำเป็นที่มันจะต้องถูกแยกส่วน

โดยสรุปแล้ว การออกแบบที่ดีคือการทำให้โค้ดของคุณมีความยืดหยุ่นสูงที่สุด (Highly Decoupled) เพื่อให้ระบบสามารถรองรับการเปลี่ยนแปลงในอนาคตได้อย่างง่ายดายและปลอดภัย ทำให้คุณพร้อมสำหรับการเป็น Senior Developer ที่แท้จริง


อ่านเพิ่มเติม