Ở phần trước, chúng ta đã tìm hiểu về cách implement nguyên lý IoC bằng việc sử dụng Factory pattern và đạt được mức độ đầu tiên của việc thiết kế các class có ít sự phụ thuộc hơn.
Phần này chúng ta sẽ tìm hiểu việc implement DIP bằng cách tạo các abstract class.

Đầu tiên, chúng ta cần biết nguyên lý DIP nghĩa là gì?
DIP là một trong 5 nguyên lý thiết kế hướng đối tượng được giới thiệu bởi Robert Martin (Uncle Bob)

Phát biểu của nguyên lý như sau:

  1. Các module high-level không nên phụ thuộc vào các module low-level. Cả 2 nên phụ thuộc vào abstract class.
  2. Abstract class không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào abstract class.

Để hiểu hơn về DIP chúng ta cùng xem lại ví dụ ở chapter trước

public class CustomerBusinessLogic
{
    public CustomerBusinessLogic()
    {
    }

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

        return _dataAccess.GetCustomerName(id);
    }
}

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

public class DataAccess
{
    public DataAccess()
    {
    }

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

Trong ví dụ trên, chúng ta đã implemented factory pattern để thực hiện IoC. Nhưng lớp CustomerBusinessLogic vẫn đang sử dụng lớp DataAccess cụ thể. Vì vậy nó vẫn mang tính phụ thuộc, mặc dù chúng ta đã đảo ngược việc tạo đối tượng phụ thuộc cho lớp factory.

Bằng việc sử dụng DIP, chúng ta sẽ khiến lớp CustomerBusinessLogic và lớp DataAccess có ít sự phụ thuộc hơn như sau.

Đầu tiên chúng ta sẽ xác định lớp high-level và lớp low-level. Lớp high-level là lớp phụ thuộc vào lớp khác. Trong ví dụ này, CustomerBusinessLogic phụ thuộc vào lớp DataAccess, vì vậy CustomerBusinessLogic là lớp high-level và DataAccess là lớp low-level. Lúc này, theo nguyên tắc đầu tiên của DIP, CustomerBusinessLogic không nên phụ thuộc vào một đối tượng cụ thể của lớp DataAccess, cả 2 nên phụ thuộc vào abstract class.

Nguyên tắc thứ 2 của DIP là: “Abstract class không nên phụ thuộc vào chi tiết. Chi tiết nên phụ thuộc vào abstract class”.

Abstract class là gì?
Trừu tượng hóa và đóng gói là những tính chất quan trọng của lập trình hướng đối tượng. Có rất nhiều định nghĩa và các cách diễn giải khác nhau, từ những người khác nhau, nhưng chúng ta có thể hiểu abstract class bằng việc sử dụng ví dụ trên;

Trong tiếng anh, abstraction nghĩa là đang nói tới một thứ gì đó không-cụ-thể. Trong các quy ước lập trình, cả 2 lớp CustomerBusinessLogic và DataAccess là các lớp cụ thể, nghĩa là chúng ta có thể tạo các đối tượng của chúng. Vì vậy, abstraction trong lập trình nghĩa là tạo một interface hoặc một abstract class không-cụ-thể. Điều này có nghĩa rằng chúng ta không thể tạo một đối tượng của interface hoặc một abstract class. Theo DIP, CustomerBusinessLogic (high-level module) không nên phụ thuộc vào một lớp DataAccess cụ thể (low-level module). Cả 2 lớp này nên phụ thuộc vào abstraction, nghĩa là cả 2 lớp nên phụ thuộc vào một interface hoặc một abstract class.

Bây giờ chúng ta sẽ xem nên đặt gì trong interface (hoặc abstract class)? Như các bạn thấy, lớp CustomerBusinessLogic sử dụng phương thức GetCustomerName() của lớp DataAccess (trong thực tế, sẽ có rất nhiều phương thức của customer được đặt trong DataAccess). Vì vậy chúng ta có thể chuyển phương thức GetCustomerName(int id) vào interface như sau:

public interface ICustomerDataAccess
{
    string GetCustomerName(int id);
}

Bây giờ chúng ta sẽ implement lớp ICustomerDataAccess

public class CustomerDataAccess: ICustomerDataAccess
{
    public CustomerDataAccess()
    {
    }

