07/09/2018, 10:58

Tìm Hiểu Về Dependency Injection Qua Ví Dụ Cụ Thể

Dependeny Injection Là Gì Có nhiều định nghĩa khác nhau trên internet về Dependency Injection. Dưới đây là một định nghĩa mà theo mình thấy giải thích được một cách rõ ràng nhất thuật ngữ này: Dependeny Injection là một kiểu mẫu lập trình (design pattern) được sử dụng để cố gắng đạt được sự ...

Dependeny Injection Là Gì

Có nhiều định nghĩa khác nhau trên internet về Dependency Injection. Dưới đây là một định nghĩa mà theo mình thấy giải thích được một cách rõ ràng nhất thuật ngữ này:

Dependeny Injection là một kiểu mẫu lập trình (design pattern) được sử dụng để cố gắng đạt được sự không phụ thuộc giữa các object với nhau khi có mối quan hệ phụ thuộc giữa một object này với một object khác.

Dependency Injection Là Gì -  Giải Thích Sử Dụng Java

Tuy nhiên nếu sau khi đọc xong thuật ngữ trên bạn vẫn chưa hiểu gì thì hãy coi đây là chuyện bình thường vì các định nghĩa về một design pattern thường khá trừu tượng (abstract) mà không đi vào cụ thể. Việc đi sử dụng Dependency Injection như thế nào sẽ phụ thuộc vào cách triển khai trong từng tình huống cụ thể (cũng như kỹ năng lập trình của developer).

Câu Chuyện Về 1200 Dòng Code Thần Thánh

Bây giờ chúng ta sẽ tham khảo một ví dụ cụ thể có thể áp dụng Dependency Injection. Hãy xem xét trường hợp chúng ta tham gia phát triển ứng dụng quản lý tài khoản ngân hàng và cần thêmm tính năng để thông báo người dùng khi một giao dịch chuyển tiền được thực hiện từ tài khoản của họ.

Với yêu cầu như trên chúng ta tạo một class MailNotificationService đóng vai trò là một service thực việc thông báo qua mail như sau:

package vn.codehub.java.di.services;

public class MailNotificationService {

    public void notify(String to, String subject, String body) {
        // logic gửi email tới địa chỉ email người dùng
    }
}

Và class Account để theo dõi tài khoản của khách hàng cũng như lưu trữ thông tin cá nhân của khách hàng (như địa chỉ email, số ĐT...):

package vn.codehub.java.di;

public class Account {
    public String email;
    public String phone;

    public void transfer() {
        // logic xác thực người dùng
        // ...
        this.notifyTranferMoney();
    }
    public voi notifyTranferMoney() {
        // gửi thông báo về giao dịch chuyển tiền
        MailNotificationServer notificationService = new MailNotificationService();
        notificationService.notify(this.email, "Thông báo chuyển tiền", "Kính chào quý khách, quý khách đã thực hiện giao dịch...");
    }
}

Bây giờ hãy cũng phân tích xem đoạn code như trên có ổn hay không?

Trước tiên chúng ta để ý:

Class AccountController cần sử dụng MailNotificationService để thực hiện việc gửi email. Lúc này AccountController có một dependency là MailNotificationService. Hay nói cách khác AccountController phụ thuộc vào MailNotificationService.

Đoạn code mà chúng ta thấy ở trên vẫn chạy mượt mà và cũng sẽ chẳng có vấn đề gì cho tới một ngày đẹp trời ngân hàng đối thủ có tính năng thông báo qua SMS để giúp người dùng có thể chủ động theo dõi biến động trong tài khoản cũng như các thông báo để quảng cáo về các dịch vụ gia tăng khác.

Và sau buổi họp vào ngày đẹp trời không lâu sau đó, quản lý của bộ phận marketing quyết định đổi từ thông báo qua email bằng tính năng thông báo SMS.

Điều này có nghĩa chúng ta cần rà lại toàn bộ trong trong ứng dụng để tìm ra những đoạn code thực hiện việc gửi email thông báo sẽ được thay bằng gửi SMS thông báo. Tuy nhiên mọi chuyện không dừng ở đó vì team dev nhận ra đã sử dụng MailNotificationService không chỉ cho thông báo chuyển tiền mà do phương thức notify trong class này quá thần thánh nên nhiều developer đã sử dụng đi sử dụng lại ở các nơi khác nhau trong ứng dụng để gửi thông báo đủ các thể loại cho người dùng. Tìm sơ bộ thì bạn thấy có khoảng 1200 dòng có khởi tạo object từ MailNotificationService.

Chúng ta khai báo thêm một class MailNotificationService để thực việc gửi thông báo qua SMS như sau:

package vn.codehub.java.di.services;

public class SMSNotificationService {

    public void notify(String to, String subject, String body) {
        // logic gửi SML tới số điện thoại người dùng
    }
}

