Ở 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/

Lập trình đôi (tiếng Anh: Pair Programming) là kiểu lập trình đòi hỏi hai kỹ sư phần mềm cùng tham gia một nỗ lực lập trình chung trên một máy trạm, nghĩa là chỉ có một màn hình, một bàn phím. Mỗi người thực hiện việc mà người kia hiện không làm. Ví dụ, người này gõ các bộ test đơn vị (unit test), người kia nghĩ về các lớp đầu vào (input) sẽ thỏa mãn bộ test đó; hoặc người này viết mã còn người kia quan sát để hướng dẫn hoặc kiểm lỗi. Người ta khuyên rằng hai người nên luân phiên đổi vai trò, khoảng nửa giờ một lần.

Ưu điểm:
Lập trình đôi được quảng cáo là đem lại các lợi ích sau (theo thứ tự từ lớn tới nhỏ):

  • Tăng kỷ luật làm việc. Những người làm việc theo cặp thường có xu hướng “làm đúng” hơn và ít nghỉ giải lao dài.
  • Mã chương trình tốt hơn. Những người làm việc theo cặp ít có xu hướng chọn giải pháp ngõ cụt mà thường cho ra các thiết kế với chất lượng cao hơn.
  • Duy trì tốt luồng làm việc. Người này có thể người kia xem cả hai đang làm đến đâu. Nếu bị ngắt quãng, một người giải quyết ngắt quãng trong khi người kia tiếp tục làm việc.
  • Nâng cao tinh thần. Với một số lập trình viên, làm việc theo cặp có thể vui vẻ hơn.
  • Sở hữu tập thể đối với mã chương trình. Khi mọi người trong một dự án đều làm việc theo cặp, và các cặp thường xuyên quay vòng, thi ai cũng có kiến thức về toàn bộ mã nguồn.
  • Cố vấn. Kể cả những lập trình viên mới vào nghề, người nào cũng biết một cái gì đó mà người khác không biết. Lập trình đôi là một cách nhẹ nhàng cho việc lan tỏa kiến thức đó.
  • Gắn kết nhóm. Các thành viên trong nhóm gắn bó với nhau nhanh hơn khi làm việc theo cặp. Lập trình đôi có thể khuyến khích sự gắn bó trong nhóm.
  • Ít ngắt quãng hơn. Người ta thường ngại làm đứt quãng một nhóm người (đôi) hơn là ngắt quãng một người đang làm việc một mình.
  • Đòi hỏi ít máy trạm hơn, do hai người dùng chung một máy.

Khuyết điểm

  • Các lập trình viên nhiều kinh nghiệm có thể cảm thấy nhàm chán khi hướng dẫn một lập trình viên ít kinh nghiệm trong một môi trường lập trình đôi.
  • Nhiều kỹ sư thích làm việc một mình hơn, và có thể thấy môi trường lập trình đôi rất cồng kềnh.
  • Khó so sánh được và mất giữa các môi trường đôi và không đôi, do các phương pháp đo năng suất lập trình viên còn đang gây nhiều tranh cãi.
  • Các khác biệt trong phong cách viết mã có thể gây ra xung đột.
  • Trong trường hợp các thành viên trong lập trình có lịch làm việc lệch nhau (điều thường thấy trong các môi trường coi trọng sự cân bằng giữa công việc và cuộc sống), thời gian làm việc theo cặp ít đi, dẫn tới việc tăng thời gian hoàn tất công việc.
  • Khi một công ty đánh giá cao việc làm việc từ xa (làm việc tại nhà), hay khi một nhân viên phải làm việc tại nhà vì lý do nào đó, việc lập trình đôi có thể khó khăn hoặc không thể thực hiện được.
  • Không hiểu ý nhau giữa 2 lập trình viên

Pair programing phù hợp lựa chọn khi nào?
– Pair programing thực sự phù hợp cho các dự án phức tạp, cần nhiều thời gian để làm quen, hiểu dự án. Khi đó có thể ghép cặp 1 thành viên mới và 1 thành viên có kinh nghiệm để vừa hoàn thành mục tiêu sản phẩm, vừa truyền đạt hiểu biết dự án.
- Trong trường hợp sự chênh lệnh về kĩ năng của các thành viên trong team sản phẩm là lớn, nhưng có nhu cầu transfer nhanh các kĩ thuật để làm việc.
– Trong các dự án refactor code, yêu cầu cao về coding convention, pair programing phát huy vai trò, khi luôn có 1 người giám sát thực hiện kĩ thuật.

