Tôi đã thiết kế ra design pattern Trứng có trước hay Gà có trước như thế nào

1603

Bài viết được sự cho phép của tác giả Phạm Công Sơn

Nhắc đến design pattern thì có lẽ đã quá quen thuộc với dân lập trình rồi. Ngay từ ngày nhập học ở FPT APTECH vào năm 2005 tôi vẫn nhớ rất rõ lời thầy Thành giám đốc trung tâm rằng “Các bạn sẽ được đào tạo để làm việc với ngôn ngữ lập trình hướng đối tượng”. Tuy chưa hiểu mấy cho đến khi trải qua môn Java, C# tôi cũng đã bắt đầu dần hiểu về OOP và tiếp xúc với design pattern. Tại thời điểm đó trang web mà tôi học về design pattern là https://www.dofactory.com/. Hồi còn đi học nói chung là có rất nhiều tài liệu trên internet để tham khảo, nhưng có lẽ dofactory là một huyền thoại đối với tôi, vì nó là trang web duy nhất giúp tôi học lập trình mà còn nhớ từ hồi đó cho tới tận bây giờ.

  Design pattern là gì? Tại sao nên sử dụng Design pattern?
  10 kênh Youtube học lập trình không thể bỏ qua dành cho Junior Web Developer / Designer

Tiếp cận từ sớm, ngay từ lúc còn mới học code tuy vậy cũng phải mất tới vài năm đi làm tôi mới có thể sử dụng được một vài design pattern trong một số trường hợp, bài toán. Còn lại cũng nhiều design partern có biết, có học cho tới nay vẫn chưa được sử dụng. Vì không phải design pattern nào cũng có thể đưa vào thực tế. Tôi cũng đã có một khoảng thời gian chỉ làm Frontend và chả động tí gì tới design pattern luôn.

Khi chưa được vào các dự án lớn thì nhu cầu dùng cũng sẽ là rất ít. Cho đến khi vào khoảng năm 2014 tôi vào làm việc tại một công ty định vị. Lúc này tôi phải làm việc nhiều tới xử lý số liệu, xử lý giao tiếp giữa các khối v.v… thì hầu như chỗ nào tôi cũng phải thiết kế code và sử dụng design pattern.

Liên tục sử dụng design pattern trong các dự án, thiết kế code trước khi thực hiện đã trở thành một thói quen của tôi. Lâu dần đã thành kỹ năng, và điển hình là tôi đã thiết kế được mô hình “Trứng có trước hay gà có trước” được sử dụng khá là nhiều nhưng ít nhất có 2 dự án lớn đó là: Dự án Phân tích dữ liệu hộp đen cho Taxi, Bus và Dự án Staxi xây dựng Server giao tiếp giữa các khối: Mobile, Tổng đài, Server.

Hôm nay tôi xin chia sẻ với các bạn về mô hình này.

1. Từ đâu mà tôi thiết kế mô hình này?

Trong lần nhận dự án xử lý dữ liệu logfile hộp đen cho Oto, Taxi, Bus tôi cần phải xây dựng 3 hệ thống phân tích data. Ở đây có thể hình dung có 3 hệ thống nhưng cách thức hoạt động sẽ là hoàn toàn giống nhau. Sự khác biệt ở đây chương trình phân tích chỉ là cấu hình, đầu vào và các xử lý khác nhau. Dẫn tới yêu cầu đặt ra khi viết các xử lý phân tích ở hệ thống nào thì sử dụng cấu hình, data đầu vào phải là của hệ thống đó. Chính vì điều này việc sử dụng interfaceabstract ở đây cho cấu trúc data và các xử lý là chưa đủ do tính ràng buộc chặt chẽ giữa các đối tượng trong cấu trúc hệ thống.

Nói thì có thể khó hiểu, chúng ta có thể xem qua ví dụ sau cho dễ hiểu

public class Family
{
    public IFather Father { set; get; }
    public IChild Child { set; get; }

    public void Happy()
    {
        Father.LoveChild(Child);
        Child.LoveFather(Father);
    }
}
public interface IFather
{
    void LoveChild(IChild child);
}
public interface IChild
{
    void LoveFather(IFather father);
}

