logo

Thông báo

Icon
Error

Chia sẻ
Tùy chọn
Xem
Xem bài viết cuối
Offline admin  
#1 Đã gửi : 28/04/2017 lúc 03:53:13(UTC)
admin

Danh hiệu: Administration

Chức danh:

Nhóm: Administrators
Gia nhập: 23-07-2013(UTC)
Bài viết: 6,108
Man
Viet Nam
Đến từ: Vietnam

Cảm ơn: 10 lần
Được cảm ơn: 2 lần trong 2 bài viết

DEPENDENCY INJECTION vs INVERSION OF CONTROL

Dependency Injection vs Inversion of Control

Định nghĩa và khái niệm DI

Hiện nay, các lập trình viên hay lẫn lộn giữa các khái niệm Dependency Inversion, Inversion of Control (IoC), Dependency Injection (DI). Ba khái niệm này tương tự nhau nhưng không hoàn toàn giống nhau.

Dependency Injection vs Inversion of Control

Sự khác biệt giữa 3 khái niệm trên:

  • Dependency Inversion: Đây là một nguyên lý để thiết kế và viết code.
  • Inversion of Control: Đây là một design pattern được tạo ra để code có thể tuân thủ nguyên lý Dependency Inversion. Có nhiều cách hiện thực pattern này: ServiceLocator, Event, Delegate, … Dependency Injection là một trong các cách đó.
  • Dependency Injection: Đây là một cách để hiện thực Inversion of Control Pattern (Có thể coi nó là một design pattern riêng cũng được). Các module phụ thuộc (dependency) sẽ được inject vào module cấp cao.

Khi nói tới DI, tức là nói tới Depedency Injection. Hiện nay, một số DI container như Unity, StructureMap v…v, hỗ trợ chúng ta trong việc cài đặt và áp dụng Dependency Injection vào code (Sẽ nói ở bài sau), tuy nhiên vẫn có thể gọi chúng là IoC Container, ý nghĩa tương tự nhau.

Có thể hiểu Dependency Injection một cách đơn giản như sau:

  1. Các module không giao tiếp trực tiếp với nhau, mà thông qua interface. Module cấp thấp sẽ implement interface, module cấp cao sẽ gọi module cấp thấp. Ví dụ: Để giao tiếp với database, ta có interface IDatabase, các module cấp thấp là XMLDatabaseSQLDatabase. Module cấp cao là CustomerBusiness sẽ sử dụng interface IDatabase.
  2. Việc khởi tạo các module cấp thấp sẽ do DI Container thực hiện. Ví dụ: Trong module CustomerBusiness, ta sẽ không khởi tạo IDatabase db = new XMLDatabase(), việc này sẽ do DI Container thực hiện. Module CustomerBusiness sẽ không biết gì về module XMLDatabase hay SQLDatabase.
  3. Việc Module nào gắn với interface nào sẽ được config trong code hoặc trong file XML.
  4. DI được dùng để làm giảm sự phụ thuộc giữa các module, dễ dàng hơn trong việc thay đổi module, bảo trì code và testing.

Các dạng DI

Có 3 dạng Dependency Injection:

  1. Constructor Injection: Các dependency sẽ được container truyền vào (inject vào) 1 class thông qua constructor của class đó. Đây là cách thông dụng nhất.
  2. Setter Injection: Các dependency sẽ được truyền vào 1 class thông qua các hàm Setter.
  3. Interface Injection: Class cần inject sẽ implement 1 interface. Interface này chứa 1 hàm tên Inject. Container sẽ injection dependency vào 1 class thông qua việc gọi hàm Inject của interface đó. Đây là cách rườm rà và ít được sử dụng nhất.

Ưu điểm và khuyết điểm của DI

