การใช้ Design Patterns: Singleton Pattern
ในบทที่แล้ว เราได้เรียนรู้เกี่ยวกับ Interfaces และการนำไปใช้เพื่อสร้างโค้ดที่ยืดหยุ่นและลดการพึ่งพากัน ในบทนี้ เราจะมาทำความรู้จักกับ Design Pattern ที่ชื่อว่า Singleton Pattern ซึ่งเป็น Pattern ที่มีประโยชน์ในการควบคุมการสร้าง Object ให้มีเพียง Instance เดียวเท่านั้น
Singleton Pattern คืออะไร
Singleton Pattern เป็น Design Pattern ประเภท Creational (เกี่ยวกับการสร้าง Object) ที่มีจุดประสงค์เพื่อให้มั่นใจว่าคลาสจะมีเพียง Instance เดียวเท่านั้น และมีจุดเข้าถึง (Global Access Point) ให้ Instance นั้นได้จากทุกที่ในโปรแกรม
ลองนึกภาพว่าคุณมี Configuration Class ที่เก็บการตั้งค่าต่างๆ ของ Application ของคุณ คุณต้องการให้ทุกส่วนของ Application สามารถเข้าถึง Configuration นี้ได้ แต่คุณไม่ต้องการให้มี Configuration หลายชุดที่อาจทำให้เกิดความขัดแย้งกัน Singleton Pattern ช่วยให้คุณมั่นใจได้ว่าจะมี Configuration Instance เดียวเท่านั้นที่ถูกใช้งาน
การ Implement Singleton Pattern
การ Implement Singleton Pattern ใน PHP มีขั้นตอนดังนี้:
- กำหนด Constructor ให้เป็น Private: เพื่อป้องกันการสร้าง Instance จากภายนอกคลาสโดยตรง
- สร้าง Static Private Variable: เพื่อเก็บ Instance ของคลาส
- สร้าง Static Public Method: เพื่อให้เข้าถึง Instance ของคลาส (Lazy Initialization)
ตัวอย่างโค้ด:
<?php
class DatabaseConnection {
private static ?DatabaseConnection $instance = null;
private string $connectionString;
// Constructor เป็น private ป้องกันการสร้าง instance จากภายนอก
private function __construct(string $connectionString) {
$this->connectionString = $connectionString;
echo "สร้าง Database Connection: " . $connectionString . "<br>";
}
// ป้องกันการ Clone
private function __clone() {}
// ป้องกันการ Unserialize
public function __wakeup()
{
throw new \Exception("Cannot unserialize singleton");
}
// Method สำหรับเข้าถึง Instance (Lazy Initialization)
public static function getInstance(string $connectionString): DatabaseConnection {
if (self::$instance === null) {
self::$instance = new DatabaseConnection($connectionString);
}
return self::$instance;
}
public function getConnectionInfo(): string {
return $this->connectionString;
}
}
// การใช้งาน
$db1 = DatabaseConnection::getInstance("mysql://user:password@host:port/database");
echo "Connection Info (db1): " . $db1->getConnectionInfo() . "<br>";
$db2 = DatabaseConnection::getInstance("mysql://user:password@host:port/database");
echo "Connection Info (db2): " . $db2->getConnectionInfo() . "<br>";
// ตรวจสอบว่า $db1 และ $db2 เป็น Instance เดียวกัน
if ($db1 === $db2) {
echo "db1 และ db2 เป็น Instance เดียวกัน<br>";
}
// พยายามสร้าง Instance โดยตรง (จะเกิด Error)
// $db3 = new DatabaseConnection(); // Error: Call to private DatabaseConnection::__construct()
?>
คำอธิบาย:
private static ?DatabaseConnection $instance = null;: ประกาศ Static Variable ชื่อ$instanceเพื่อเก็บ Instance ของDatabaseConnection(Nullable Type Hint?หมายความว่าสามารถเป็น null ได้)private function __construct(string $connectionString) { ... }: Constructor ถูกกำหนดให้เป็นprivateเพื่อป้องกันการสร้าง Instance โดยตรงจากภายนอก การทำเช่นนี้บังคับให้ใช้getInstance()เท่านั้นpublic static function getInstance(string $connectionString): DatabaseConnection { ... }:getInstance()เป็น Static Method ที่ใช้ในการเข้าถึง Instance ของDatabaseConnectionถ้ายังไม่มี Instance จะสร้าง Instance ใหม่ (Lazy Initialization) แล้วเก็บไว้ใน$instanceถ้ามี Instance อยู่แล้วก็จะ Return Instance เดิมprivate function __clone() {}: ป้องกันการ Clone Instance โดยการกำหนด Method__clone()ให้เป็นprivatepublic function __wakeup() { throw new \Exception("Cannot unserialize singleton"); }: ป้องกันการ Unserialize Instance โดยการ Throw Exception ใน Method__wakeup()
จากตัวอย่าง จะเห็นว่าไม่ว่าเราจะเรียก DatabaseConnection::getInstance() กี่ครั้ง ก็จะได้ Instance เดียวกันเสมอ นั่นคือ $db1 และ $db2 ชี้ไปยัง Object เดียวกันใน Memory
ข้อดีและข้อเสียของ Singleton Pattern
ข้อดี:
- ควบคุมการสร้าง Instance: มั่นใจได้ว่าจะมี Instance เดียวเท่านั้น
- Global Access Point: สามารถเข้าถึง Instance ได้จากทุกที่ในโปรแกรม
- Lazy Initialization: สร้าง Instance เมื่อต้องการใช้งานจริงๆ เท่านั้น ช่วยประหยัด Resource
ข้อเสีย:
- Global State: Singleton Instance เป็น Global State ซึ่งอาจทำให้ Code เข้าใจยากและทดสอบยาก
- Tight Coupling: อาจทำให้ Code พึ่งพา Singleton Class มากเกินไป ทำให้เปลี่ยนแปลงยาก
- Difficult to Test: เนื่องจาก Singleton เป็น Global State การ Mock หรือ Stub Singleton เพื่อ Test อาจทำได้ยาก
เหตุผลเชิงออกแบบ, Trade-offs และแนวปฏิบัติที่ดี
เหตุผลเชิงออกแบบ: Singleton Pattern ถูกใช้เมื่อต้องการควบคุมการสร้าง Object ให้มีเพียง Instance เดียวเท่านั้น และต้องการให้ Instance นั้นสามารถเข้าถึงได้จากทุกที่ในโปรแกรม เหมาะสำหรับกรณีที่ต้องการจัดการ Resource ที่มีจำกัด เช่น Database Connection, Configuration Settings หรือ Logging Service
Trade-offs: ข้อเสียหลักของ Singleton คือการสร้าง Global State ซึ่งอาจทำให้ Code เข้าใจยากและทดสอบยาก นอกจากนี้ยังอาจทำให้ Code พึ่งพา Singleton Class มากเกินไป ทำให้เปลี่ยนแปลงยาก
แนวปฏิบัติที่ดี:
- ใช้ Singleton อย่างระมัดระวัง: พิจารณาให้ดีว่าจำเป็นต้องใช้ Singleton จริงๆ หรือไม่ บางครั้ง Dependency Injection อาจเป็นทางเลือกที่ดีกว่า
- พยายามลดการพึ่งพา Singleton: พยายาม Inject Singleton Instance เข้าไปใน Class ที่ต้องการใช้งาน แทนที่จะเรียกใช้
getInstance()โดยตรง - ออกแบบ Singleton ให้ Testable: พยายามทำให้ Singleton สามารถ Mock หรือ Stub ได้ง่าย เพื่อให้สามารถ Test Code ที่ใช้งาน Singleton ได้
- พิจารณาการใช้ Factory Pattern: ในบางกรณี Factory Pattern อาจเป็นทางเลือกที่ดีกว่า Singleton โดยเฉพาะอย่างยิ่งถ้าต้องการความยืดหยุ่นในการสร้าง Instance ของ Class ที่แตกต่างกัน
สรุป: Singleton Pattern เป็น Design Pattern ที่มีประโยชน์ในการควบคุมการสร้าง Object ให้มีเพียง Instance เดียวเท่านั้น แต่ควรใช้งานอย่างระมัดระวังเนื่องจากมีข้อเสียบางประการ การทำความเข้าใจ Trade-offs และปฏิบัติตามแนวปฏิบัติที่ดีจะช่วยให้คุณใช้ Singleton ได้อย่างมีประสิทธิภาพ
ในบทถัดไป เราจะมาเรียนรู้เกี่ยวกับ Design Pattern อีกประเภทหนึ่งที่ชื่อว่า Factory Pattern ซึ่งเป็น Pattern ที่มีประโยชน์ในการสร้าง Object ที่ซับซ้อน