Chương 11: System - Phần 2

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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
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.
1
2
3
4
5
6
7
8
9
10
11
12
//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é.

Comments

Popular Posts