Ở phần này chúng ta sẽ tìm hiểu về IoC và cách để implement nó. Đây là bước đầu tiên để đạt được mục tiêu làm giảm sự phụ thuộc giữa các lớp ứng dụng. Các bước được minh họa theo follow sau:

 

Inversion of Control (IoC) là một nguyên lý thiết kế (mặc dù, một vài người vẫn đề cập tới nó như là một design pattern). Giống như tên của nó đã gợi ý, nó được sử dụng để đảo ngược các loại điều khiển khác nhau trong thiết kế hướng đối tượng nhằm mục tiêu làm giảm sự phụ thuộc giữa các đối tượng. Ở đây điều-khiển đề cập tới bất kì tính năng (phạm vi) bổ sung mà một lớp có, ngoài tính năng chính của lớp. Ví dụ lớp thao tác với Database chỉ nên làm việc với database, logger là một tính-năng-bổ-sung. Điều này bao gồm việc kiểm soát luồng của ứng dụng, và kiểm soát luồng của việc tạo đối tượng hoặc tạo các đối tượng phụ thuộc và binding chúng.

Để tiếp cận vấn đề theo cách dân-dã, giả sử bạn lái xe đến nơi làm việc. Điều này có nghĩa là bạn điều khiển (control) chiếc xe. Nguyên lý IoC gợi ý việc đảo ngược điều khiển, có nghĩa rằng thay vì việc bạn tự lái xe, bạn gọi Grab và có tài xế lái xe chở bạn tới cơ quan. Vì vậy việc này gọi là đảo-ngược sự phụ thuộc, từ bạn tới tài xế taxi. Bạn không cần tự lái xe và có tài xế để làm việc này, vì thế bạn có thể dành thời gian để tập trung vào công việc của mình.

Nguyên lý IoC giúp thiết kế các lớp ít sự phụ thuộc, chúng dễ dàng hơn trong việc test, maintainable và extensible.

Tiếp theo chúng ta sẽ cùng tìm hiểu, IoC đảo ngược các loại điều khiển khác nhau như thế nào.

Điều khiển luồng của ứng dụng.
Cùng xem xét ứng dụng đơn giản sau:

namespace FlowControlDemo
{
    class Program
    {
        static void Main(string[] args)
        {
           bool continueExecution = true;
            do
            {
                Console.Write("Enter First Name:");
                var firstName = Console.ReadLine();

                Console.Write("Enter Last Name:");
                var lastName = Console.ReadLine();

                Console.Write("Do you want to save it? Y/N: ");

                var wantToSave = Console.ReadLine();

                if (wantToSave.ToUpper() == "Y")
                    SaveToDB(firstName, lastName);

                Console.Write("Do you want to exit? Y/N: ");

                var wantToExit = Console.ReadLine();

                if (wantToExit.ToUpper() == "Y")
                    continueExecution = false;

            }while (continueExecution);
         
        }

        private static void SaveToDB(string firstName, string lastName)
        {
            //save firstName and lastName to the database here..
        }
    }
}

Ở ví dụ đơn giản trên, hàm Main() của chương trình cho phép bạn nhập vào firstname và lastname. Lưu dữ liệu và tiếp tục các bước hoặc thoát chương trình, sự phụ thuộc thể hiện ở việc input của user. Ở đây, luồng của chương trình phụ thuộc vào hàm Main().

IoC có thể được áp dụng để giải quyết bài toán này, bằng cách tạo một ứng dụng có giao diện đồ họa, ví dụ như 1 webpage, hoặc một ứng dụng windows form. Nơi mà framework sẽ điều khiển luồng của ứng dụng thông qua các event.

Đây là các đơn giản để implement IoC vào việc điều khiển luồng của ứng dụng.

Điều khiển việc tạo đối tượng phụ thuộc
IoC có thể được sử dụng khi tạo các đối tượng của một lớp phụ thuộc.
Chúng ta cùng xem ví dụ sau:

public class A
{
    B b;

    public A()
    {
        b = new B();
    }

