Viblo CTF
+3

Dependency Injection

Là một lập trình viên, chắc chắn bạn đã từng nghe, biết đến khái niệm Dependency Injection và tác dụng mà nó đem lại. Nhiều người sợ rằng khi dùng Dependency Injection, họ sẽ mất nhiều thời gian để xây dựng kiến trúc phần mềm mà không thực sự thấy được ý nghĩa của nó. Trong bài viết này, tôi sẽ đưa ra và phân tích một vài lý do mà chúng ta nên dùng Dependency Injection.

1. Dependency Injection là gì?

Wikipedia định nghĩa về Dependency Injection:

In software engineering, dependency injection is a technique in which an object receives other objects that it depends on. These other objects are called dependencies. In the typical "using" relationship[1] the receiving object is called a client and the passed (that is, "injected") object is called a service. The code that passes the service to the client can be many kinds of things and is called the injector. Instead of the client specifying which service it will use, the injector tells the client what service to use. The "injection" refers to the passing of a dependency (a service) into the object (a client) that would use it.

Hiểu đơn giản thì Dependency Injection là một mô hình lập trình, cách tổ chức code sao cho các đoạn code khác nhau, các module khác nhau, các class khác nhau không phụ thuộc nhau một cách cứng nhắc, mà cần có một cơ chế thay đổi các thành phần phụ thuộc cả ở thời điểm chạy và thời điểm biên dịch.

Khi sử dụng mô hình Dependency Injection ta có thể dễ dàng module hóa ứng dụng (modular), test (testable) và bảo trì (maintainable).

  • Modular : giúp tạo các class hoặc mô-đun hoàn toàn tư cung cấp, độc lập nhau.
  • Testable : giúp viết test dễ dàng, ví dụ như unittest.
  • Maintainable : Vì mỗi lớp trở thành module, việc quản lý nó trở nên dễ dàng hơn.

Tất cả các project đều có các thành phần phụ thuộc vào nhau, dự án càng lớn thì càng nhiều thành phần phụ thuộc, Dependency Injection giúp cho quản lý các thành phần phụ thuộc này tốt nhất.

2. Vấn đề

Khi làm việc, chúng ta luôn luôn gặp phải sự phụ thuộc giữa các thành phần trong code.

Ví dụ trong lập trình thủ tục:

function getUsers() {
     global $database;
     return $database->getAll('users');
}

Ở đây hàm getUsers() có phụ thuộc vào biến $database qua việc lấy ra tất cả user trong cơ sở dữ liệu. Như vậy sẽ có một số vấn đề sau:

  • Hàm getUsers() cần sử dụng biến $database, nghĩa là phải kết nối được với cơ sở dữ liệu. Có kết nối thành công với cơ sở dữ liệu thì hàm này mới chạy đúng.
  • Biến $database là biến global nên rất có thể nó bị ghi đè bởi một số thư viện hoặc đoạn code khác trong cùng phạm vi.

Với vấn đề đầu tiên, bạn có thể đã sử dụng các cấu trúc try-catch, nhưng nó vẫn không giải quyết được vấn đề thứ hai.

Ví dụ khác cho một class:

class User 
{
    private $database = null;

    public function __construct() {
        $this->database = new Database('host', 'user', 'password', 'dbname');
    }

    public function getUsers() {
        return $this->database->getAll('users');
    }
}

$user = new User();
$user->getUsers();

Đoạn code này lại có những vấn đề sau:

  1. Class User có phụ thuộc ngầm với database. Tất cả các phụ thuộc phải luôn luôn rõ ràng. Điều này đi ngược với nguyên tắc Dependency inversion principle.

  2. Nếu ta muốn thay đổi thông tin cơ sở dữ liệu, ta cần chỉnh sửa class User. Như vậy rất bất tiện, mỗi class nên hoàn toàn modular hoặc như hộp đen. Nếu ta cần tác động nhiều hơn với class, ta nên sử dụng các thuộc tính và phương thức public của nó thay vì chỉnh sửa class nhiều lần. Điều này đi ngược nguyên tắc Open/closed principle

  3. Giả sử hệ thống hiện tại đang sử dụng MySQL. Nếu được yêu cầu phải thay đổi hệ quản trị cơ sở dữ liệu khác thì ta phải làm như thế nào? Ta phải sửa lại toàn bộ những phần code đã dùng MySQL 😃))

  4. Class User không nhất thiết cần phải biết về kết nối cơ sở dữ liệu, nó chỉ nên chứa chức năng riêng. Vì vậy, viết code kết nối cơ sở dữ liệu trong class User không làm module hóa được nó. Điều này đi ngược với nguyên tắc Single responsibility principle. Giống như thế giới thực, mỗi đối tượng của một lớp chỉ nên có nhiệm vụ cụ thể của riêng mình.

  5. Việc viết Unittest cho class User sẽ trở nên khó khăn hơn vì chúng ta đang khởi tạo cơ sở dữ liệu bên trong hàm khởi tạo của nó. Như vậy, không thể viết test cho class User mà không viết test cho cơ sở dữ liệu trước.

Thật sự có quá nhiều vấn đề phải không nào 😃

Áp dụng Dependency Injection

Ví dụ sử dụng Dependency Injection cho class User:

class User 
{
    private $database = null;

    public function __construct(Database $database) {
        $this->database = $database;
    }

    public function getUsers() {
        return $this->database->getAll('users');
    }
}

$database = new Database('host', 'user', 'pass', 'dbname');
$user = new User($database);
$user->getUsers();

Như vậy, thay vì ta phải khởi tạo database trong hàm khỏi tạo của User:

$this->database = new Database('host', 'user', 'password', 'dbname');

thì sẽ truyền $database như một biến của hàm khởi tạo Model:

public function __construct(Database $database)

và User sẽ được khởi tạo bằng cách:

$user = new User($database);

Bây giờ ta sẽ đi phân tích xem Dependency Injection giúp giải quyết các vấn đề bên trên như thế nào nhé.

  • Vấn đề 1: Ta đã làm cho sự phụ thuộc của User vào cơ rsowr dữ liệu trở nên rõ ràng khi truyền database vào như 1 biến của hàn khởi tạo User.
public function __construct(Database $database)
  • Vấn đề 2: Class User bây giờ không cần quan tâm đến cách kết nối cơ sở dữ liệu nữa. Thứ duy nhất cần là 1 instance của Database. Khi thông tin kết nối thay đổi, ta cũng không cần phải sửa lại code của class User nữa. Ta sẽ cung cấp cho nó những gì nó cần.
  • Vấn đề 3: Vì ta không còn khởi tạo cơ sở dữ liệu trong User nữa nên class User cũng không cần biết loại cơ sở dữ liệu nào được sử dụng. Database sẽ phải là nơi viết những thứ cần thiết cho việc thay đổi cơ sở dữ liệu và chuyển đến class User. Ví dụ, ta có thể tạo 1 interface thực thi các phương thức chung cho tất cả các loại cơ sở dữ liệu khác nhau mà chúng đều phải thực hiện.
  • Vấn đề 4: Khi dùng Dependency Injection, class User không cần biết việc kết nối cơ sở dữ liệu như thế nào nữa. Nó chỉ cần một instance của Database đã kết nối thành công mà thôi.
  • Vấn đề 5: Nếu bạn đã từng biết Unittest thì bán sẽ thấy rằng việc viết test sẽ đơn giản hơn khi bạn dùng Dependency Injection. Unittest là test đơn vị mã nguồn, yêu cầu các đơn vị này phải độc lập nhau. Khi đó, bạn sẽ thấy rất đơn giản để test bằng cách giả lập các đối tượng phụ thuộc (ví dụ dùng Mockery).

3. Phân loại

Như vậy là chúng ta đã hiểu được các tác dụng cơ bản của Dependency Injection. Hãy xem các cách sử dụng nó nhé.

Có 3 cách Dependency Injection:

  • Constructor Injection
  • Setter Injection
  • Interface Injection

Constructor Injection

Ví dụ ở bên trên chính là ví dụ cho Constructor Injection. Constructor Injection nên dùng khi:

  • Các thành phần phụ thuộc rất cần thiết, không thể làm việc mà không có thành phần này.
  • Vì hàm constructor chỉ được gọi tại thời điểm khởi tạo 1 lớp, ta phải đảm bảo rằng các thành phần phụ thuộc không thể thay đổi khi đối tượng được sử dụng.

Tuy nhiên, Constructor Injection có 1 nhược điểm là vì trong hàm khởi tạo đối tượng có các thành phần phụ thuộc nên khó khi muốn mở rộng hay ghi đè nó trong các lớp con. Setter Injection

Ví dụ:

class User 
{
    private $database = null;

    public function setDatabase(Database $database) {
        $this->database = $database;
    }

    public function getUsers() {
        return $this->database->getAll('users');
    }
}

$database = new Database('host', 'user', 'pass', 'dbname');
$user = new User();
$user->setDatabase($database);
$user->getUsers();

Ở ví dụ này, ta dùng hàm setter setDatabase() để thêm thành phần phụ thuộc Database vào class User. Nếu ta cần 1 thành phần phụ thuộc khác thì ta có thể tạo thêm 1 hàm setter khác với cách làm tương tự.

Như vậy, Setter Injection có tác dụng:

  • Cho phép tùy chọn các phụ thuộc của lớp và lớp có thể tạo ra với các giá trị mặc định khác nhau.
  • Thêm các thành phần phụ thuộc một cách đơn giản thông qua hàm setter mà không làm hỏng logic của code.

Interface Injection Trong cách này, định nghĩa một interface sao cho các thành phần phụ thuộc đều được tích hợp trong interface.

Ví dụ:

interface SomeInterface {
    function getUsers(Database $database);
}

Như vậy, bất kỳ một class nào cần implement interface SomeInterface sẽ phải cung cấp phụ thuộc Database vào hàm getUsers().

Tổng kết

Ở bài viết này, mình đã đưa ra và giải thích một số lý do mà chũng ta nên sử dụng Dependency Injection trong việc xây dựng chương trình và các cách dùng nó. Mong bài viết có giúp ích cho các bạn.

Tài liệu tham khảo

https://en.wikipedia.org/wiki/Dependency_injection

https://codeinphp.github.io/post/dependency-injection-in-php/

https://xuanthulab.net/di-dependency-injection-trong-php.html


All Rights Reserved