    public string GetCustomerName(int id) {
        return "Dummy Customer Name";        
    }
}

Tiếp theo chúng ta cần thay đổi lớp factory bằng việc trả về ICustomerDataAccess, thay vì trả về lớp DataAccess:

public class DataAccessFactory
{
    public static ICustomerDataAccess GetCustomerDataAccessObj() 
    {
        return new CustomerDataAccess();
    }
}

Tiếp tục thay đổi lớp  CustomerBusinessLogic, bằng việc sử dụng ICustomerDataAccess, thay vì đối tượng DataAccess:

public class CustomerBusinessLogic
{
    ICustomerDataAccess _custDataAccess;

    public CustomerBusinessLogic()
    {
        _custDataAccess = DataAccessFactory.GetCustomerDataAccessObj();
    }

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

Cuối cùng, chúng ta đã implement DIP trong ví dụ trên, nơi module high-level (CustomerBusinessLogic) và module low-level (CustomerDataAccess) phụ thuộc vào abstraction (ICustomerDataAccess). Ngoài ra, abstraction (ICustomerDataAccess) không phục thuộc vào chi tiết (CustomerDataAccess), nhưng chi tiết phụ thuộc vào abstraction.

Toàn bộ code cuối cùng như sau:

public interface ICustomerDataAccess
{
    string GetCustomerName(int id);
}

public class CustomerDataAccess: ICustomerDataAccess
{
    public CustomerDataAccess() {
    }

    public string GetCustomerName(int id) {
        return "Dummy Customer Name";        
    }
}

public class DataAccessFactory
{
    public static ICustomerDataAccess GetCustomerDataAccessObj() 
    {
        return new CustomerDataAccess();
    }
}

public class CustomerBusinessLogic
{
    ICustomerDataAccess _custDataAccess;