Dĩ nhiên, DI không phải vạn năng, nó cũng có những ưu điểm và khuyết điểm, do đó không phải project nào cũng nên áp dụng DI. Với những dự án lớn, code nhiều, DI là thứ rất cần thiền để đảm bảo code dễ bảo trì, dễ thay đổi. Vì vậy, bản thân các framework nổi tiếng như Spring, Struts2, ASP.NET MVC, … đều hỗ trợ hoặc tích hợp sẵn DI. ASP.NET MVC từ bản 5 trở xuống cho phép ta sử dụng DI container từ thư viện, từ bản 6 thì tích hợp sẵn DI luôn, không cần phải thêm thư viện gì.

ƯU ĐIỂMKHUYẾT ĐIỂM
  • Giảm sự kết dính giữa các module
  • Code dễ bảo trì, dễ thay thế module
  • Rất dễ test và viết Unit Test
  • Dễ dàng thấy quan hệ giữa các module (Vì các dependecy đều được inject vào constructor)
  • Khái niệm DI khá “khó tiêu”, các developer mới sẽ gặp khó khăn khi học
  • Sử dụng interface nên đôi khi sẽ khó debug, do không biết chính xác module nào được gọi
  • Các object được khởi tạo toàn bộ ngay từ đầu, có thể làm giảm performance
  • Làm tăng độ phức tạp của code

TIẾN TRÌNH CỤ THỂ ỨNG DỤNG IOC & DEPENDENCY INJECTION

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.

Tại sao phải sử dụng Dependecy Injection:

Khi module cấp thấp thay đổi, module cấp cao phải thay đổi theo. Một thay đổi sẽ kéo theo hàng loạt thay đổi, giảm khả năng bảo trì của code.

Nếu tuân theo Dependendy Inversion principle, các module cùng phụ thuộc vào 1 interface không đổi. Ta có thể dễ dàng thay thế, sửa đổi module cấp thấp mà không ảnh hưởng gì tới module cấp cao.

Cách thức làm việc của Dependency Inversion: 

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

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

Mã:
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.

Mã:
// 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:

Mã:
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.

Mã:
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.

Mã:
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:

Mã:
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.

Mã:
//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.

Để hiểu nguyên lý hoạt động của chúng, mình sẽ cùng các bạn cách viết một DI Container đơn giản (chúng cũng không quá “ghê gớm” hay phức tạp như bạn nghĩ đâu). Sau đó mình sẽ hướng dẫn cách sử dụng cái DI Container có sẵn, cũng như áp dụng IoC và project MVC.

1. Tự viết 1 DI Container đơn giản

Các bạn có thể dùng git để clone project về máy và bắt đầu làm theo mình: https://github.com/ToiDiCodeDaoSampleCode/SimpleIoC. Các class và interface vẫn như trong phần 2, có điều mình đã bổ sung thêm 1 số class mock – module giả. Trong thực tế, ta sử dụng các class mock này để viết Unit Test.

SimpleIoC

DI Container thường có 1 function dùng để setup module và interface, một function khác để lấy module dựa theo interface. Ở đây mình gọi 2 function đó là SetModule và GetModule.

Mã:
public class DIContainer
{
    public static void SetModule<TInterface, TModule>()
    {
        SetModule(typeof(TInterface), typeof(TModule));
    }
 
    public static T GetModule<T>()
    {
        return (T)GetModule(typeof(T));
    }
}

Code của hàm Main cũng rất đơn giản. Ta chỉ cài đặt các interface và module tương ứng thông qua function SetModule. Với class Cart, ta chỉ cần gọi hàm GetModule. DIContainer sẽ tự inject IDatabase, ILogger vào theo code ta đã viết.

Mã:
//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<Cart>(); 

Class DI Container sẽ có các đặc tính sau:

  • Lưu trữ các Interface, Module tương ứng vào một Dictionary có Key là Interface, Value là Module. Để lấy một Module từ Container, ta cần đưa vào Interface của Module đó.
  • Khi cài đặt một module, container sẽ tìm Constructor đầu tiên của module đó.
  • Nếu contructor không có tham số (Module không có dependency), container sẽ gọi constructor này để khởi tạo module.
  • Nếu constructor này có tham số (Có dependency), container sẽ khởi tạo các tham số này, gán chúng vào constructor của module. Đây là quá trình injection.

