การใช้ Dependency Injection (DI) ใน PHP
ในบทที่แล้ว เราได้เรียนรู้เกี่ยวกับการเขียน Unit Tests ด้วย PHPUnit เพื่อให้มั่นใจในคุณภาพและความถูกต้องของโค้ดของเรา ในบทนี้ เราจะมาเรียนรู้เกี่ยวกับ Dependency Injection (DI) ซึ่งเป็นหลักการออกแบบที่ช่วยให้โค้ดของเรามีความยืดหยุ่น (flexible), ทดสอบได้ง่าย (testable), และนำกลับมาใช้ใหม่ได้ (reusable) มากยิ่งขึ้น
Dependency คืออะไร?
ก่อนที่เราจะพูดถึง Dependency Injection เรามาทำความเข้าใจก่อนว่า "Dependency" คืออะไร ในบริบทของการเขียนโปรแกรม Dependency หมายถึง เมื่อคลาสหนึ่ง (Class A) ต้องการใช้ฟังก์ชันการทำงานของอีกคลาสหนึ่ง (Class B) Class A จะมี Dependency ต่อ Class B
ลองพิจารณาตัวอย่างง่ายๆ:
class UserProfile {
private $database;
public function __construct() {
$this->database = new Database();
}
public function getUserProfile($userId) {
// ดึงข้อมูลผู้ใช้จากฐานข้อมูลโดยใช้ $this->database
return $this->database->query("SELECT * FROM users WHERE id = " . $userId);
}
}
ในตัวอย่างนี้ คลาส UserProfile มี Dependency ต่อคลาส Database เพราะ UserProfile ต้องใช้ Database เพื่อดึงข้อมูลผู้ใช้
ปัญหาของการสร้าง Dependency โดยตรง (Hard Dependency)
การสร้าง Dependency โดยตรงภายในคลาส (เช่น $this->database = new Database();) ก่อให้เกิดปัญหาหลายอย่าง:
- ความยืดหยุ่นต่ำ: หากเราต้องการเปลี่ยนไปใช้ฐานข้อมูลอื่น (เช่น PostgreSQL) เราจะต้องแก้ไขโค้ดภายในคลาส
UserProfile - ทดสอบได้ยาก: ในระหว่างการทดสอบ (Unit Testing) เราไม่สามารถจำลอง (mock) คลาส
Databaseได้ง่าย ทำให้การทดสอบUserProfileกลายเป็น Integration Test มากกว่า Unit Test - นำกลับมาใช้ใหม่ได้ยาก: คลาส
UserProfileผูกติดอยู่กับคลาสDatabaseทำให้ยากต่อการนำUserProfileไปใช้ในบริบทอื่นที่ไม่ต้องการใช้Database
Dependency Injection (DI) คืออะไร?
Dependency Injection (DI) เป็นหลักการออกแบบที่แก้ปัญหา Hard Dependency โดยการ "Inject" หรือ "ส่งมอบ" Dependency ให้กับคลาส แทนที่จะให้คลาสสร้าง Dependency เอง DI ช่วยให้เราสามารถเปลี่ยน Dependency ได้ง่าย, ทดสอบโค้ดได้ง่ายขึ้น, และทำให้โค้ดมีความยืดหยุ่นมากขึ้น
โดยพื้นฐานแล้ว DI คือการ "ส่ง" (inject) สิ่งที่คลาสต้องการ (dependency) เข้าไปในคลาส แทนที่คลาสจะต้องสร้างมันขึ้นมาเอง
รูปแบบการทำ Dependency Injection
มีหลายรูปแบบในการทำ Dependency Injection แต่รูปแบบที่พบบ่อยที่สุดคือ Constructor Injection
Constructor Injection
Constructor Injection คือการ Inject Dependency ผ่าน Constructor ของคลาส:
class UserProfile {
private $database;
public function __construct(Database $database) {
$this->database = $database;
}
public function getUserProfile($userId) {
// ดึงข้อมูลผู้ใช้จากฐานข้อมูลโดยใช้ $this->database
return $this->database->query("SELECT * FROM users WHERE id = " . $userId);
}
}
// การใช้งาน
$database = new Database();
$userProfile = new UserProfile($database); // Inject Database object
$userData = $userProfile->getUserProfile(123);
ในตัวอย่างนี้ เรา Inject Database object เข้าไปใน UserProfile ผ่าน Constructor แทนที่จะให้ UserProfile สร้าง Database object เอง
ข้อดีของ Constructor Injection:
- Dependency ถูกกำหนดไว้อย่างชัดเจนใน Constructor ทำให้ง่ายต่อการเข้าใจว่าคลาสต้องการอะไร
- รับประกันว่า Dependency จะถูก Inject เมื่อสร้าง Object
- ทำให้การทดสอบง่ายขึ้น เพราะเราสามารถ Inject Mock object เข้าไปได้
ตัวอย่างการทดสอบด้วย Mock object:
// สมมติว่าเรามี MockDatabase class สำหรับการทดสอบ
class MockDatabase {
public function query($sql) {
// คืนค่าข้อมูลจำลองสำหรับการทดสอบ
return ['id' => 123, 'name' => 'Test User'];
}
}
// ใน Unit Test
$mockDatabase = new MockDatabase();
$userProfile = new UserProfile($mockDatabase); // Inject MockDatabase
$userData = $userProfile->getUserProfile(123);
// ตรวจสอบว่า $userData เป็นข้อมูลจำลองที่เรากำหนดไว้
// ...
Service Container (Dependency Injection Container)
เมื่อโปรเจกต์ของเรามีขนาดใหญ่ขึ้นและมี Dependency ซับซ้อนมากขึ้น การจัดการ Dependency ด้วยตนเอง (เช่น การสร้าง Object และ Inject ทีละตัว) จะกลายเป็นเรื่องยุ่งยาก Service Container หรือ Dependency Injection Container (DIC) คือเครื่องมือที่ช่วยเราจัดการ Dependency เหล่านี้
Service Container มีหน้าที่หลักในการ:
- ลงทะเบียน (Register) Dependency: บอก Container ว่าจะสร้าง Object ของแต่ละคลาสอย่างไร
- แก้ไข (Resolve) Dependency: เมื่อเราต้องการ Object ของคลาสใด Container จะสร้าง Object และ Inject Dependency ที่จำเป็นให้โดยอัตโนมัติ
ตัวอย่างการใช้ Service Container (อย่างง่าย):
class Container {
private $bindings = [];
public function bind($abstract, $concrete) {
$this->bindings[$abstract] = $concrete;
}
public function resolve($abstract) {
if (isset($this->bindings[$abstract])) {
$concrete = $this->bindings[$abstract];
if (is_callable($concrete)) {
return $concrete($this); // Resolve using a closure
}
return new $concrete; // Resolve using class name
}
return null; // Or throw an exception if not found
}
}
// การใช้งาน
$container = new Container();
// ลงทะเบียน Database class
$container->bind('Database', 'Database');
// ลงทะเบียน UserProfile class
$container->bind('UserProfile', function ($container) {
return new UserProfile($container->resolve('Database'));
});
// Resolve UserProfile class
$userProfile = $container->resolve('UserProfile');
$userData = $userProfile->getUserProfile(123);
ในตัวอย่างนี้ เราสร้าง Container class อย่างง่ายขึ้นมาเพื่อแสดงแนวคิด Service Container จริงๆ แล้ว Framework ส่วนใหญ่ (เช่น Laravel, Symfony) จะมี Service Container มาให้พร้อมใช้งานอยู่แล้ว
ข้อดีของการใช้ Service Container:
- ลดความซับซ้อนในการจัดการ Dependency
- ช่วยให้โค้ดมีความเป็นระเบียบและง่ายต่อการบำรุงรักษา
- รองรับการทำ Dependency Injection ได้อย่างเต็มรูปแบบ
สรุป
Dependency Injection (DI) เป็นหลักการออกแบบที่สำคัญที่ช่วยให้โค้ดของเรามีความยืดหยุ่น, ทดสอบได้ง่าย, และนำกลับมาใช้ใหม่ได้ การใช้ Constructor Injection เป็นรูปแบบที่พบบ่อยที่สุดในการทำ DI และ Service Container ช่วยให้เราจัดการ Dependency ที่ซับซ้อนได้อย่างมีประสิทธิภาพ
ในบทถัดไป เราจะมาเรียนรู้เกี่ยวกับการสร้าง Middleware ใน PHP ซึ่งเป็นอีกหนึ่งแนวคิดที่สำคัญในการสร้าง Application ที่มีความยืดหยุ่นและจัดการ Request/Response ได้อย่างมีประสิทธิภาพ