Việc tiếp theo cần làm là tìm ra toàn bộ code sử dụng MailNotificationService để thay bằng SMSNotificaitonService và sau đó build lại ứng dụng. Vấn đề là sếp cần bạn kiểm tra toàn bộ các tính năng liên quan mà 1200 dòng code trên có dính dáng tới. 1200 dòng code trước đây được coi là thần thánh thì giờ bỗng dưng biến thành 1200 dòng code trời đánh thánh vật.

Sử Dụng Dependency Injection

Câu hỏi đặt ra ở đây đó là như vậy thì có cách nào để khi thay đổi dependency của một object mà không ảnh hướng tới các object khác (hay nói rộng hơn là các thành phần khác) trong ứng dụng hay không? Câu trả lời là CÓ. Và chắc tới đây bạn cũng đoán ra đó là sử dụng Dependency Injection design pattern.

Bây giờ hãy xem xét trường hợp nếu bạn sử dụng Dependency Injection vào trường hợp trên như thế nào?

Bạn nghĩ sao về việc thay đổi UsersController thành giống như sau:

package vn.codehub.java.di;

public class Account {
    public String email;
    public String phone;
    public NotificationService notificationService;

    public function Account(String email, String phone, NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void transfer() {
        // logic xác thực người dùng
        // ...
        this.notifyTranferMoney();
    }
    public voi notifyTranferMoney() {
        // gửi thông báo về giao dịch chuyển tiền
        this.notificationService.notify(this.email, "Thông báo chuyển tiền", "Kính chào quý khách, quý khách đã thực hiện giao dịch...");
    }
}

Trong đó class NotificationService là một interface:

public interface NotificationService {
    public void notify(String to, String subject, String body);
}

Ở trên bạn chú ý bằng việc sử dụng NotificationService là một interface khi khởi tạo class Account chúng ta không ép client code buộc phải sử dụng MailNotificationService hay SMSNotificaitonService.

Tuy nhiên nếu chỉ dừng lại ở đây thì vẫn chưa có gì khác biệt so với cách làm trước nếu như khi khởi tạo object từ Account bạn vẫn truyền vào một object tao bởi MailNotificaitonService hoặc SMSNotificaitonService. Sự khác biệt thực sự rõ rệt khi bạn tạo một Injector thực hiện việc Inject dependency vào Account class.

Service Consumer

Tuy nhiên trước khi đi vào triển khai các injector chúng ta sẽ thống nhất với nhau rằng các object (hoặc class) sử dụng một trong hai service thông báo bằng email hoặc SML được gọi là consumer của các service này.

Chúng ta sẽ tạo một interface chhung cho các Consumer như sau:

public interface Consumer {
    public void notifyTranferMoney();

    // các method notify khác có sử dụng NotificationService là dependency 
    // ...
}

Service Injector

Sau khi có interface cho consumer chúng ta sẽ tiếp tục bằng việc khai báo một NotificationServiceInjector interface như sau:

public interface NotificationServiceInjector {
    public Consumer getConsumer();
}

Ở đoạn code trên phương thức getConsumer sẽ được dùng để trả ngược về object nào sử dụng service này. Hay nói cách khác phương thức này sẽ tham chiếu người lại về object có sử dụng dependency. Để hiểu rõ hơn về điều này chúng ta sẽ tạo tiếp một class EmailServiceInjector như sau:

public class EmailServiceInjector implements NotificationServiceInjector {
    @Override
    public Consumer getConsumer(String email, String phone) {
        return new Account(email, phone, new EmailNotificationService());
    }
}

Và tương tự SMSServiceInjector:

public class SMSServiceInjector implements NotificationServiceInjector {
    @Override
    public Consumer getConsumer(String email, String phone) {
        return new Account(email, phone, new SMSNotificationService());
    }
}

Với hai injector như trên chúng ta sẽ có thể dễ dàng inject dependency vào consumer thông qua phương thức getConsumer của từng các injector này:

public class Account {

    public static void main(String[] args) {
        // Send email
        injector = new EmailServiceInjector();
        account = injector.getConsumer("test account", "01224234");
        account.notifyTranferMoney();
    }
}

Tương tự chúng ta cũng có thể sử dụng SMSServiceInjector để inject SMSNotificaitonService vào Account. Trong các class injector bạn cũng có thể áp dụng một design pattern khác là Singleton để lấy ra consumer tuy nhiên để đơn giản thì ở đây tôi đã bỏ qua cách làm này.

Kết Luận

Tới đây chúng ta đã kết thúc việc tìm hiểu về Dependency Injection với ví dụ sử dụng ngôn ngữ lập trình Java. DI là một trong nhứng design pattern đang được sử dụng phổ biến gần đây đặc biệt là trong Spring framework. Nắm vững về Dependency Injection sẽ giúp cho việc quản lý và maintain code trở lên dễ dàng hơn.

0