Dependency Injection và Inversion of Control – Phần 2: Áp dụng DI vào code

Series bài viết Dependency Injection và Inversion of Control gồm 3 phần:

  1. Định nghĩa
  2. Áp dụng DI vào code
  3. Viết DI Container. Áp dụng DI vào ASP.NET MVC

Bạn đã đọc phần 1 nhưng vẫn chưa hiểu rõ lắm về DI, IoC, chưa biết cách áp dụng chúng vào code? Đừng lo, ở phần 2 này sẽ cung cấp những đoạn code mẫu, giải thích rõ hơn về những điều mình đã nói ở phần 1. Sau khi đọc xong phần này, các bạn quay lại phần 1 thì sẽ thấy “thông” ra được nhiều thứ nhé.

Dependency là gì?

Dependency là những module cấp thấp, hoặc cái service gọi từ bên ngoài. Với cách code thông thường, các module cấp cao sẽ gọi các module cấp thấp. Module cấp cao sẽ phụ thuộc và module cấp thấp, điều đó tạo ra các dependency.

ioc-and-mapper-in-c-4-638

Để dễ hiểu, hãy xem hàm Checkout của class Cart dưới đây. Hàm này sẽ lưu order xuống database và gửi email cho user. Class Cart sẽ khởi tạo và gọi module Database, module EmailSender, module Logger, các module này chính là các dependency.

    public class Cart
    {
        public void Checkout(int orderId, int userId)
        {
            Database db = new Database();
            db.Save(orderId);

            Logger log = new Logger();
            log.LogInfo("Order has been checkout");

            EmailSender es = new EmailSender();
            es.SendEmail(userId);
        }
    }

Cách làm này có gì sai không? Có vẻ là không, viết code cũng nhanh nữa. Nhưng cách viết này “có thể” sẽ dẫn tới một số vấn đề trong tương lai:

  • Rất khó test hàm Checkout này, vì nó dính dáng tới cả hai module Database và EmailSender.
  • Trong trường hợp ta muốn thay đổi module Database, EmailSender,… ta phải sửa toàn bộ các chỗ khởi tạo và gọi các module này. Việc làm này rất mất thời gian, dễ gây lỗi.
  • Về lâu dài, code sẽ trở nên “kết dính”, các module có tính kết dính cao, một module thay đổi sẽ kéo theo hàng loạt thay đổi. Đây là nỗi ác mộng khi phải maintainance code.

Inversion of Control và Dependency Injection đã ra đời để giải quyết những vấn đề này.

Làm sao để hạn chế coupling giữa các class. Đã có Inversion of Control

Để các module không “kết dính” với nhau, chúng không được kết nối trực tiếp, mà phải thông qua interface. Đó cũng là nguyên lý cuối cùng trong SOLID.

1. Các module cấp cao không nên phụ thuộc vào các modules cấp thấp. Cả 2 nên phụ thuộc vào abstraction.

2. Interface (abstraction) không nên phụ thuộc vào chi tiết, mà ngược lại. ( Các class giao tiếp với nhau thông qua interface, không phải thông qua implementation.)

Ta lần lượt tạo các interface IDatabase, IEmailSender, ILogger, các class kia ban đầu sẽ lần lượt kế thừa những interface này. Để dễ hiểu, giờ mình sẽ tạm gọi  IDatabase, IEmailSender, ILogger là Interface, các class như Database, EmailSender, Logger là Module.

    // Interface
    public interface IDatabase
    {
        void Save(int orderId);
    }

    public interface ILogger
    {
        void LogInfo(string info);
    }

    public interface IEmailSender
    {
        void SendEmail(int userId);
    }

    // Các Module implement các Interface
    public class Logger : ILogger
    {
        public void LogInfo(string info)
        {
            //...
        }
    }
    
    public class Database : IDatabase
    {
        public void Save(int orderId)
        {
            //...
        }
    }

    public class EmailSender : IEmailSender
    {
        public void SendEmail(int userId)
        {
            //...
        }
    }