    public void Task1() {
        // do something here..
        b.SomeMethod();
        // do something here..
    }

}

public class B {

    public void SomeMethod() { 
        //doing something..
    }
}

Trong ví dụ trên lớp A gọi b.SomeMethod() để thự thi task1. Lớp A không thể thực thi thành công nếu thiếu lớp B và vì thế bạn có thể nói rằng: “Lớp A bị phụ thuộc vào lớp B” hoặc “lớp B là một phụ thuộc của lớp A”.

Trong lập trình hướng đối tượng, các lớp tương tác với nhau để thực hiện một hoặc nhiều chức năng của ứng dụng, như là ví dụ trên, lớp A và B. Lớp A tạo và quản lý vòng đời của đối tượng thuộc lớp B. Về cơ bản, nó kiểm soát việc tạo và vòng đời của các đối tượng thuộc lớp phụ thuộc.

Nguyên lý IoC gợi ý việc đảo ngược sự phụ thuộc. Điều này có nghĩa là chúng ta cần chuyển việc điều khiển sang một lớp khác như sau:

public class A
{
    B b;

    public A()
    {
        b = Factory.GetObjectOfB ();
    }

    public void Task1() {
        // do something here..
        b.SomeMethod();
        // do something here..
    }
}

public class Factory
{
    public static B GetObjectOfB() 
    {
        return new B();
    }
}

Như bạn thấy, lớp A sử dụng lớp Factory để lấy đối tượng của lớp B. Vì vậy chúng ta đã đảo ngược việc tạo đối tượng phụ thuộc từ lớp A tới lớp Factory. Lớp A sẽ không tạo các đối tượng của lớp B nữa, thay vào đó, sử dụng lớp Factory để lấy đối tượng của lớp B.

Chúng ta sẽ hiểu điều này qua ví dụ thực tế hơn như sau.

Trong lập trình hướng đối tượng, các lớp nên được thiết kế để có sự liên kết lỏng lẻo (ít sự phụ thuộc). Điều này có nghĩa là khi ta thay đổi một class, không bắt buộc phải thay đổi các lớp khác, vì thế ứng dụng có thể dễ maintainable và extensible hơn. Cùng xem thiết kế theo kiến trúc n-tier:

Trong thiết kế n-tier, User Interface (UI) sử dụng tầng Service để lấy hoặc lưu dữ liệu. Tầng Service ử dụng lớp BusinessLogic để thực thi các nghiệp vụ với dữ liệu. Lớp BusinessLogic này phục thuộc vào lớp DataAccess trong việc lấy hoặc lưu dữ liệu vào database. Đây là kiến trúc n-tier đơn giản. Giờ, chúng ta hãy tập trung vào 2 lớp BusinessLogic và DataAccess để tìm hiểu về IoC.

Xem ví dụ code của lớp BusinessLogic và DataAccess:

public class CustomerBusinessLogic
{
    DataAccess _dataAccess;

    public CustomerBusinessLogic()
    {
        _dataAccess = new DataAccess();
    }

    public string GetCustomerName(int id)
    {
        return _dataAccess.GetCustomerName(id);
    }
}

public class DataAccess
{
    public DataAccess()
    {
    }

    public string GetCustomerName(int id) {
        return "Dummy Customer Name"; // get it from DB in real app
    }
}

Giống ví dụ trước về lớp A, B. Lớp CustomerBusinessLogic phụ thuộc vào lớp DataAccess. Nó tạo đối tượng của lớp DataAccess và lấy dữ liệu của customer.

Vậy điều gì là không đúng ở đây?