Bài viết này chúng ta sẽ làm rõ các vấn đề
1. Technical debt là gì?
2. Khi nào gặp?
3. Phòng tránh và hạn chế như nào?
4. Làm sao để thay đổi?

Technical debt là gì? – Khi nào gặp?
Theo dịch thuật, technical debt có nghĩa là nợ kĩ thuật. Diễn giải ra, nợ kĩ thuật là khi vì tính chất, yêu cầu của dự án, khi code, chúng ta:
– Bỏ qua bước thiết kế hệ thống.
– Bỏ qua design pattern.
– Bỏ qua SOLID.
– Bỏ qua coding conventions.
– Thực hiện hotfix mà chưa đánh giá hết rủi ro.
– Nhằm đáp ứng yêu cầu về deadline, chọn giải pháp chưa thực sự tối ưu để giải quyết vấn đề.
– Copy paste thì nhanh hơn là refactor code.
Với khá nhiều “bỏ qua” và cách tiếp cận giải quyết bài toán như vậy. Dự án sẽ tồn đọng các vấn đề về kĩ thuật, lớn dần theo thời gian. Cho đến thời điểm, dự án sẽ lâm vào trạng thái sửa chữa, cập nhật thì mệt mỏi, đập đi xây lại thì không đủ nguồn lực, thời gian, kinh tế.

Phòng tránh, hạn chế bằng cách nào?
Ngoài các yếu tố phòng tránh về kĩ thuật. Developer hay Technical Leader cần có mindset về việc xử lý technical debt.
Mindset này thể hiện ở cách team tư duy về dự án. Luôn nhớ:
– refactor code ngay khi có thể.
– Chậm chắc, chuẩn chỉ, chỉn chu hơn là nhanh, tiềm ẩn rủi ro.
– Luôn có effort cho việc xử lý technical debt trong thời gian làm dự án.
– Quyết liệt với việc có sản phẩm tốt.

Vài lời chém gió, hi vọng mọi người khi làm dự án sẽ không bao giờ va phải dự án nào cần maintain mà có technical debt.

Xuất phát từ mấy vấn đề nên muốn cover lại kiến thức về static và singleton.

– Một là để hiểu rõ thêm vấn đề
– Hai là làm rõ thêm vấn đề

Vậy static có đặc điểm gì? Khi nào nên dùng nó?
Singleton thì sao? Dùng thế nào? Các vấn đề cần lưu ý khi sử dụng?

Ok, xắn ✋ vào tìm hiểu static trước.

Định nghĩa: một class static sẽ không cho bạn tạo một cài đặt. Nghĩa là bạn sẽ không thể sử dụng từ khoá new để tạo object. Muốn sử dụng methods và properties cần khai báo thêm từ khoá static và gọi trực tiếp bằng tên lớp.

Đặc điểm
– Có vùng nhớ riêng, không bị thay đổi.
– Chỉ có một refrence duy nhất trong ứng dụng.
– Do không thể tạo instantiated, nên không cần cấp phát thêm vùng nhớ.
– Life cycle giống application.
– Khởi tạo một lần duy nhất khi chạy chương trình.

Nên dùng khi nào
– Xây dựng các lớp không phụ thuộc vào object. Ví dụ class Utility, lưu các thông tin dùng chung cho toàn bộ ứng dụng.
– Tiết kiệm bộ nhớ.

Ok, giờ tới singleton.

Mình xin được copy lại góc nhìn của anh Hiền blog guru
Vì sao ra đời?
Trong GoF, Singleton được đưa ra với mục đích sau: “Ensure a class only has one instance, and provide a global point of access to it.” – “Đảm bảo một class chỉ có duy nhất một instance, và cung cấp một điểm truy cập duy nhất trên toàn cục tới instance.”

Lợi ích:
Nó cung cấp ý tưởng của việc một instance duy nhất, điều mà chúng ta gặp trong nhiều bài toán thực tế: một ứng dụng được khởi tạo, một cấu hình hệ thống, một logger…

Đặc điểm:
Về mặt runtime
– Tiết kiệm: Không giống như static trong class, object và các giá trị trong đó chỉ được khởi tạo khi cần thiết. Memory và cả CPU đều được tiết kiệm (static khởi tạo ngay khi chạy chương trình).
– Chủ động quản lý: Cho phép chủ động quản lý life cycle, giải phóng khi cần thiết.
Về mặt design
– Có khả năng thừa kế.
– Có thể kết hợp với các partern khác.
- Abstract hơn sử dụng static trong class.

Bàn thêm về singleton