Hàm checkout mới sẽ trông như sau:

        public void Checkout(int orderId, int userId)
        {
            // Nếu muốn thay đổi database, ta chỉ cần thay dòng code dưới
            // Các Module XMLDatabase, SQLDatabase phải implement IDatabase
            //IDatabase db = new XMLDatabase(); 
            //IDatebase db = new SQLDatabase();
            IDatabase db = new Database();
            db.Save(orderId);

            ILogger log = new Logger();
            log.LogInfo("Order has been checkout");

            IEmailSender es = new EmailSender();
            es.SendEmail(userId);
        }

Với interface, ta có thể dễ dàng thay đổi, swap các module cấp thấp mà không ảnh hưởng tới module Cart. Đây là bước đầu của IoC.

Để dễ quản lý, ta có thể bỏ tất cả những hàm khởi tạo module vào constructor của class Cart.

    public class Cart
    {
        private readonly IDatabase _db;
        private readonly ILogger _log;
        private readonly IEmailSender _es;

        public Cart()
        {
            _db = new Database();
            _log = new Logger();
            _es = new EmailSender();
        }

        public void Checkout(int orderId, int userId)
        {
            _db.Save(orderId);
            _log.LogInfo("Order has been checkout");
            _es.SendEmail(userId);
        }
    }

Cách này thoạt nhìn khá khá ổn. Tuy nhiên, nếu có nhiều module khác cần dùng tới Logger, Database, ta lại phải khởi tạo các Module con ở constructor của module đó. Có vẻ không ổn phải không nào?

ioc-and-mapper-in-c-6-638

Ban đầu, người ta dùng ServiceLocator để giải quyết vấn đề này. Với mỗi Interface, ta set một Module tương ứng. Khi cần dùng, ta sẽ lấy Module đó từ ServiceLocator. Đây cũng là một cách để hiện thực IoC.

    public static class ServiceLocator
    {
        public static T GetModule()
        {
            //....
        }
    }

    //Ta chỉ việc gọi hàm GetModule
    public class Cart 
    {
        public Cart()
        {
            _db = ServiceLocator.GetModule();    
            _log = ServiceLocator.GetModule();     
            _es = ServiceLocator.GetModule(); 
        }
    }

Cách này vẫn còn khuyết điểm: toàn bộ các class đều phụ thuộc vào ServiceLocator.

Dependency Injection giải quyết được vấn đề này. Các Module cấp thấp sẽ được inject (truyền vào) vào Module cấp cao thông qua Constructor hoặc thông qua Properties. Nói một cách đơn giản dễ hiểu về DI:

Ta không gọi toán tử new để khởi tạo instance, mà instance đó sẽ được truyền từ ngoài vào (Truyền manual, hoặc nhờ DI Container).

ioc-and-mapper-in-c-8-638

Sau khi áp dụng Dependency Injection, ta sẽ sử dụng class Cart như sau:

    public Cart(IDatabase db, ILogger log, IEmailSender es)
    {
            _db = db;
            _log = log;
            _es = es;
     }

     //Dependency Injection một cách đơn giản nhất
     Cart myCart = new Cart(new Database(),
                       new Logger(), new EmailSender());
     //Khi cần thay đổi database, logger
     myCart = new Cart(new XMLDatabase(),
                  new FakeLogger(), new FakeEmailSender());

Chắc bạn nghĩ: Sau khi dùng Dependency Injection thì cũng phải khởi tạo Module à, thế thì còn dở hơn ServiceLocator rồi. Thông thường, ta sử dụng DI Container. Chỉ việc define một lần, DI Container sẽ tự thực hiện việc inject các module cấp thấp vào module cấp cao.

            //Với mỗi Interface, ta define một Module tương ứng
            DIContainer.SetModule<IDatabase, Database>();
            DIContainer.SetModule<ILogger, Logger>();
            DIContainer.SetModule<IEmailSender, EmailSender>();

            DIContainer.SetModule<Cart, Cart>();

            //DI Container sẽ tự inject Database, Logger vào Cart
            var myCart = DIContainer.GetModule(); 

            //Khi cần thay đổi, ta chỉ cần sửa code define
            DIContainer.SetModule<IDatabase, XMLDatabase>();