Ví dụ này miêu tả quan hệ gia đình =)). Bố yêu con và con yêu bố. Tuy nhiên sẽ bị trường hợp bố yêu nhầm con ông hàng xóm. Các bạn hãy xem code kế thừa tiếp theo.

public class FatherVN : IFather
{
    public bool Nice { set; get; }
    public void LoveChild(IChild child)
    {
        // Không thể truy vấn thuộc tính child.Cry
        // Vì child ở đây chỉ là interface IChild
    }
}

public class ChildVN : IChild
{
    public bool Cry { set; get; }
    public void LoveFather(IFather father)
    {
        // Không thể truy vấn thuộc tính Nice
        // vì father ở đây chỉ là interface IFather
    }
}

public class FatherUSA : IFather
{
    public bool Strong { set; get; }
    public void LoveChild(IChild child)
    {
        // Không thể truy vấn thuộc tính child.Tall
        // Vì child ở đây chỉ là interface IChild
    }
}

public class ChildUSA : IChild
{
    public bool Tall { set; get; }
    public void LoveFather(IFather father)
    {
        // Không thể truy vấn thuộc tính Strong
        // vì father ở đây chỉ là interface IFather
    }
}

Giả sử chúng ta có 2 gia đình đại diện cho Việt Nam và Mỹ thì ta có những claas như sau FatherVNChildVNFatherUSAChildUSA ở đây có thể thấy tại các phương thức LoveChild chúng ta không thể truy vấn tới thuộc tính của biến father và tương tự phương thức LoveChild cũng không thể truy vấn tới thuộc tính của biến child.

Chúng ta xem tiếp phần sử dụng sau khi đã cụ thể hóa các class

var familyVN = new Family
{
    Father = new FatherVN { },
    Child = new ChildVN { }
};
familyVN.Happy();

var familyUSA = new Family
{
    Father = new FatherUSA { },
    Child = new ChildUSA { }
};
familyUSA.Happy();

Đây là khi sử dụng đúng hệ thống, có nghĩa là con ông nào thì thương bố ông ấy, bố ông nào thì thương con ông ấy. Nhưng nếu là như thế này thì sẽ có sự nhầm lẫn.

var familyUnknown = new Family
{
    Father = new FatherVN { },
    Child = new ChildUSA { }
};
familyUnknown.Happy();

Và giờ đây rất dễ thấy là bị nuôi con ông hàng xóm =)). Từ đó có thể thấy nếu sử dụng interface hay abstract một cách thông thường thì sẽ không đáp ứng được yêu cầu của hệ thống là các đối tượng phải liên kết chặt chẽ với nhau. Hệ thống nào có đối tượng nào thì chỉ sử dụng trong hệ thống đó thôi. Con tôi tôi nuôi, con ông hàng xóm ông hàng xóm nuôi. Không có chuyện lẫn lộn gia đình này với gia đình kia. Đó chính là nguyên nhân và cũng là mục đích tôi thiết kế ra mô hình “Trứng có trước hay Gà có trước”.

2. Cụ thể cách thức xây dựng mô hình

Vẫn là bài toán bố yêu con như trên nhưng để đảm bảo con ông nào ông ấy yêu thương, nuôi nấng thì tôi tổ chức các thành phần như sau

public interface IChild { }
public interface IFather { }

public interface IChild : IChild where TFather : IFather
{
    void LoveFather(TFather father);
}

public interface IFather : IFather where TChild : IChild
{
    void LoveChild(TChild child);
}

Như các bạn đã thấy, lúc này đối tượng đã có 1 sự liên kết nhẹ là con đã cụ thể yêu một ông bố nào đó, và ông bố cũng yêu cụ thể một ông con nào đó. Không còn mập mờ nữa. Tuy nhiên điều đó là chưa đủ. Còn cần phải khai báo một đối tượng Family để gói chặt 2 bố con lại với nhau như sau

public class Family<TFather, TChild>
    where TFather : IFather, IFather, new()
    where TChild : IChild, IChild, new()
{
    public TFather Father { set; get; } = new TFather { };
    public TChild Child { set; get; } = new TChild { };

    public void Happy()
    {
        Father.LoveChild(Child);
        Child.LoveFather(Father);
    }
}