    public CustomerBusinessLogic()
    {
        _custDataAccess = DataAccessFactory.GetCustomerDataAccessObj();
    }

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

 

Tóm tắt

Lợi ích của việc implement DIP trong ví dụ trên là lớp CustomerBusinessLogic và CustomerDataAccess có ít sự phụ thuộc, vì CustomerBusinessLogic không phụ thuộc vào lớp DataAccess cụ thể, thay vào đó, nó liên kết với interface ICustomerDataAccess. Vì vậy bây giờ chúng ta có thể dễ dàng sử dụng một lớp khác implement ICustomerDataAccess với một cách implement hoàn toàn khác lớp chúng ta vừa viết ở trên.

Chúng ta vẫn chưa hoàn thiện việc thiết kế các lớp ít sự phụ thuộc, bời vì lớp CustomerBusinessLogic vẫn bao gồm một lớp factory để lấy quan hệ với ICustomerDataAccess.Vấn đề này sẽ được Dependency Injection pattern xử lý. Trong bài tiếp theo, chúng ta sẽ học cách sử dụng DI và Strategy pattern để phát triển tiếp ví dụ này.

 

Series

Phần 1

Phần 2

Ở 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 không dễ để làm rõ sự khác nhau giữa các khái niệm này.
Loạt bài viết này cố gắng mang đến cho các bạn cái nhìn tổng quan nhất, với mong muốn cùng các bạn khám phá kiến thức về IoC, DIP, DI..

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

Phần 2: http://thaotrinh.info/ioc-introduction-chapter-2/

Một trong các đòi hỏi lớn của việc quản lý danh mục sản phẩm là phải thiết kế để đáp ứng các nhu cầu về hiệu năng và tích hợp với các hệ thống khác. Để đáp ứng các yêu cầu tốt, thì phải được thiết kế từ tổng thể ngay từ đầu. Có vậy mới tạo ra sự phát triển liền mạch, nhất quán giúp đảm bảo tính ổn định, cũng như tiến độ làm việc.

Continue reading

Đầu tiên chúng ta bắt đầu với bài toán quản lý sản phẩm với các nghiệp vụ phức tạp. Ví dụ hệ thống quản lý sản phẩm của các trang thương mại điện tử (Amazon, shoppe, tiki..).

Với bất kì sản phẩm phần mềm nào, chúng ta đều có chung mục đích thiết kế:

– DRY (Don’t Repeat Yourself), các nghiệp vụ được phân tách rõ ràng, cô đọng và tập trung cao.
– Có thể mở rộng, sửa đổi các thành phần độc lập.
– Linh hoạt trong việc phát triển.
– Dễ dàng test và bảo trì.

Các mục tiêu này sẽ ảnh hưởng tới cách thiết kế mô hình ứng dụng khác nhau. Chúng ta sẽ phân tích 3 mô hình thiết kế hay được sử dụng để tìm hiểu ưu, nhược điểm cũng như chọn cách thiết kế hệ thống phù hợp nhất.

Mô hình MVC

Mô hình MVC là mô hình phổ biến trong việc lựa chọn và xây dựng ứng dụng.
Đó là mô hình đơn giản, dễ tiếp cận và nhanh chóng đưa ra các tính năng.
Theo mô hình MVC thì phần quản lý sản phẩm sẽ theo mô hình như sau:

Với mô hình MVC như trên việc xử lý ở product model sẽ trở nên phức tạp. Lúc này product model sẽ là nơi:

– Định nghĩa các thuộc tính của product.
– Truy xuất database.
– Và thực thi các nghiệp vụ.

Thông thường mô hình MVC sẽ sử dụng pattern Active Record để mapping với database. Đây là cách làm đơn giản, nhưng có các hạn chế sau:

– Model product trở nên quá nặng khi chứa quá nhiều các logic từ định nghĩa thuộc tính, truy xuất dữ liệu, thực thi nghiệp vụ…
– Do không phân tách được các tầng nghiệp vụ và dữ liệu, nên khó thực hiện unit test các thành phần riêng biệt.
– Việc thay đổi logic truy xuất dữ liệu và logic nghiệp vụ khó khăn. Do các thành phần bị phụ thuộc vào nhau.
– Tính đóng gói của nghiệp vụ không đảm bảo. Các nghiệp vụ sẽ chỉ được cài đặt theo mô hình CRUD. Vì vậy càng về sau sự trùng lặp các logic càng cao.

Do các hạn chế đó nên đây không phải mô hình phù hợp với các ứng dụng logic phức tạp như ecommerce. Nó sẽ dẫn tới chi phí maintain về sau cao.

Mô hình ba lớp.

Hướng tiếp cận tiếp theo là phân tích và thiết kế theo phương pháp n-tier hay Domain Driven Design. Đặc trưng của mô hình này là:

– Phân tách tầng nghiệp vụ khỏi tầng ứng dụng và tầng truy xuất cơ sở dữ liệu. Tầng này gọi là Domain Layer. Tầng này là nơi chứa tất cả các logic nghiệp vụ.
– Trong tầng nghiệp vụ, tập trung vào design model sao cho model phản ánh đầy đủ nhất tính chất nhất quán của nghiệp vụ. Tầng này có thể chia thành 2 phần riêng biệt là Domain ModelDomain Service. Domain Service đóng vai trò cung cấp các nghiệp vụ ra bên ngoài (tới tầng Aplication) xoay quanh các domain model của hệ thống.
– Thiết kế tầng infrastructure chứa các logic về truy xuất dữ liệu, thao tác với database, messeage queue.

DDD Partern

Lợi ích:
Với việc tổ chức hệ thống này, các tầng ứng dụng, nghiệp vụ và data access sẽ chia tách riêng biệt, và chỉ tương tác thông qua interface. Các nghiệp vụ sẽ xoay quanh model product. Tầng data access sẽ truy xuất hoặc lưu trữ các object products.
Hạn chế:
Việc áp dụng mô hình 3 lớp xoay quanh model product cũng dẫn tới khó khăn là việc thiết kế model cho các nghiệp vụ đọc-dữ-liệu.
Đồng thời nó cũng ảnh hưởng tới tốc độ phát triển của hệ thống khi cài đặt các nghiệp vụ query đa dạng và phức tạp trong khi phuc vụ client. Việc này bị ảnh hưởng bởi cách thiết kế model và tầng Repository.

Mô hình CQRS

Mô hình CQRS (Command Query Responsibility Segregation) là sử mở rộng của mô hình trong ba lớp trong DDD. Đặc trưng quan trọng của CQRS là việc tách hai phần logic đọc và ghi dữ liệu ra hai phần riêng biệt:

– Phần ghi dữ liệu: được thực hiện qua việc send các command tới các handler thông qua command bus. Comand hanlder đóng vai trò tương tự domain service sẽ tương tác với các model để thực hiện các nghiệp vụ thay đổi dữ liệu.
– Phần đọc dữ liệu: được thiết kế riêng không lệ thuộc vào các model của phần ghi dữ liệu. Do đó có thể linh hoạt trong việc truy xuất database, cũng như sử dụng các data source khác nhau để tối ưu về tốc độ truy xuất.


Mô hình CQRS đã khắc phục các hạn chế đã nêu ở mục 2 bên trên. CQRS mang lại các lợi thế lớn:

– Cho phép phát triển và tối ưu phần đọc dữ liệu riêng biệt với phần ghi dữ liệu.
– Việc mô hình hoá các nghiệp vụ ghi dữ liệu dưới các command cho phép che đậy tốt các logic nghiệp vụ, giúp việc mở rộng dễ dàng hơn. Đồng thời các command đó có thể dễ dàng chuyển đổi giữa xử lý đồng bộ và bất đồng bộ thông qua lớp abstractcommand bus mà không thay đổi mô hình. Giúp cung cấp một mô hình nhất quán, xuyên suốt trong bộ kiến trúc.

Tóm tắt về 3 mô hình (cả 3 mô hình trên đều có ưu/nhược điểm)

– Mô hình MVC đơn giản, nhưng hạn chế khi giải quyết nghiệp vụ phức tạp và testing, mở rộng
– Mô hình ba lớp phù hợp cho việc xử lý nghiệp vụ phức tạp, nhưng có nhiều hạn chế khi tối ưu phần đọc ra.
– Mô hình CQRS mở rộng từ mô hình ba lớp, giải quyết tốt việc chia tách đọc ghi, nhưng cũng đòi hỏi phải thiết kế phức tạp hơn.

Trên đây, chúng ta đã tìm hiểu qua về 3 mô hình khi thiết kế hệ thống. Và với các hệ thống lớn, cần đòi hỏi xử lý nghiệp vụ phức tạp, đồng thời đáp ứng hiệu năng, mô hình chúng ta nên chọn là CQRS. Mặc dù còn hạn chế về độ phức tạp khi triển khai, đòi hỏi team work cùng hiểu về hệ thống, tuy nhiên những giá trị mà nó mang lại nên được đánh đổi.

Phần tiếp theo chúng ta sẽ tìm hiểu tiếp về mô hình CQRS-ES.

Bài viết được tham khảo từ
tiki-engineering.
future-processing.

bài trước mình đã giới thiệu với các bạn về UnitOfWork Design Pattern.

Nhắc lại vể lợi ích của UOW

