Dependency Injection (DI) là một khái niệm đã rất phổ biến trong thiết kế hướng đối tượng. Hôm nay chúng ta sẽ cùng nhắc lại và củng cố thêm về một số khía cạnh của DI nhé. Các ví dụ trong bài viết này sẽ được viết bằng PHP.

Hãy tưởng tượng bạn cần xây dựng một ứng dụng thông báo đa kênh. Ứng dụng của bạn có thể gửi thông báo qua Mobile Apps, qua Email, hoặc qua kênh Slack (Tích hợp API). Nếu bạn đã thân thuộc với Object-Oriented Programming (OOP), điều đầu tiên bạn nghĩ đến có thể là thiết kế một Abstract Class như thế này

abstract class BaseNotification
{
   abstract public function send();
}

Để có thể gửi thông báo qua Email, chúng ta sẽ tiến hành extends class lên và triển khai phương thức send()

class MailNotification extends BaseNotification
{
   public function send()
   {
      $mailService = new MailService();
      doSomeThingToSend();
   }
}

Nếu có bất cứ một cách gửi thông báo nào mới, ta hoàn toàn có thể làm tương tự như trên. Ví dụ với Slack nhé:

class SlackNotification extends BaseNotification
{
   public function send()
   {
      $slackService = new SlackService();
      doSomeThingToSend();
   }
}

Nhìn không tệ, phải không ?. Nó tuân thủ khá tốt “Open for extension, Close for modification” của SOLID. Và chúng ta có thể thêm bất cứ dịch vụ gửi thông báo nào mà mình muốn.

Tuy nhiên mọi thứ không đơn giản như vậy

Bẫy kế thừa

Chúng ta thấy rằng nếu mỗi lần phát sinh nhu cầu mới, bạn lại phải tạo ra một class mới. Nôm na rằng, ứng dụng của bạn sẽ sớm tràn ngập các class khác nhau để xử lý cùng một vấn đề. Code của bạn tái sử dụng rất kém, lặp đi lặp lại một việc giữa các class.

Tính linh hoạt và mở rộng của các đoạn code kia cũng thấp, khi nó đang phụ thuộc cứng vào các service bên trong nó, vi phạm vào quy tắc loosely coupling (phụ thuộc mềm, tức phụ thuộc vào các đối tượng trừu tượng).

Ngoài ra, nếu để ý kỹ hơn, chúng ta sẽ thấy các đoạn code trên rất khó để mocking / testing. Làm sao để bạn có thể giả lập các đầu vào cho các dịch vụ khi cần viết unit test

Và rõ ràng rằng, chúng ta đã vi phạm tính Single responsibility, khi class của chúng ta ngoài việc send thông báo, còn chịu trách nhiệm khởi tạo những thứ nó cần để làm việc đó ?.

Đây chính là Bẫy kế thừa khi thiết kế OOP. Trong ví dụ trên, chúng ta đang thể hiện tính đa hình (polymorphism) giữa các đối tượng thông qua kế thừa. Hành vi của các đối tượng con hoàn toàn dựa trên những gì chúng kế thừa lại, và tự chúng kiểm soát hành vi đó (ở đây là phương thức send()). Về cơ bản chúng ta không có cách nào để tác động được hành vi của chúng từ bên ngoài.

Composition over inheritance

Điều này dẫn đến một nguyên lý thiết kế rằng:

“Tính đa hình của hành vi đối tượng và tái sử dụng các thành phần nên được thể hiện qua các phụ thuộc mà nó nhận được, chứ không phải thông qua kế thừa”

Trong ví dụ trên, chúng ta có thể hiểu rằng
Class Notification chỉ nên làm việc duy nhất là gửi tin nhắn, còn các phụ thuộc đều được tiêm (inject) từ bên ngoài vào. Các phụ thuộc này (Như MailService, SlackService) sẽ quyết định hành vi gửi tin nhắn

Đến đây, có lẽ các bạn đã hiểu cốt lõi Dependency injection là gì. Điều này cũng dẫn đến khái niệm khá phổ biến: Inversion of Control (IoC). Dịch nôm na ra, nó là “đảo chiều kiểm soát”: thay vì tự các đối tượng quyết định hành vi của nó, hành vi được kiểm soát thông qua các phụ thuộc.

Ví dụ của chúng ta có thể được viết lại như thế này:

class Notification
{
   public $service;

   public function __construct(Service $service)
   {
      $this->service = $service;
   }

   public function send()
   {
      $this->service->doSomethingWithService();
      doSomethingToSend()
   }
}

//Và chúng ta sẽ thể hiện từng dịch vụ như sau

$mailNotification = new Notification(new MailService());
$slackNotification = new Notification(new SlackService());

Sau đó thì như mọi người đều biết, chúng ta có thể abstract hóa các service thông qua một interface. Nếu bạn đang sử dụng một PHP Framework có các bộ Container tự động (Ví dụ Laravel), bạn có thể đăng ký thể hiện với Service Container để hoàn thành trọn vẹn DI.

Đến đây chúng ta cơ bản đã hoàn thành Dependency Injection. Mình sẽ không bàn tới Service Container trong các framework, vì cái đó đã ở bên ngoài khái niệm DI cơ bản (Nhiều người vẫn có thể coi Service Container là một phần của DI).

Không phải khi nào DI cũng đúng

Không có cái gì trên đời là viên đạn bạc cả. Nghĩa là bạn không thể tìm ra một solution cho mọi vấn đề

Nếu bạn chỉ có một thể hiện duy nhất cho một interface, DI có thể ko cần thiết. DI sẽ rất thích hợp khi bạn cần thể hiện tính đa hình. Nếu bạn không cần điều này, hãy cân nhắc sử dụng DI.

DI có thể khiến ứng dụng của bạn trở nên cồng kềnh với quá nhiều các interface, các thể hiện gắn với các interface đó. Đôi khi, sự phụ thuộc “cứng” cũng có những lợi ích nhất định, và khiến code dễ maintain hơn. Inject everything không phải khi nào cũng hiệu quả. Chúng ta cần cân nhắc kỹ bài toán của mình trước khi triển khai một mẫu thiết kế nào đó.


Khóa học “Career Jumpstart with PHP” - Khởi động sự nghiệp với lập trình PHP đã quay trở lại, sắp ra mắt từ tháng 6/2022 với chương trình học mới, giáo trình cập nhật mới nhất.

-> mang tới lộ trình học vừa sức, dễ xin việc, tạo nền móng để tiến xa trên con đường sự nghiệp, phù hợp cho bất kì ai đang muốn tìm kiếm công việc lập trình đầu tiên.

Liên hệ tư vấn: Ms Mẫn - 0963023185 (zalo)