Đến lúc này thì đúng là bố con đã trở về một gia đình. Không còn lẫn lộn bố con ông hàng xóm nữa. TFather phải là bố của TChild và TChild phải là con của TFather sự ràng buộc này luôn tạo một khối duy nhất. Và từ đó ta tạo ra các khối có chức năng tương tự nhau giống nhau, nhưng cụ thể xử lý là khác nhau.

public class FatherVN : IFather
{
    public bool Nice { set; get; }
    public void LoveChild(ChildVN child)
    {
        // Truy vấn được thuộc tính child.Cry
    }
}
public class ChildVN : IChild
{
    public bool Cry { set; get; }
    public void LoveFather(FatherVN father)
    {
        // Truy vấn được thuộc tính father.Nice
    }
}

public class FatherUSA : IFather
{
    public bool Strong { set; get; }
    public void LoveChild(ChildUSA child)
    {
        // Truy vấn được thuộc tính child.Tall
    }
}

public class ChildUSA : IChild
{
    public bool Tall { set; get; }
    public void LoveFather(FatherUSA father)
    {
        // Truy vấn được thuộc tính father.Tall
    }
}

var familyUSA = new Family<FatherUSA, ChildUSA> { };
familyUSA.Happy();

var familyVN = new Family<FatherVN, ChildVN> { };
familyVN.Happy();

Nếu như chúng ta cố tình như sau

var familyUnknown1 = new Family<FatherUSA, ChildVN> { }; // Bố Mỹ nhưng con Việt Nam
var familyUnknown2 = new Family<FatherVN, ChildUSD> { }; // Bố Việt Nam nhưng con Mỹ

Lúc này sẽ lỗi và không thể thực hiện biên dịch được. Và tên gọi “Trứng có trước hay Gà có trước” cũng từ đó mà tôi đặt tên cho mô hình này. Vì đơn giản là các class khai báo không biết là cái nào có trước cái nào có sau. FatherVN khai trước hay ChildVN khai trước? Khai báo FatherVN nhưng lại cần có ChildVN trước, nhưng khai báo ChildVN lại cần có FatherVN trước cơ. Ơ hại não nhỉ

3. Ứng dụng thực tế trong các dự án tôi đã thực hiện

Để xây dựng các hệ thống có chức năng tương tự nhau mà trong đó các thành phần được tổ chức liên kết chặt chẽ, nhằm mục đích các đối tượng truy vấn đến đích xác kiểu dữ liệu của các đối tượng mà không thông qua interface.

3.1 Cụ thể trong dự án phân tích dữ liệu logfile

public abstract partial class Adapter<TSystem, TClient, TClientInfo, TRawData> : IAdapter, IAdapter
        where TSystem : SystemBase<TClient, TRawData>, new()
        where TClient : ClientControlBase, new()
        where TRawData : RawData, new()
        where TClientInfo : IClientInfo
{
    // Code code
}
public abstract class ClientControl<TSystem, TRawData, TStatus> : ClientControlBase         where TRawData : RawData, new()
        where TStatus : ClientState, new()
        where TSystem : ISystem
{
    // Code code
}
public abstract partial class SystemBase<TClientControl, TRawData> : ISystem     where TClientControl : ClientControlBase, new()
    where TRawData : RawData, new()
{
    // Code Code
}
public abstract partial class ClientControlBase : ClientControlBase where TRawData : RawData, new()
{
    // Code code
}

Sau đây là cụ thể các hệ thống phân tích

// Hệ thống Taxi
public class TaxiAdapter : Adapter<TaxiSystem, TaxiClientControl, TaxiClient, TaxiRawData>
{
}
public class TaxiClientControl : ClientControl<TaxiSystem, TaxiRawData, TaxiState>
{
}
public class TaxiSystem : SystemBase<TaxiClientControl, TaxiRawData>
{
}
// Khai báo class nào trước bi giờ nhỉ :D
// Khai báo TaxiAdapter lại cần có TaxiSystem, TaxiClientControl trước
// Khai báo TaxiClientControl lại cần có TaxiSystem Trước
// Mà khai báo TaxiSystem lại cần có TaxiClientControl trước cơ

