Định nghĩa
Theo wikipedia:Dependency Injection is a software design pattern that allows removing hard-coded dependencies and making it possible to change them, whether at run-time or compile-time.Tạm dịch:
Dependency Injection là một mẫu thiết kết phần mềm mà nó cho phép chúng ta loại bỏ các sự phụ thuộc của các đối tượng trong chương trình và dễ dàng thay đổi chúng tại thời điểm ứng được chạy hay biên dịch.
Đơn giản hóa khái niệm hay tổng quan về DI
Định nghĩa như vậy là quá phức tạp cho những ai lần đầu tiên làm quen với điều này. Do vậy chúng ta cần phác thảo qua một vài ví dụ cũng như lợi ích khi chương trình được thiết kế theo khái niệm (kiến trúc) này.Nhắc đến phần mềm khi nói đến một phần mềm, hay một tính năng trong phầm mềm đó, hoặc là zoom lại gần hơn để nhìn thấy một đối tượng của một lớp (class) nào đó đều cần nạp
Input để ra một Output
nào đó. Như trong ZF1 để Application có thể run được
nó cần phải gọi bootstrap, thật ra trong quá trình đó bao gồm là quá
trình đọc file application.ini và khởi tạo resource cần thiết cho hệ
thống.Nhiệm vụ cơ bản nhất của Dependency Injection (DI) là làm giảm sự phụ thuộc của những thành phần khác nhau trong hệ thống. Các đối tượng giờ chỉ cần tập trung tới hành vi (behavior) của chúng mà không cần quan tâm đến việc thiết lập các thuộc tính của nó.
Do vậy giờ đây một tính năng có thể tách ra làm hai #task riêng biệt mà có thể phân chia cho 2 lập trình viên (Developer) khác nhau. Điều này rất lợi ích trong quá trình phát triển phần mềm. Vì các #task độc lập nhau và có thể phân nhỏ để chia cho nhiều người. Và như vậy khi có lỗi xảy ra thì cũng nhanh chóng biết được từ đâu, do đó chúng dễ dàng được debug hơn.
Ví dụ với PHP.
Nói như vậy là quá nhiều về lý thuyết. Chúng ta cần một ví dụ để làm rõ những điều trên.Tham khảo qua một vòng các bài viết tiếng Việt khác, thấy chúng được các tác giả mô tả quá phức tạp với nhiều vấn đề khác nhau, khiến chúng ta không còn tập trung vào cái lõi của DI. Nhưng trước khi bạn vào ví dụ, bạn cần biết một điều thú vị sau:
Trên wiki khi nói về DI có đề cập về một tư tưởng dựa trên một câu nói nổi tiếng trong nền công nghiệp giải trí Hollywood: Don't call us, we'll call you. Cái này gọi là Hollywood Principle, tạm dịch là "Đừng gọi cho chúng tôi, chúng tôi sẽ gọi bạn".
Với cách làm truyền thống, chúng ta thử qua một ví dụ sau:
Chúng ta có một class gọi là Taxi, với hai biến khởi tạo chính với các thành phần như: id của nó, tài xế driver,giá tiền mỗi một km pricePerKm,và thuế giá trị gia tăng taxVAT. Có một phương thức là số tiền mà người dùng cần phải trả dựa trên quãng đường.
Code ngắn gọn nhất có thể mô tả như sau:
class Taxi{
protected $id;
protected $driver;
protected $pricePerKm;
protected $taxVAT;
public function __construct($taxi_id, $config)
{
$this->id = $taxi_id;
$diver = UserModel::getDefaultDriverOfTaxi($this->id);
$this->setDriver($driver);
$this->setPricePerKm($config['price_per_km']);
$this->setTaxVAT($config['tax_vat']);
}
public function setDriver(User $driver)
{
$this->driver = $driver;
return $this;
}
public function calFee($distance)
{
//@todo cal the fee here
return $distance*$this->getPricePerKm()*(1+$this->getTaxVAT());
}
}
Phân tích tổng quan về cách cài đặt trên
Với cách cài đặt (implement) như trên, chúng ta thấy có rất nhiều điều cần phải làm khi khởi tạo đối tượngTaxi. Như việc lấy ra tài xế driver, gán dữ liệu cho giá tiền taxi mỗi một km, thuế giá trị gia tăng.
Điều này làm tăng sự phụ thuộc (highly coupled dependency) của lớp Taxi vào các lớp khác và vào các thiết lập (setting/configuration) khác ban đầu cho nó.Bài toán tính giá tiền (output) dựa trên khoảng cách khoảng cách (input) bắt đầu từ việc thiết lập các config (input) như giá tiền taxi, thuế VAT, tài xế trở nên nhập nhằng trong một Concern (class). Điều này dẫn tới nó sẽ trở nên khó cho việc tách #task (input/output) riêng biệt, nếu như bạn phải chia việc cho 2 Developers để tối ưu resource.
Hơn nữa, việc thiết lập config ngay bên trong cùng một concern (Taxi class) sẽ dẫn đến phức tạp như, giả sử khi đó cái config không phải là array mà là một object khi đó sẽ là vấn đề như:
public function __construct($taxi_id, $config)
{
/**...*/
if( $config instanceOf StdClass)
{
$this->setPricePerKm($config->price);
$this->setTaxVAT($config->tax);
}
else
{
$this->setPricePerKm($config['price_per_km']);
$this->setTaxVAT($config['tax_vat']);
}
}
Việc không đồng nhất propery trong object và array cũng như các tham số truyền vào lẽ ra không thuộc phạm vi (concern)
của class Taxi, nó chỉ cần biết các thuộc tính của chính nó: id, driver, pricePerKm, taxVAT là có thể làm việc được, mà không cần quan tâm đến việc từ đâu có các giá trị config như trên.Do vậy chúng ta cần thiết kế lại việc set giá trị cho nó được độc lập với các thành phần khác.
Ví dụ tiếp theo về cải tiến chương trình theo DI:
class Taxi{
public function __construct($id_taxi, Config $config)
{
$this->id = $taxi_id;
$diver = UserModel::getDefaultDriverOfTaxi($this->id);
$this->setDriver($driver);
$config->setConfigurationToTaxi($this);
}
}
class Config{
public function setConfigurationToTaxi(Taxi $taxi)
{
$taxi->setPricePerKm(
$this->getConfigPrice()
);
$taxi->setTaxVAT(
$this->getConfigVAT()
);
}
public function getConfigPrice()
{
//Lấy giá trị giá tiền trên mỗi từ database, const...
}
public function getConfigVAT()
{
//Lấy giá trị thuế VAT từ database, const...
}
}
Giờ đây, bạn có thể nhìn ra được một phần nào đó tách biệt giữa input và ouput như đã đề cập.
Đối tượng Config (input) sẽ thiết lập các giá trị ban đầu cho đối tượng của lớp Taxi. Lớp Taxi
giờ đây chỉ quan tâm tới việc thực hiện tính toán logic trong hàm calFee mà không quan tâm
đến việc tử đâu có được giá tiền và thuế. Đây chính là cốt lõi của Dependency Injection.Nhưng như vậy là chưa đủ đẹp, bạn cần phải chuyển code của việc get $driver ra khỏi __construct của lớp Taxi. Đến đây chúng ta có thể nghĩ: vậy thì setDriver tại nơi nó gọi ra như trong Controller hay Model nào đó. Như vậy thì có thể đúng nhưng chưa đủ, thậm chí dẫn đến vi phạm nguyên tắc phạm nguyên tắc Don't repeat yourself , code sẽ được lặp đi lặp lại rất nhiều mỗi khi ai đó muốn sử dụng cái đối tượng đó.
Trong ngữ cảnh này tốt nhất là chúng ta nên có thêm một
Container, hay một Service hay một Factory, tùy ngữ cảnh bạn
muốn sử dụng. Ví dụ trong trường hợp này, chúng ta có một Service để lấy ra chiếc một chiếc Taxi như sau:interface ITaxi{
public function calFee($distance);
}
class Taxi implement ITaxi{
public function __construct($id_taxi, Config $config)
{
$this->id = $taxi_id;
$config->setConfigurationToTaxi($this);
}
}
class Service{
protected $arrTaxi;
/**
* @return ITaxi Trả về một Interface chỉ có phương thức tính tiền.
*/
public function getTaxi($taxi_id)
{
if( isset($this->arrTaxi[$taxi_id]) )
{
return $this->arrTaxi[$taxi_id];
}
$taxi = new Taxi($taxi_id, new Config());
$diver = UserModel::getDefaultDriverOfTaxi($taxi_id);
$taxi->setDriver($driver);
return $this->arrTaxi[$taxi_id] = $taxi;
}
}
Giải thích thêm về cách implement ở trên:
- Việc thêm Interface là ITaxi nhằm báo cho những lập trình viên khác khi sử dụng đối tượng thuộc lớp này, chỉ cần quan tâm đến phương thức tính tiền của nó.
- Đảm bảo rằng đối tượng sẽ chỉ khởi tạo một lần và được sử dụng cho các lần sau.
- Việc thiết lập này sẽ tránh đẩy phần việc đó cho những chỗ gọi ra nó, do đó cũng tránh được
việc
quênthiết lập giá trị ban đầu nhưdriver.
Kết quả
Giờ đây rõ ràng là việc sử dụng class Taxi để tính tiền đã được dễ dàng hơn rất nhiều. Người dử dụng giờ đây chỉ cần gọi hàm:$taxi = Service::getTaxi($taxi_id); //An instance of ITaxi $fee = $taxi->calFee($distance = 10);