Việc implement cũng không phức tạp lắm, bạn đọc code và comment sẽ hiểu thôi.

Mã:
public class DIContainer
{
    //Dictionary để chứa các interface và module tương ứng
    private static readonly Dictionary<Type, object> 
               ResgisteredModules = new Dictionary<Type, object>();
 
    //Hai hàm cơ bản, ở đây mình chuyển <T> thành 
    //dạng Type trong C# để dễ viết code
    public static void SetModule<TInterface, TModule>()
    {
        SetModule(typeof(TInterface), typeof(TModule));
    }
 
    public static T GetModule<T>()
    {
        return (T)GetModule(typeof(T));
    }
 
 
    private static void SetModule(Type interfaceType, Type moduleType)
    {
        //Kiểm tra module đã implement interface chưa
        if (!interfaceType.IsAssignableFrom(moduleType))
        {
            throw new Exception("Wrong Module type");
        }
 
        //Tìm constructor đầu tiên
        var firstConstructor = moduleType.GetConstructors()[0];
        object module = null;
        //Nếu như không có tham số
        if (!firstConstructor.GetParameters().Any())
        {
            //Khởi tạo module
            module = firstConstructor.Invoke(null); // new Database(), new Logger()
        }
        else
        {
            //Lấy các tham số của constructor
            var constructorParameters = firstConstructor.GetParameters(); //IDatebase, ILogger
 
            var moduleDependecies = new List<object>();
            foreach (var parameter in constructorParameters)
            {
                var dependency = GetModule(parameter.ParameterType); //Lấy module tương ứng từ DIContainer
                moduleDependecies.Add(dependency);
            }
 
            //Inject các dependency vào constructor của module
            module = firstConstructor.Invoke(moduleDependecies.ToArray());
        }
        //Lưu trữ interface và module tương ứng
        ResgisteredModules.Add(interfaceType, module);
    }
 
    private static object GetModule(Type interfaceType)
    {
        if (ResgisteredModules.ContainsKey(interfaceType))
        {
            return ResgisteredModules[interfaceType];
        }
        throw new Exception("Module not register");
    }
}

Kết quả:

DI Container

2. Sử dụng DI Container từ các framework có sẵn

Tất nhiên, nếu người khác đã viết sẵn, kiểm thử và fix lỗi, chúng ta có thể tái sử dụng mà không cần phải viết lại từ đầu cho mệt. Mình sẽ hướng dẫn các bạn sử dụng DI Container của Ninject và Unity.

Dùng Nuget (hoặc Package Manager Console) để cài đặt Ninject và Unity. Nhấp chuột phải vào project SimpleIoC, chọn Manage Nuget packages.

Nuget (OR Package Manager Console) de cai Ninject and Unity

Nuget (OR Package Manager Console) de cai Ninject and Unity

Vì bạn đã chia code ra thành các module và interface rồi, ta chỉ cần thay code của DIContainer thành code Unity và Ninject là được.

Ninject

Mã:
var kernel = new StandardKernel();
kernel.Bind<IDatabase>().To<Database>();
kernel.Bind<ILogger>().To<Logger>();
kernel.Bind<IEmailSender>().To<EmailSender>();
kernel.Bind<Cart>().To<Cart>();
 
//DI Container sẽ tự inject Database, Logger vào Cart
var myCart = kernel.Get<Cart>();

Unity

Mã:
var container = new UnityContainer();
container.RegisterType<IDatabase, Database>();
container.RegisterType<ILogger, Logger>();
container.RegisterType<IEmailSender, EmailSender>();
container.RegisterType<Cart, Cart>();
 