  • Quản lý danh sách các đối tượng logic nghiệp vụ được thay đổi trong một transaction.
  • Khi một transaction được complete, tất cả thay đổi sẽ được gửi tới database như một big unit of work

Bài toán:

Để hình dung rõ hơn, lần này chúng ta sẽ xem xét bài toán thực tế khi quản trị hệ thống muốn xóa một đơn hàng. Chúng ta cần xóa dữ liệu ở 2 bảng OrdersOrderDetails.

Theo cách làm thông thường, chúng ta sẽ xóa dữ liệu bảng OrderDetails trước, sau đó xóa dữ liệu bảng Orders. Với cách làm này rủi ro xảy ra nếu sau khi xóa dữ liệu bảng OrderDetails xong, một exception xảy ra (ví dụ kinh điển là mất mạng) lúc này dữ liệu bảng Orders chưa được xóa, dẫn tới việc không đảm bảo tính toàn vẹn dữ liệu và có thể gây lỗi hệ thống khi truy xuất đơn hàng.

Để giải quyết bài toán này. Chúng ta sử dụng pattern UnitOfWork, thực hiện việc xóa dữ liệu ở 2 bảng trong cùng 1 transaction. Bất kì ngoại lệ nào xảy ra, dữ liệu sẽ được rollback. Nếu thao tác xóa dữ liệu thành công, transaction sẽ được commit.

Đồ nghề:

Xây dựng hệ thống

Cùng xem xét lại quan điểm:
Simplicity is the ultimate sophistication
Áp dụng thực tế vào bài toán khi làm việc với Database, để tránh việc code thừa ở khắp nơi khi gọi query dữ liệu. Trong ví dụ này chúng ta dùng thêm repository pattern để xử lý tầng kiến trúc làm việc với database.

Với tư tưởng như trên, chúng ta xây dựng lớp Repository chuyên xử lý các thao tác thông thường với database. Lớp OrderRepository xử lý các vấn đề đặc thù của Domain Order

Cấu trúc dự án tổng thể như sau:

Đầu tiên chúng ta xây dựng interface IRepository

public interface IRepository<TEntity> where TEntity : class
    {
        TEntity Get(int id);
        IEnumerable<TEntity> GetAll();
        IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate);