Trong ví dụ trên, BusinessLogic và DataAccess là 2 lớp có kết nối chặt chẽ với nhau bởi vì CustomerBusinessLogic bao gồm liên kết cụ thể của lớp DataAccess. Nó cũng tạo một đối tượng của lớp DataAccess và quản lý vòng đời của đối tượng.

    Các vấn đề của ví dụ trên như sau:

  • 1. CustomerBusinessLogic và DataAccess là 2 lớp có thiết kế chặt chẽ với nhau. Vì vậy, khi thay đổi lớp DataAccess sẽ dẫn tới việc thay đổi trong lớp CustomerBusinessClass. Ví dụ, nếu chúng ta thêm, xóa, hoặc đổi tên bất kì phương thức nào trong lớp DataAccess, thì sau đó chúng ta cũng cần thay đổi trong lớp CustomerBusinessLogic.
  • 2. Trong trường hợp dữ liệu khách hàng được lấy từ 1 database khác, hoặc được lấy từ api (website khác) điều này dẫn tới việc thay đổi trong lớp CustomerBusinessLogic.
  • 3. Lớp CustomerBusinessLogic tạo một đối tượng của lớp DataAccess bằng
  • cách sử dụng từ khóa new. Có rất nhiều lớp khác cũng sử dụng lớp DataAccess và tạo các đối tượng. Vì vậy, nếu bạn đổi tên của lớp, thì sau đó bạn sẽ cần tìm và sửa code ở rất nhiều nơi trong ứng dụng.
  • 4. Vì lớp CustomerBusinessLogic tạo một đối tượng của lớp DataAccess cụ thể, nên dẫn tới khó cho việc testing.

Để giải quyết các vấn để trên và tạo ra một thiết kế lỏng lẻo, ít sự phụ thuộc, chúng ta có thể sử dụng nguyên lý IoC và DIP cùng với nhau. Luôn nhớ rằng IoC là một nguyên lý, không phải là một pattern. Nó chỉ đưa ra hướng dẫn thiết kế ở tầng high-level chứ không đưa ra cách thực thi cụ thể. Bạn có thể tự do áp dụng nguyên lý IoC theo cách mà bạn muốn.

Một số cách implements nguyên lý IoC như sau:

Ở ví dụ này, chúng ta sẽ sử dụng Factory pattern để áp dụng IoC.

Đầu tiên tạo class Factory trả về một đối tượng của lớp DataAccess

public class DataAccessFactory
{
    public static DataAccess GetDataAccessObj() 
    {
        return new DataAccess();
    }
}

Lớp CustomerBusinessLogic sẽ sử dụng lớp DataAccessFactory để lấy đối tượng của lớp DataAccess

public class CustomerBusinessLogic
{

    public CustomerBusinessLogic()
    {
    }

    public string GetCustomerName(int id)
    {
        DataAccess _dataAccess =  DataAccessFactory.GetDataAccessObj();

        return _dataAccess.GetCustomerName(id);
    }
}

Ở đây chúng ta đã đảo ngược điều khiển của việc tạo đối tượng của lớp phụ thuộc từ CustomerBusinessLogic sang lớp DataAccessFactory.

Đây là cách đơn giản để implementation IoC và là bước đầu tiên để tạo ra thiết kế lỏng lẻo, ít sự phụ thuộc. Và giống như đề cập ở phần 1, chúng ta vẫn chưa hoàn thành việc thiết kế lỏng lẻo chỉ bằng cách sử dụng IoC. Cùng với IoC chúng ta cần sử dụng thêm DIP, Strategy và DI (Dependency Injection) pattern. Các vấn đề này sẽ tiếp tục được trình bày trong phần tiếp theo.

Chắc hẳn các thuật ngữ Inversion of Control (IoC), Dependency Inversion Principle (DIP), Dependency Injection (DI), và IoC containers không còn xa lạ với nhiều người. Tuy nhiên để làm rõ hơn sự khác nhau giữa các khái niệm này, là không dễ?

Để hiểu rõ các khái niệm trên, chúng ta xem xét biểu đồ sau:

Theo hình trên chúng ta thấy IoC và DIP là các nguyên lý thiết kế ở tầng high level, chúng nên được dùng khi thiết kế các lớp ứng dụng. Vì là các nguyên lý, chúng là các best practices, nhưng lại không cung cấp một cách implement cụ thể.
Dependency Injection (DI) là một partern, và IoC container là một framework.

