Design Patterns: แนวทางการแก้ปัญหาที่พบบ่อย
หลังจากที่เราได้เรียนรู้หลักการออกแบบ OOP ที่ดีอย่าง SOLID Principles โดยเฉพาะ Dependency Inversion Principle (DIP) แล้ว เราจะพบว่ามีปัญหาบางอย่างที่เกิดขึ้นซ้ำๆ ในการพัฒนาซอฟต์แวร์ ซึ่งปัญหาเหล่านี้มักจะมีแนวทางการแก้ไขที่เป็นมาตรฐานและได้รับการพิสูจน์แล้วว่ามีประสิทธิภาพ นั่นคือ Design Patterns
Design Patterns คืออะไร?
Design Patterns ไม่ใช่โค้ดสำเร็จรูปที่คุณสามารถคัดลอกและวางได้ทันที แต่เป็น พิมพ์เขียว (Blueprint) หรือ แนวทาง (Guideline) ในการแก้ปัญหาการออกแบบซอฟต์แวร์ที่เกิดขึ้นซ้ำๆ Design Patterns อธิบายถึงปัญหา บริบทที่ปัญหานั้นเกิดขึ้น และแนวทางการแก้ไขปัญหาที่ได้รับการพิสูจน์แล้วว่ามีประสิทธิภาพ
คิดง่ายๆ ว่า Design Patterns คือคลังความรู้ที่รวบรวมประสบการณ์ของนักพัฒนาซอฟต์แวร์รุ่นก่อนๆ ที่ได้ลองผิดลองถูกมาแล้ว เมื่อเราเจอปัญหาที่คล้ายกัน เราสามารถศึกษา Design Pattern ที่เกี่ยวข้องเพื่อนำแนวทางการแก้ไขนั้นมาปรับใช้กับโปรเจกต์ของเราได้
เหตุผลที่ต้องใช้ Design Patterns:
- Code Reusability: ช่วยให้เรานำแนวทางการแก้ปัญหาที่ได้รับการพิสูจน์แล้วกลับมาใช้ใหม่ได้ ลดการคิดค้นวิธีแก้ปัญหาเองตั้งแต่ต้น
- Improved Communication: สร้างภาษาเดียวกันระหว่างนักพัฒนา ทำให้สื่อสารและเข้าใจโค้ดได้ง่ายขึ้น
- Better Architecture: ช่วยให้เราออกแบบซอฟต์แวร์ที่มีความยืดหยุ่น บำรุงรักษาง่าย และขยายได้ในอนาคต
- Proven Solutions: เป็นแนวทางการแก้ปัญหาที่ได้รับการทดสอบและใช้งานจริงมาแล้ว มั่นใจได้ในระดับหนึ่งว่ามีประสิทธิภาพ
ประเภทของ Design Patterns
Design Patterns สามารถแบ่งออกเป็น 3 ประเภทหลักๆ ตามจุดประสงค์ในการใช้งาน:
- Creational Patterns: เกี่ยวกับการสร้าง Object อย่างมีประสิทธิภาพ
- Structural Patterns: เกี่ยวกับการจัดโครงสร้าง Class และ Object เพื่อสร้างระบบที่ใหญ่และซับซ้อน
- Behavioral Patterns: เกี่ยวกับการกำหนดปฏิสัมพันธ์ระหว่าง Object และการจัดการ Algorithm
แต่ละประเภทก็จะมี Design Patterns ย่อยๆ อีกมากมาย แต่ในบทเรียนนี้เราจะมาดูตัวอย่าง Design Patterns ที่สำคัญและใช้งานบ่อยในแต่ละประเภท เพื่อให้เห็นภาพรวมและเข้าใจแนวคิดพื้นฐาน
ตัวอย่าง Design Patterns
ต่อไปนี้เป็นตัวอย่าง Design Patterns ที่พบได้บ่อย พร้อมคำอธิบายและตัวอย่างโค้ด (อย่างง่าย) ใน PHP:
1. Singleton Pattern (Creational)
ปัญหา: ต้องการให้มี Object เพียงตัวเดียวของ Class นั้นๆ และต้องการให้สามารถเข้าถึง Object นั้นได้จากทุกที่ในโปรแกรม
แนวทาง: สร้าง Class ที่มี Constructor เป็น `private` เพื่อป้องกันการสร้าง Object จากภายนอก และมี Static Method ที่ทำหน้าที่สร้างและคืนค่า Object ตัวเดียวของ Class นั้น
class Database {
private static $instance;
private $connection;
private function __construct() {
// Connect to database (example)
$this->connection = new PDO("mysql:host=localhost;dbname=mydb", "user", "password");
}
public static function getInstance(): Database {
if (self::$instance === null) {
self::$instance = new Database();
}
return self::$instance;
}
public function getConnection(): PDO {
return $this->connection;
}
}
// Usage
$db1 = Database::getInstance();
$db2 = Database::getInstance();
// $db1 และ $db2 คือ Object ตัวเดียวกัน
var_dump($db1 === $db2); // Output: bool(true)
เหตุผลเชิงออกแบบ: Singleton Pattern เหมาะสำหรับกรณีที่ต้องการควบคุมการเข้าถึงทรัพยากรที่มีจำกัด เช่น Database Connection หรือ Configuration File
Trade-offs: อาจทำให้เกิดปัญหาในการทดสอบ (Unit Testing) และอาจละเมิด Single Responsibility Principle (SRP) หาก Class Singleton ทำหน้าที่หลายอย่างเกินไป
2. Factory Pattern (Creational)
ปัญหา: ต้องการสร้าง Object โดยไม่ต้องระบุ Class ที่จะสร้างโดยตรง
แนวทาง: สร้าง Factory Class ที่มีหน้าที่ในการสร้าง Object ตามเงื่อนไขที่กำหนด
interface Animal {
public function makeSound(): string;
}
class Dog implements Animal {
public function makeSound(): string {
return "Woof!";
}
}
class Cat implements Animal {
public function makeSound(): string {
return "Meow!";
}
}
class AnimalFactory {
public static function createAnimal(string $type): Animal {
switch ($type) {
case 'dog':
return new Dog();
case 'cat':
return new Cat();
default:
throw new InvalidArgumentException("Invalid animal type: " . $type);
}
}
}
// Usage
$dog = AnimalFactory::createAnimal('dog');
echo $dog->makeSound(); // Output: Woof!
$cat = AnimalFactory::createAnimal('cat');
echo $cat->makeSound(); // Output: Meow!
เหตุผลเชิงออกแบบ: Factory Pattern ช่วยลดการพึ่งพา (Coupling) ระหว่าง Class ที่เรียกใช้ (Client) กับ Class ที่ถูกสร้าง (Concrete Classes) ทำให้ Client ไม่จำเป็นต้องรู้รายละเอียดเกี่ยวกับการสร้าง Object
Trade-offs: อาจเพิ่มความซับซ้อนให้กับโค้ดเล็กน้อย และต้องแก้ไข Factory Class ทุกครั้งที่เพิ่ม Class ใหม่
3. Strategy Pattern (Behavioral)
ปัญหา: ต้องการเลือก Algorithm หรือพฤติกรรมที่แตกต่างกันใน Runtime
แนวทาง: สร้าง Interface สำหรับ Algorithm และ Implement Algorithm แต่ละแบบใน Class ที่แตกต่างกัน จากนั้นให้ Client เลือก Algorithm ที่ต้องการใช้ผ่าน Interface
interface PaymentStrategy {
public function pay(float $amount): string;
}
class CreditCardPayment implements PaymentStrategy {
private $cardNumber;
private $expiryDate;
private $cvv;
public function __construct(string $cardNumber, string $expiryDate, string $cvv) {
$this->cardNumber = $cardNumber;
$this->expiryDate = $expiryDate;
$this->cvv = $cvv;
}
public function pay(float $amount): string {
return "Paid $" . $amount . " using Credit Card: " . $this->cardNumber;
}
}
class PayPalPayment implements PaymentStrategy {
private $email;
public function __construct(string $email) {
$this->email = $email;
}
public function pay(float $amount): string {
return "Paid $" . $amount . " using PayPal: " . $this->email;
}
}
class ShoppingCart {
private $paymentStrategy;
public function setPaymentStrategy(PaymentStrategy $paymentStrategy) {
$this->paymentStrategy = $paymentStrategy;
}
public function checkout(float $amount): string {
return $this->paymentStrategy->pay($amount);
}
}
// Usage
$cart = new ShoppingCart();
$creditCardPayment = new CreditCardPayment("1234-5678-9012-3456", "12/24", "123");
$cart->setPaymentStrategy($creditCardPayment);
echo $cart->checkout(100.00); // Output: Paid $100 using Credit Card: 1234-5678-9012-3456
$payPalPayment = new PayPalPayment("test@example.com");
$cart->setPaymentStrategy($payPalPayment);
echo $cart->checkout(50.00); // Output: Paid $50 using PayPal: test@example.com
เหตุผลเชิงออกแบบ: Strategy Pattern ช่วยให้เราแยก Algorithm ออกจาก Client ทำให้ Client สามารถเปลี่ยน Algorithm ได้โดยไม่ต้องแก้ไขโค้ด
Trade-offs: อาจเพิ่มจำนวน Class ในโปรเจกต์ และต้องระบุ Algorithm ที่ต้องการใช้ใน Client
คำแนะนำเพิ่มเติม: การเลือกใช้ Design Patterns ควรพิจารณาถึงความเหมาะสมกับปัญหาและบริบทของโปรเจกต์ การใช้ Design Patterns โดยไม่จำเป็นอาจทำให้โค้ดซับซ้อนโดยใช่เหตุ
ในบทความนี้เราได้เรียนรู้เกี่ยวกับ Design Patterns และตัวอย่างบางส่วนที่สำคัญ ในบทความถัดไป เราจะลงลึกในเรื่องของการทดสอบ Code OOP ด้วย Unit Testing เพื่อให้มั่นใจว่า Code ของเราทำงานได้อย่างถูกต้องและมีคุณภาพ