        TEntity SingleOrDefault(Expression<Func<TEntity, bool>> predicate);

        void Add(TEntity entity);
        void AddRange(IEnumerable<TEntity> entities);

        void Remove(TEntity entity);
        void RemoveRange(IEnumerable<TEntity> entities);
    }

Và lớp Repository implement interface IRepository

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
    {
        protected readonly DbContext Context;
        protected readonly DbSet<TEntity> DbSetEntity;

        public Repository(DbContext context)
        {
            Context = context;
            DbSetEntity = Context.Set<TEntity>();
        }

        public void Add(TEntity entity)
        {
            DbSetEntity.Add(entity);
        }

        public void AddRange(IEnumerable<TEntity> entities)
        {
            DbSetEntity.AddRange(entities);
        }

        public IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate)
        {
            return DbSetEntity.Where(predicate);
        }

        public TEntity Get(int id)
        {
            return DbSetEntity.Find(id);
        }

        public IEnumerable<TEntity> GetAll()
        {
            return DbSetEntity.ToList();
        }

        public void Remove(TEntity entity)
        {
            DbSetEntity.Remove(entity);
        }

        public void RemoveRange(IEnumerable<TEntity> entities)
        {
            DbSetEntity.RemoveRange(entities);
        }

        public TEntity SingleOrDefault(Expression<Func<TEntity, bool>> predicate)
        {
            return DbSetEntity.SingleOrDefault(predicate);
        }
    }

Interface IOrderRepository kế thừa từ IRepository

public interface IOrderRepository : IRepository<Orders>
{
    IEnumerable<Orders> GetTopOrders(int count);
    IEnumerable<Orders> GetOrdersWithCustomers(int pageIndex, int pageSize, string customerId);
}

Lớp OrderRepository kế thừa từ IOrderRepositoryRepository

public class OrderRepository : Repository<Orders>, IOrderRepository
{

    public OrderRepository(NorthwindContext context) : base(context)
    {
    }

    public IEnumerable<Orders> GetOrdersWithCustomers(int pageIndex, int pageSize, string customerId)
    {
        return NorthwindContext.Orders
            .Where(o => o.CustomerId == customerId)
            .OrderBy(c => c.OrderId)
            .Skip((pageIndex - 1) * pageSize)
            .Take(pageSize)
            .ToList();
    }

    public IEnumerable<Orders> GetTopOrders(int count)
    {
        return NorthwindContext.Orders.OrderByDescending(c => c.OrderId).Take(count).ToList();
    }

    public NorthwindContext NorthwindContext
    {
        get { return Context as NorthwindContext; }
    }
}

Tiếp theo chúng ta tạo interface IUnitOfWork

public interface IUnitOfWork : IDisposable
{
    IOrderRepository Orders { get; }
    ICustomerRepository Customers { get; }
    IOrderDetailRepository OrderDetails { get; }
    int Complete();
}

Và implement của nó là class UnitOfWork

public class UnitOfWork : IUnitOfWork
{
    private readonly NorthwindContext _context;

    public UnitOfWork(NorthwindContext context)
    {
        _context = context;
        Orders = new OrderRepository(_context);
        Customers = new CustomerRepository(_context);
        OrderDetails = new OrderDetailRepository(_context);
    }

    public IOrderRepository Orders { get; set; }

    public ICustomerRepository Customers { get; set; }

    public IOrderDetailRepository OrderDetails { get; set; }

    public int Complete()
    {
        return _context.SaveChanges();
    }

    public void Dispose()
    {
        _context.Dispose();
    }
}

Vậy là chúng ta đã implement xong mô hình của hệ thống, áp dụng 2 partern là RepositoryUnitOfWork.

Tiếp theo chúng ta sẽ tới lớp Program và viết mã test hoạt động của hệ thống trong hàm Main

Class program sẽ trông như sau:

class Program
{
    static void Main(string[] args)
    {
        using (var unitOfWork = new UnitOfWork(new NorthwindContext()))
        {

            // Example1
            var customersByCity = unitOfWork.Customers.GetCustomerByCity("London");

            // Example2
            var customers = unitOfWork.Customers.GetAll();

            // Example3
            var order = unitOfWork.Orders.Get(10250);
            var orderDetails = unitOfWork.OrderDetails.GetByOrderId(order.OrderId);
            unitOfWork.OrderDetails.RemoveRange(orderDetails);
            unitOfWork.Orders.Remove(order);
            unitOfWork.Complete();

            Console.WriteLine("Done!");
        }
    }
}

Source code 

Trong bài viết này, chúng ta sẽ cùng thảo luận về Unit of Work Design Pattern.

Nội dung chính:

  • Lợi ích khi sử dụng Unit of Work design pattern?
  • Work và Unit trong ứng dụng phần mềm.
  • Logical transaction! = Physical CRUD.
  • So sánh với một transaction.
  • Ý tưởng việc implement UOW.
  • Code demo C#

 

Lợi ích khi sử dụng Unit of Work design pattern?

Khái niệm UOW

  • First it maintains in-memory updates.
  • Second it sends these in-memory updates as one transaction to the database.

Giải thích

– Thứ 1: uow quản lý các trạng thái cập nhật dữ liệu trong bộ nhớ.

– Thứ 2: uow thực hiện cập nhật tất cả dữ liệu trong bộ nhớ “như là – một transaction” vào database.

Từ khái niệm trên, chúng ta rút ra được lợi ích khi sử dụng uow

  • Quản lý danh sách các đối tượng logic nghiệp vụ được thay đổi trong một transaction.
  • Khi một transaction được complete, tất cả thay đổi sẽ được gửi tới database như một big unit of work

Work và Unit trong ứng dụng phần mềm.

Một cách đơn giản để định nghĩa Work là: "Thực hiện một vài tác vụ". Các tác vụ ở đây có thể là Thêm, Sửa, Xóa dữ liệu.
Chúng ta có thể cùng xem một ví dụ về việc ứng dụng quản lý khách hàng. Khi chúng ta thêm/sửa/xóa khách hàng trong database, đó là một unit. Kịch bản đơn giản của chúng ta đó là:

1 customer CRUD = 1 unit of work

 

Logical transaction! = Physical CRUD

Trong bài toán thực tế, một khách hàng có thể có nhiều địa chỉ giao hàng khác nhau (địa chỉ nhà, địa chỉ cơ quan…).

Tiếp theo ví dụ trên, chúng ta xem xét ông khách hàng Shiv. Giả sử ông này có 2 địa chỉ, vậy khi thay đổi thông tin của khách hàng Shiv này, chúng ta cần thay đổi 3 bản ghi. Xem xét kịch bản như sau:

3 Customer CRUD = 1 Logical unit of work

Trong các dự án thực tế, chúng ta có thể đưa nhiều tác vụ khác nhau vào một unit of work.

Một ví dụ đơn giản khác, đó chính là bài toán rút tiền tại cây ATM. Khi khách hàng thực hiện rút tiền, ngân hàng cập nhật thông tin bảng giao dịch, cập nhật số dư khách hàng, gửi tin nhắn, in hóa đơn.. Tất cả các tác vụ đó có thể đưa vào một UOW

So sánh với simple transaction

Một UOW sẽ rất khác với một transaciton database. UOW chỉ gửi những bản ghi được thay đổi tới database mà không phải tất cả các bản ghi!

Điều này có nghĩa là gì?

Ví dụ ứng dụng của bạn lấy từ DB ra 3 bản ghi dữ liệu, nhưng chỉ thay đổi 2 trong 3 bản ghi đó. UOW chỉ gửi 2 bản ghi được thay đổi này tới DB để tiến hành cập nhật. Việc này giúp tối ưu performance cho hệ thống của bạn.

Tóm lại

1 Unit of work = Modified records in a transaction

Sample code for unit of work

Bước 1: Tạo Interface

Theo phần trước đã thảo luận, chúng ta biết rằng UOW theo dõi và quản lý những thay đổi trong các đối tượng nghiệp vụ.

Bây giờ hãy xem xét bài toán với rất nhiều các đối tượng nghiệp vụ với nhiều kiểu khác nhau. Ví dụ bạn có thể có customer, suplier, account…Các đối tượng này chính là các entities mà UOW quan tâm và xử lý.

Xem xét kiến trúc sau

Với thiết kế UOW tracking nhiều đối tượng, chúng ta có thể giảm thiểu rất nhiều số mã trùng lặp.

Đầu tiên chúng ta tạo interface IEntity  như sau:

public interface IEntity
{
    int Id { set; get; }
    void Insert();
    void Update();
    List<IEntity> Load();
}

Các đối tượng Customer, Supplier, Order sẽ implement interface này, theo kiến trúc đơn giản như sau:

Bước 2: Implement the IEntity interface

public class Customer : IEntity
{
    private int _CustomerCode = 0;
    public int Id
    {
        get { return _CustomerCode; }
        set { _CustomerCode = value; }
    }
    private string _CustomerName = "";
    public string CustomerName
    {
        get { return _CustomerName; }
        set { _CustomerName = value; }
    }
    public void Insert()
    {
        DataAccess obj = new DataAccess();
        obj.InsertCustomer(_CustomerCode, CustomerName);
    }
    public  List<IEntity> Load()
    {
        DataAccess obj = new DataAccess();
        Customer o = new Customer();
        SqlDataReader ds = obj.GetCustomer(Id);
        while (ds.Read())
        {
            o.CustomerName = ds["CustomerName"].ToString();
        }
        List<IEntity> Li = (new List<Customer>()).ToList<IEntity>();
        Li.Add((IEntity) o);
        return Li;
    }
    public void Update()
    {
        DataAccess obj = new DataAccess();
        obj.UpdateCustomer(_CustomerCode, CustomerName);
    }
}

Bước 3: Create the unit of work collection

Bước tiếp theo chúng ta sẽ tạo một lớp đơn giản SimpleExampleUOW.

Lớp này sẽ định nghĩa 2 collection Changed New, sử dụng để lưu trữ các đối tượng được thay đổi, hoặc tạo mới.

Lớp SimpleExampleUOW tạm thời trông sẽ như sau:

public class SimpleExampleUOW
{
    private List<T> Changed = new List<T>();
    private List<T> New = new List<T>();
    …..
    …..
}

Khi chúng ta thêm mới một đối tượng, đối tượng sẽ được thêm vào Add collection

Khi dữ liệu được lấy từ database, nó sẽ được thêm vào Changed collection

public void Add(T obj)
{
    New.Add(obj);
}
public  void Load(IEntity o)
{
    Changed  = o.Load() as List<T>;
}

Theo định nghĩa về UOW, tất cả các thay đổi cần được gửi tới database như là một transaction (need to be send in one go to database). Vì vậy chúng ta cần tạo một hàm Commit, sẽ lặp qua tất các các đối tượng trong New Changed collections sau đó gọi các phương thức Insert Update dữ liệu.

Ở đây chúng ta sử dụng đối tượng TransactionScope để chắc chắn rằng các thay đổi được commited in an atomic fashion.

Atomic fashion có nghĩa là tất cả các thay đổi đều được cập nhật thành công, hoặc không có thay đổi nào được cập nhật.

public void Committ()
{
    using (TransactionScope scope = new TransactionScope())
    {
        foreach (T o in Changed)
        {
            o.Update();
        }
        foreach (T o in New)
        {
            o.Insert();
        }
        scope.Complete();
    }
}

Lớp SimpleExampleUOW  giờ sẽ đầy đủ hơn như này

public class SimpleExampleUOW
{
    private List<IEntity> Changed = new List<IEntity>();
    private List<IEntity> New = new List<IEntity>();
    public void Add(IEntity obj)
    {
        New.Add(obj);
    }
    public void Committ()
    {
        using (TransactionScope scope = new TransactionScope())
        {
            foreach (IEntity o in Changed)
            {
                o.Update();
            }
            foreach (IEntity o in New)
            {
                o.Insert();
            }
            scope.Complete();
        }    
    }
   public  void Load(IEntity o)
    {
        Changed  = o.Load() as List<IEntity>;
    }
}

Bước 4: See it working

Đầu tiên, chúng ta tạo 2 đối tượng nghiệp vụ là Customer Supplier. Thay đổi giá trị của 2 đối tượng này, sau đó sử dụng UOW để gửi tất cả các thay đổi này tới database, cập nhật database bằng phương thức commit.

Code demo như sau:

Customer Customerobj = new Customer();// record 1 Customer
Customerobj.Id = 1000;
Customerobj.CustomerName = "shiv";

Supplier SupplierObj = new Supplier(); // Record 2 Supplier
Supplierobj.Id = 2000;
Supplierobj.SupplierName = "xxxx";

SimpleExampleUOW UowObj = new SimpleExampleUOW();
UowObj.Add(Customerobj); // record 1 added to inmemory
UowObj.Add(Supplierobj); // record 1 added to inmemory
UowObj.Committ(); // The full inmemory collection is sent for final committ

Source code demo: Github