Sau khi áp dụng Dependency Injection, code bạn sẽ dài hơn, có vẻ “phức tạp” hơn và sẽ khó debug hơn. Đổi lại, code sẽ uyển chuyển, dễ thay đổi cũng như dễ test hơn.

Như mình đã nói ở bài trước, không phải lúc nào DI cũng là lựa chọn phù hợp, ta cần cân nhắc các ưu khuyết điểm. DI được áp dụng trong nhiều framework back-end (ASP.MVC, Struts2) lẫn front-end (AngularJS, KnockoutJS). Đa phần các dự án lớn trong các công ty IT đều áp dụng DI, do đó những kiến thức về DI sẽ rất hữu ích khi phỏng vấn cũng như làm việc.

angular

Vậy cái DI Container phía trên ở đâu ra? Ta có thể tự viết, hoặc sử dụng một số DI Container phổ biến trong C# như: Unity, StructureMap, NInject. Ở phần 3, mình sẽ hướng dẫn cách viết 1 DI Container đơn giản và dùng các DI Container sẵn có  nhé.

14 thoughts on “Dependency Injection và Inversion of Control – Phần 2: Áp dụng DI vào code”

  1. Bài viết rất hay, thanks bạn.

    Nhưng mình có 1 thắc mắc. Ví dụ mình có nhiều class Database như XMLDatabse, SQLDatabase… cùng implement interface IDatabase. Vậy trong DI mình phải config thế nào để tùy trường hợp mình sẽ dùng các Database khác nhau.

    Thanks

    Like

    1. Trên bài có nói về việc dùng DI Container:
      //Trong trường hợp cần thay đổi
      DIContainer.SetModule();
      Như vậy bạn chì cần thay đổi XMLDatabase thành tên database bạn cần dùng thôi.

      Like

      1. Hi bạn, ý mình là trong project mình sẽ dùng nhiều loại database khác nhau. Sẽ có feature mình dùng XMLDatabase để lưu có feature dùng SQLDatabase. Nhưng lúc config DI thì chỉ config đc 1 loại. Vậy thì khi chương trình chạy thì làm sao switch Database.

        Like

    2. Mình cũng thấy cách set Interface cho một Implement không ổn chút nào.
      Hồi đó dùng Spring có file xml để config được tự do chỉ định Implement với Interface cho một class nào đó.
      Giờ thì dùng Phalcon DI (PHP) cách làm rất lạ như khá thú vị và hiệu quả.

      Like

  2. “Hi bạn, ý mình là trong project mình sẽ dùng nhiều loại database khác nhau. Sẽ có feature mình dùng XMLDatabase để lưu có feature dùng SQLDatabase. Nhưng lúc config DI thì chỉ config đc 1 loại. Vậy thì khi chương trình chạy thì làm sao switch Database.”

    Mình cũng thấy cách set Interface cho một Implement không ổn chút nào.
    Hồi đó dùng Spring có file xml để config được tự do chỉ định Implement với Interface cho một class nào đó.
    Giờ thì dùng Phalcon DI (PHP) cách làm rất lạ như khá thú vị và hiệu quả (https://docs.phalconphp.com/en/latest/reference/di.html)

    Like

  3. – e chưa hiểu câu này lắm ạ: DI Container sẽ tự thực hiện việc inject các module cấp thấp vào module cấp cao

    – cái ví dụ này:
    //Với mỗi Interface, ta define một Module tương ứng
    DIContainer.SetModule();
    DIContainer.SetModule();
    DIContainer.SetModule();

    DIContainer.SetModule();

    //DI Container sẽ tự inject Database, Logger vào Cart
    var myCart = DIContainer.GetModule();

    //Khi cần thay đổi, ta chỉ cần sửa code define
    DIContainer.SetModule();

    sao Database, Logger lại inject được vào Cart vậy ạ?

    Like

  4. //DI Container sẽ tự inject Database, Logger vào Cart
    var myCart = DIContainer.GetModule();
    ————–
    Khúc này em chưa hiểu lắm anh ơi, làm sao mà nó tự inject vô được vậy anh @@?

    Like

Leave a comment