การใช้ Dependency Injection: ลด Coupling
ในบทที่แล้ว เราได้เรียนรู้เกี่ยวกับการจัดการ Exceptions เพื่อให้โปรแกรม PHP ของเรามีความแข็งแกร่งและสามารถรับมือกับข้อผิดพลาดที่ไม่คาดฝันได้ ในบทนี้ เราจะมาพูดถึงเทคนิคการออกแบบซอฟต์แวร์ที่สำคัญอีกอย่างหนึ่ง นั่นคือ Dependency Injection (DI) ซึ่งจะช่วยให้โค้ดของเรามีความยืดหยุ่น, ทดสอบได้ง่าย, และลดความสัมพันธ์ (Coupling) ระหว่างคลาสต่างๆ
Dependency Injection คืออะไร
Dependency (การพึ่งพา) หมายถึง เมื่อคลาสหนึ่งต้องการใช้งานฟังก์ชันการทำงานของอีกคลาสหนึ่ง คลาสแรกจะ "พึ่งพา" คลาสที่สอง Dependency Injection คือรูปแบบการออกแบบที่คลาสหนึ่งได้รับ Dependencies ของมันจากภายนอก แทนที่จะสร้าง Dependencies เหล่านั้นขึ้นมาเองภายในคลาส
ลองพิจารณาตัวอย่างง่ายๆ สมมติว่าเรามีคลาส User ที่ต้องการบันทึกข้อมูลผู้ใช้ลงในฐานข้อมูล:
class User {
private $database;
public function __construct() {
$this->database = new Database(); // สร้าง Database object ภายในคลาส
}
public function register(string $username, string $password): void {
// ... ทำงานบางอย่าง ...
$this->database->insert('users', ['username' => $username, 'password' => $password]);
}
}
ในตัวอย่างนี้ คลาส User สร้างอินสแตนซ์ของคลาส Database ภายใน Constructor เอง ซึ่งทำให้เกิดปัญหาหลายอย่าง:
- Coupling สูง: คลาส
Userผูกติดอยู่กับคลาสDatabaseอย่างแน่นหนา หากเราต้องการเปลี่ยนไปใช้ฐานข้อมูลอื่น (เช่น MySQL เป็น PostgreSQL) เราจะต้องแก้ไขโค้ดภายในคลาสUser - ทดสอบยาก: ในการทดสอบคลาส
Userเราจะต้องมี Database จริงๆ (หรือจำลองขึ้นมา) ซึ่งอาจจะยุ่งยากและใช้เวลานาน
Dependency Injection เข้ามาช่วยแก้ปัญหาเหล่านี้ได้ โดยการให้เราส่ง Dependencies (ในที่นี้คือ Database object) เข้ามาในคลาส User จากภายนอก:
class User {
private $database;
public function __construct(Database $database) {
$this->database = $database; // รับ Database object ผ่าน Constructor
}
public function register(string $username, string $password): void {
// ... ทำงานบางอย่าง ...
$this->database->insert('users', ['username' => $username, 'password' => $password]);
}
}
ตอนนี้ คลาส User ไม่ได้สร้าง Database object เองอีกต่อไป แต่ได้รับมันผ่าน Constructor ซึ่งทำให้:
- Coupling ต่ำ: คลาส
Userไม่ได้ผูกติดอยู่กับคลาสDatabaseโดยตรง เราสามารถส่ง Database object ประเภทอื่นๆ เข้ามาได้ ตราบใดที่มัน Implement Interface ที่Userต้องการ (เราจะพูดถึง Interfaces ในบทถัดไป) - ทดสอบง่าย: ในการทดสอบคลาส
Userเราสามารถส่ง Mock Database object เข้ามาได้ ซึ่งจะทำให้เราสามารถควบคุมพฤติกรรมของ Database ได้อย่างเต็มที่
Dependency Injection ช่วยให้โค้ดของเรามีความยืดหยุ่นและทดสอบได้ง่ายขึ้น เพราะเราสามารถสับเปลี่ยน Dependencies ได้ตามต้องการ โดยไม่ต้องแก้ไขโค้ดภายในคลาส
Constructor Injection
Constructor Injection คือรูปแบบของ Dependency Injection ที่เราส่ง Dependencies ผ่าน Constructor ของคลาส ดังที่เราได้เห็นในตัวอย่างข้างต้น:
class User {
private $database;
public function __construct(Database $database) {
$this->database = $database; // Constructor Injection
}
// ...
}
// ตัวอย่างการใช้งาน
$database = new Database();
$user = new User($database); // Inject Database object เข้าไป
Constructor Injection เป็นรูปแบบที่ได้รับความนิยมมากที่สุด เพราะมันทำให้ Dependencies ของคลาสชัดเจนตั้งแต่แรก และบังคับให้เราต้องส่ง Dependencies ที่จำเป็นเข้ามาเมื่อสร้างอินสแตนซ์ของคลาส
ข้อดีของ Constructor Injection:
- บังคับให้ Dependencies ถูกส่งเข้ามาเสมอ
- ทำให้ Dependencies ชัดเจนและง่ายต่อการเข้าใจ
- ทำให้คลาส Immutable (ไม่เปลี่ยนแปลง) ได้ง่ายขึ้น
ข้อเสียของ Constructor Injection:
- อาจทำให้ Constructor ยาวและซับซ้อน หากคลาสมี Dependencies จำนวนมาก
Setter Injection
Setter Injection คือรูปแบบของ Dependency Injection ที่เราส่ง Dependencies ผ่าน Setter method:
class User {
private $database;
public function setDatabase(Database $database): void {
$this->database = $database; // Setter Injection
}
// ...
}
// ตัวอย่างการใช้งาน
$user = new User();
$user->setDatabase(new Database()); // Inject Database object ผ่าน Setter method
Setter Injection เหมาะสำหรับ Dependencies ที่ Optional หรือ Dependencies ที่อาจเปลี่ยนแปลงได้ในภายหลัง
ข้อดีของ Setter Injection:
- สามารถ Inject Dependencies ได้หลังจากสร้างอินสแตนซ์ของคลาสแล้ว
- เหมาะสำหรับ Dependencies ที่ Optional
ข้อเสียของ Setter Injection:
- ไม่บังคับให้ Dependencies ถูกส่งเข้ามา ทำให้คลาสอาจทำงานไม่ถูกต้องหากไม่มี Dependencies
- ทำให้ Dependencies ไม่ชัดเจนเท่า Constructor Injection
เหตุผลเชิงออกแบบ Trade-offs และแนวปฏิบัติที่ดี
การเลือกใช้ Constructor Injection หรือ Setter Injection ขึ้นอยู่กับสถานการณ์และความต้องการของโปรเจกต์
- Constructor Injection: ควรใช้สำหรับ Dependencies ที่จำเป็นต่อการทำงานของคลาส และไม่ควรเปลี่ยนแปลงในภายหลัง
- Setter Injection: ควรใช้สำหรับ Dependencies ที่ Optional หรือ Dependencies ที่อาจเปลี่ยนแปลงได้ในภายหลัง
นอกจากนี้ การใช้ Interface ร่วมกับ Dependency Injection เป็นแนวปฏิบัติที่ดี เพราะจะช่วยให้โค้ดของเรามีความยืดหยุ่นมากยิ่งขึ้น เราสามารถเปลี่ยน Implementation ของ Dependency ได้ง่ายๆ โดยไม่ต้องแก้ไขโค้ดที่ใช้งาน Dependency (เราจะพูดถึง Interfaces ในบทถัดไป)
ตัวอย่าง:
interface DatabaseInterface {
public function insert(string $table, array $data): void;
public function select(string $table, array $conditions): array;
}
class MySQLDatabase implements DatabaseInterface {
// ... Implement methods ...
}
class PostgreSQLDatabase implements DatabaseInterface {
// ... Implement methods ...
}
class User {
private $database;
public function __construct(DatabaseInterface $database) {
$this->database = $database;
}
// ...
}
// ตัวอย่างการใช้งาน
$mysqlDatabase = new MySQLDatabase();
$user = new User($mysqlDatabase); // Inject MySQLDatabase
$postgresqlDatabase = new PostgreSQLDatabase();
$user = new User($postgresqlDatabase); // Inject PostgreSQLDatabase (ไม่ต้องแก้ไข User class)
ในตัวอย่างนี้ คลาส User รับ DatabaseInterface เป็น Dependency ซึ่งทำให้เราสามารถ Inject MySQLDatabase หรือ PostgreSQLDatabase เข้ามาได้ โดยไม่ต้องแก้ไขโค้ดในคลาส User
Dependency Injection เป็นเทคนิคที่มีประโยชน์อย่างมากในการออกแบบซอฟต์แวร์ที่ยืดหยุ่น, ทดสอบได้ง่าย, และบำรุงรักษาได้ง่าย การทำความเข้าใจและนำไปประยุกต์ใช้จะช่วยให้คุณเขียนโค้ด PHP ที่มีคุณภาพมากยิ่งขึ้น