Tư tưởng: giải quyết 2 bài toán khác nhau (dù có vẻ liên quan): “Ensure a class only has one instance, and provide a global point of access to it.”
– Một class có duy nhất một instance;
– Cung cấp một điểm truy cập duy nhất trên toàn cục tới instance.
– Tác giả đã vô tình kèm cả lời giải trong bài toán với giả định: để có một điểm truy cập toàn cục duy nhất thì chỉ có duy nhất một instance được tạo ra từ một class. Bài toán “một điểm truy cập” có thể được giải quyết bởi Facade, Wrapper… không nhất thiết phải là Singleton.
Runtime
– Không “thân thiện” với theading.
Design
– Coupling: Vì là global state nên các thành phần bị gắn chặt với nhau.
– Khó / không thể viết test.
– Viết dễ sai sót, để lại lỗ hổng (xem bài trước Singleton có thực sự dễ?).

Nên dùng như thế nào?
Như vậy, ta thấy rằng đa phần những thứ dở của design pattern này là ở tư tưởng global state. Vậy nên những ai yêu thích functional programming thì sẽ rất anti-pattern này. Cũng đúng thôi, GoF sinh ra cho OOP, không phải FP. Và thời đại của GoF (1994) cũng không quan tâm nhiều tới concurrency – bài toán trở thành rất cơ bản trong thời đại này. Bởi vậy, việc sử dụng Singleton có chút thay đổi. Có 3 điều cần chú ý:

  1. Concurrency: Global state là điều tệ hại cho concurrency. Hãy giảm thiểu tối đa nếu có thể. Global state bẻ cong cách suy nghĩ về luồng và gây mệt mỏi cho việc debug trong concurrency. Nếu bạn muốn thiết kế hệ thống tối ưu hiệu năng và concurrency thì không sử dụng Singleton cũng là một ý hay.
  2. Memory:
    Lưu cái gì? Global state cũng là một ý hay vì khiến việc thiết kế và lập trình dễ dàng hơn, nó chỉ không hay khi bạn không cân nhắc tới nên lưu gì. Rất nhiều thứ có thể nhìn dưới góc độ một instance nếu chúng ta không có khả năng khái quát hoá. Logger là Singleton không? Hay có errorlog, accesslog? Database là single thì lưu cả database? Hay chỉ connection? Hay chỉ connectionString? Lưu ít nhất có thể.
    Lưu khi nào? Nhiều người hay gắn Singleton với life cycle của cả ứng dụng, kèm theo việc lưu trữ nhiều, hoặc giữ strong reference dẫn đến GC không thể hoạt động; sớm muộn gì cũng gây ra memory leak. Khởi tạo muộn nhất có thể, giải phóng sớm nhất có thể.
  3. Language: Cần lưu ý cách sử dụng trong từng ngôn ngữ, mỗi ngôn ngữ khác nhau sẽ có vấn đề khác nhau. Dù design pattern là mức thiết kế song đừng mang nguyên cách cài đặt từ ngôn ngữ này sang ngôn ngữ khác, hãy nhìn vào diagram và đặc trưng ngôn ngữ.

– Bài viết dựa trên môi trường làm việc là MacOs. Vui lòng không làm theo nếu bạn dùng window.
– Yêu cầu kĩ năng research cơ bản.
– Biết sơ qua các khái niệm: docker, kitematic, sqlserver, terminal (lol).

Cài đặt môi trường:
B1: Cài docker và sqlserver, 2 thằng này thì khá rõ để làm gì rồi ha. (hướng dẫn)
B2: Cài Kitematic, để quản lý các container (link)
B3: Chạy docker, chạy kitematic và start sql trong docker.
Lưu ý: Bỏ qua bước 2 và bước 3 nếu đã chạy sqlserver từ bước 1. Tuy nhiên khuyến khích cài đủ, chạy theo các step 1-2-3.

Tạo ứng dụng:
B1: Tạo ứng dụng mới ví dụ: asp-net-core-code-first
B1′: Thêm 2 thư viện Microsoft.EntityFrameworkCore.SqlServerMicrosoft.EntityFrameworkCore.Design
B2: Tạo class Person

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
}

B3: Tạo class PersonContext

public class PersonContext : DbContext
{
   DbSet<Person> Persons { get; set; }

   protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
   {
     optionsBuilder.UseSqlServer("Server=localhost\\sql_server_demo,1433;User=sa;Password=reallyStrongPwd123;Database=PersonDb1");
   }
}

B4: Chạy migrate trong terminal dotnet ef migrations add initial
B5: Update database dotnet ef database update
Done!
Tham khảo: ciclosoftware

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