Chúng ta sẽ bắt đầu tìm hiểu tổng quan về các khái niệm này trước khi đi vào chi tiết.

Inversion of Control
IoC là một nguyên lý thiết kế được recommend để đảo-ngược sự phụ thuộc các loại điều khiển trong thiết kế hướng đối tượng nhằm mục đích làm giảm sự phụ thuộc giữa các lớp ứng dụng. Trong trường hợp này, điều khiển có thể được hiểu là có thêm 1 trách nhiệm, chức năng cho một lớp mà ngoài phạm vi mà nó đảm nhận, chẳng hạn như việc kiểm soát luồng ứng dụng hoặc kiểm soát việc tạo và ràng buộc đối tượng phụ thuộc (Xem lại về S trong SOLID).

Dependency Inversion Principle
Nguyên tắc DIP cũng giúp giảm sự phụ thuộc giữa các lớp. Rất khuyến khích sử dụng DIP và IoC cùng nhau để đạt được mục đích cuối cùng là giảm sự phụ thuộc từ các lớp high level với low level.
DIP suggests that high-level modules should not depend on low level modules. Both should depend on abstraction.
Nguyên lý DIP được phát minh bởi Robert Martin (a.k.a. Uncle Bob). Người được biết đến là một founder của nguyên lý SOLID.

Dependency Injection
Dependency Injection (DI) là một design pattern, implement nguyên lý IoC để đảo ngược trong việc tạo các đối tượng phụ thuộc.

IoC Container
IoC container là một framework sử dụng để quản lý việc tự động tiêm sự phụ thuộc trong ứng dụng, vì thế chúng ta không cần đặt quá nhiều thời gian và effort vào nó. Một vài IoC Containers hay được sử dụng trong .NET, là Unity, Ninject, StructureMap, Autofac…

Chúng ta không thể làm giảm sự phụ thuộc giữa các lớp bằng việc sử dụng một mình IoC. Cùng với IoC chúng ta cũng cần sử dụng DIP, DI, và IoC container.

Cùng xem mô hình sau để xem các bước tiến hành thiết kế để làm giảm sự phụ thuộc trong ứng dụng.

Trong dự án asp net core, khi các bạn sử dụng Generic Repository Pattern, làm cách nào để register generic interface và implementation generic interface bằng cách sử dụng ASP.NET Core DI container?

Ok, với một interface đơn giản và implementation của nó, ta sử dụng như sau

serviceCollection.AddSingleton<IService, MyService>();

Vậy còn kiểu Generic thì sao?

Chúng ta cùng xem lại phát biểu về phương thức AddSingleton

public static IServiceCollection AddSingleton<TService, TImplementation>(this IServiceCollection serviceswhere TService : class where TImplementation : class, Tservice

Phương pháp này sử dụng được với hầu hết các trường hợp, ngoại trừ các service generic..

Xét một ví dụ đơn giản, chúng ta có interface

public interface IThing<T>
{
    string GetName { get; }
}

và một implementation của nó như sau

public class GenericThing<T> : IThing<T>
{
    public GenericThing()
    {
        GetName = typeof(T).Name;
    }

    public string GetName { get; }
}

Câu hỏi đặt ra là khi sử dụng IThing<SomeType> làm sao chúng ta lấy được chính xác GenericThing<SomeType> đã được injected vào?

Trong trường hợp này chúng ta sử dụng phương thức mở rộng khác trong ServiceCollection, bằng cách như sau

serviceCollection.AddSingleton(typeof(IThing<>), typeof(GenericThing<>));

Ok, giờ chúng ta có thể sử dụng DI bất cứ đâu cần Injected

public class ValuesController : Controller
{
    private readonly IThing<ValuesController> _thing;

    public ValuesController(IThing<ValuesController> thing)
    {
        _thing = thing;
    }
}

Trông cũng dễ hiểu phải không 🙂

Hãy comment đặt câu hỏi nếu các bạn cần trao đổi thêm về asp net core hoặc DI nhé!