Đôi lời gửi gắm trước bài viết Smile 

Xin gửi lời cảm ơn tới Matebe team đã tạo ra một nơi để chúng ta chia sẻ kiến thức và cũng là nơi khẳng định được trình độ chúng ta cho các HR và các công ty. Và lời cảm ơn đặc biệt tới leader Matebe Wink , cũng là người truyền cho mình rất nhiều cảm hứng. 

Đến hôm nay Matebe lại tiếp tục cập nhật thêm nhiều tính năng mới Smile . Thuận tiện hơn khi viết article và các tính năng cũng ngày càng thể hiện sự tốt nhất cho mọi người có thể tìm kiếm các bài viết liên quan tới công việc - ngành nghề của từng cá nhân một cách hữu ích. 

Mừng cập nhật chức năng mới mình cũng gửi thêm một bài viết nữa liên quan đến Depency Injection và Depency Inversion. Bài viết này được lấy từ Tôi Đi Code Dạo , nhằm đưa đến cho các bạn chưa biết blog này và cũng gửi cho một số bạn đang làm việc với Depency Injection. Và nói thật là các bài viết hiện nay mình vẫn đang đi copy của người khác và share cho mọi người đọc chứ trình độ của mình thì còn khá kém chưa viết được những bài riêng, mong mọi người đừng chê cười Smile 


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.


 


Depency injection và depency inversion


Để 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?


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).



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.



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é.

Cảm ơn tác giả Phạm Huy Hoàng đã có một bài viết hay về DI. Sẽ còn một phần nữa cho vấn đề code của DI :D

P/s: Mình đang tính viết vài bài về cấu trúc dữ liệu và giải thuật do mình đúc kết lại vì trình độ thấp nên cũng sẽ viết một số cái thấp cho các bạn sinh viên đọc hoặc ôn lại ít, nhưng quan trọng nhất đó là nền tảng của nền tảng :D . Không biết ai quan tâm không :D 





 


 


 


 

Login để lấy link download