// Hệ thống Bus
public class BusAdapter : Adapter<BusSystem, BusClientControl, BusClient, BusRawData>
{
}
public class BusClientControl : ClientControl<BusSystem, BusRawData, BusState>
{
}
public class BusSystem : SystemBase<BusClientControl, BusRawData>
{
}
// Khai báo class nào trước bi giờ nhỉ :D
// Khai báo BusAdapter lại cần có BusSystem, BusClientControl trước
// Khai báo BusClientControl lại cần có BusSystem Trước
// Mà khai báo BusSystem lại cần có BusClientControl trước cơ

3.2 Cụ thể trong dự án Staxi, có các khối Mobile, Điều Hành, Server có chung chức năng truyền và nhận bản tin

public abstract class Connection<TCommand, TConnection, TCenter, TState> : Connection<TCommand, TConnection, TCenter>
    where TCommand : Command
    where TConnection : Connection
    where TCenter : ICenter
    where TState : ConnectionState, new()
{
    // Code Code
}
public abstract class Command<TConnection, TCommandInfo> : Command
    where TConnection : Connection
    where TCommandInfo : ICommandInfo, new()
{
    // Code Code
}
public abstract class Center<TThread, TCenter> : Messagable, ICenter, ICenterMessage
    where TThread : CenterThread
    where TCenter : class, ICenter
{
    // Code Code
}

Sau đây là cụ thể khối nhận bản tin từ Mobile lái xe gửi về Server

public class DriverServerCenter : Center<DriverServerWorker, DriverServerCenter>
{
}
public abstract class DriverServerWorker : CenterThread
{
}
public class DriverClient : Connection<Command, DriverClient, DriverServerCenter, DriverState>, IConnectionWithManager, TConnectionKey
{
}
public abstract class DriverCommand : Command<DriverClient, TCommandInfo> where TCommandInfo : ICommandInfo, new()
{
}
// Vậy khai báo class nào trước class nào sau các bạn nhỉ
// Khai báo DriverServerCenter thì lại cần DriverServerWorker trước, thậm chí là chính nó trước DriverServerWorker
// Khai báo DriverServerWorker thì lại cần DriverServerCenter trước
// Khai báo DriverClient lại cần DriverServerCenter trước, thậm chí lại cần chính nó trước luôn.

Cụ thể khối nhận bản tin từ Mobile khách hàng gửi về Server

public partial class CustomerServerCenter : Center<CustomerServerWorker, CustomerServerCenter>
{}
public abstract class CustomerServerWorker : CenterThread
{}
public partial class CustomerClient : Connection<Command, CustomerClient, CustomerServerCenter, CustomerState>, IConnectionWithManager, TConnectionKey
{}
public abstract class CustomerCommand : Command<CustomerClient, TCommandInfo> where TCommandInfo : ICommandInfo, new()
{}
// Vậy khai báo class nào trước class nào sau các bạn nhỉ
// Khai báo CustomerServerCenter thì lại cần CustomerServerWorker trước, thậm chí là chính nó trước CustomerServerCenter
// Khai báo CustomerServerWorker thì lại cần CustomerServerCenter trước
// Khai báo CustomerClient lại cần CustomerServerCenter trước, thậm chí lại cần chính nó trước luôn.

Cụ thể code thì bên trong xử lý rất nhiều. Tuy nhiên khi tôi dựng xong hệ thống, tôi chỉ việc viết thao tác xử lý các bản tin một cách rất nhẹ nhàng, còn lại là tầng base đã xử lý hết.

4. Nhược điểm của mô hình

Về ưu điểm thì tôi có thể tạo rất nhanh các khối có chức năng giống nhau và ở mỗi hệ thống chỉ việc viết các xử lý cụ thể. Tuy nhiên một nhược điểm là với mô hình này, ở lớp cụ thể cuối cùng chúng ta hoàn toàn không thể kế thừa thêm được nữa.

Chưa kể mô hình này chỉ áp dụng cho các ngôn ngữ native, tường minh và hỗ trợ Generic. Những ngôn ngữ không tường minh như Javascript thì nó trở lên vô nghĩa.

Nếu các bạn thấy bài viết hay bổ ích hãy like và chia sẻ nhé. Nếu chỗ nào không hài lòng vui lòng comment nhẹ nhàng để tôi có động lực viết thêm chia sẻ cho các bài tiếp theo.

Sơn 20

Bài viết gốc được đăng tải tại sonpc20.com

Có thể bạn quan tâm:

Xem thêm các việc làm Developer hấp dẫn tại TopDev