//DI Container sẽ tự inject Database, Logger vào Cart
var myCart = container.Resolve<Cart>();

3. Áp dụng IoC vào project MVC

Đa phần các framework như Spring.NET, ASP.NET MVC, Spring, Struts đều có sẵn DI Container, hoặc cho phép tích hợp DI Container bên ngoài. Vì đây là blog C# nên mình sẽ hướng dẫn cách tích hợp IoC vào project MVC. Mọi công đoạn chỉ mất từ 2-5 phút. (WebForm không áp dụng IoC được vì Page Cycle khá phức tạp, không cho phép ta can thiệp vào quá trình khởi tạo page).3. Áp dụng IoC vào project MVC

Bước 1. Tạo 1 project MVC trong cùng Solution. Add References tới project SimpleIoC để tái sử dụng class cho tiện.

Áp dụng IoC vào project MVC

Bước 2. Thêm parameter vào constructor của controller, module Cart sẽ được inject vào. Nếu chạy thử bạn sẽ bị lỗi “No paramterless constructor …”

No paramterless constructor

No paramterless constructor error

Bước 3. Sử dụng Nuget, tìm và cài Unity Bootstraper for ASP.NET MVC (Các DI Container khác như NinjectStructureMap đều có Bootstraper cho MVC cả, đừng lo). Ta sẽ thấy có file mới tên là UnityConfig được tạo ra.

Ta vào file này và thực hiện việc setup các interface và module. Tùy theo phiên bản MVC mà nội dung các file tạo ra có khác nhau chút đỉnh. Tuy nhiên cú pháp cài đặt vẫn như cũ.

Unity Bootstraper for ASP.NET MVC

Bước 4. Chạy thử và … xong. Chúc mừng bạn đã hoàn thành bài viết 3 phần về IoC đầy khó khăn và gian khổ.

viet code IOC​​​​​​​

Nhờ IoC và mock, ta có thể Unit Test từng module riêng lẻ. Các bạn có thể xem thêm ở đây vì có bạn đã viết khá chi tiết rồi nên mình không viết lại nữa. Nếu có góp ý hay gạch đá gì các bạn cứ ném thoải mái trong phần comment nhé.

Ai đang xem chủ đề này?
OceanSpiders 2.0
Chủ đề tương tự
ASP.NET Core Dependency Injection (.NET Core)
Bởi admin 10-12-2018 lúc 10:22:46(UTC)
Dependency Injection with Web API and Autofac (Web API)
Bởi admin 23-02-2016 lúc 05:03:12(UTC)
Dependency Injection (Application Patterns)
Bởi admin 23-02-2016 lúc 11:52:54(UTC)
Dependency Injection (Object-oriented programing)
Bởi admin 22-05-2015 lúc 09:29:14(UTC)
Managing Dependency Injection with Autofac (Object-oriented programing)
Bởi admin 13-04-2015 lúc 05:11:35(UTC)
Simple Dependency Injection (Object-oriented programing)
Bởi admin 13-04-2015 lúc 05:09:07(UTC)
AngularJS Modularization & Dependency Injection (Angular Modules)
Bởi admin 17-03-2015 lúc 10:59:04(UTC)
Dependency Injection (Dependency Injection in Angular)
Bởi admin 16-03-2015 lúc 04:20:57(UTC)
Di chuyển  
Bạn không thể tạo chủ đề mới trong diễn đàn này.
Bạn không thể trả lời chủ đề trong diễn đàn này.
Bạn không thể xóa bài của bạn trong diễn đàn này.
Bạn không thể sửa bài của bạn trong diễn đàn này.
Bạn không thể tạo bình chọn trong diễn đàn này.
Bạn không thể bỏ phiếu bình chọn trong diễn đàn này.

| Cung cấp bởi YAF.NET 2.2.4.14 | YAF.NET © 2003-2020, Yet Another Forum.NET
Thời gian xử lý trang này hết 0.522 giây.