Design Pattern: Builder pattern

Chào các bạn, Ở những bài viết trước, mình đã đi qua 5 nguyên tắc trong SOLID. Ở bài viết này mình sẽ bắt đầu đi vào design pattern đầu tiên, đó là Builder Pattern. Builder pattern là một pattern nằm trong nhóm khởi tạo (Creational Pattern). Trước tiên mình sẽ đi vào khái niệm để biết builder pattern là cái gì.

Builder pattern là gì?

Giả sử chúng ta có một object với rất nhiều thuộc tính và việc khởi tạo những thuộc tính đó qua constructor sẽ làm cho constructor của chúng ra rườm rà và tăng khả năng lỗi. Hơn nữa không phải lúc nào những thuộc tính đó cũng được khởi tạo 1 cách đồng bộ. Có những thuộc tính được khởi tạo khi dịch chương trình, có những thuộc tính tùy theo yêu cầu người dùng và ngữ cảnh của chương trình. Builder ra đời để giải quyết vấn đề đó bằng việc chia nhỏ việc khởi tạo các tham số của object thành nhiều phương thức nhỏ.

Một ví dụ chúng ta thường hay gặp có sử dụng Builer pattern đó là StringBuilder. Chúng ta hãy cũng xem một ví dụ sử dụng StringBuilder nhé.

public static void Main()
{
    List<string> greeting = new List<string>() { "Hello", "World" };
    var str = new StringBuilder();

    str.AppendLine("<ul>");
    foreach (var word in greeting)
    {
        str.AppendFormat("<li>{0}</li>", word);
        str.AppendLine();
    }
    str.AppendLine("</ul>");

    Console.WriteLine(str);
}

Thay vì nối chuỗi string thì chúng ta khai báo một StringBuilder và append thêm vào chuỗi ban đầu. Đây là một ví dụ nhỏ mà chúng ta thường hay bắt gặp. Ngoài lề chút là bạn hãy tìm hiểu sự khác nhau giữa String và StringBuilder 🙂 Sử dụng cái nào tốt hơn. Tham khảo bài viết này nhé. Chúng ta hãy cùng áp dụng thử builder để làm một ứng dụng nhỏ nhé.

Áp dụng vào ví dụ nhỏ

public class HtmlElement
{
    public string Name { get; set; }

    public string Text { get; set; }

    public List<HtmlElement> Elements { get; set; }
        = new List<HtmlElement>();

    private int _indentSize = 2;

    public HtmlElement()
    {
    }

    public HtmlElement(string name, string text)
    {
        Name = name;
        Text = text;
    }

    public string ToStringImp(int indent)
    {
        var sb = new StringBuilder();
        var i = new string(' ', _indentSize * indent);

        sb.AppendLine($"{i}<{Name}>");

        if (!string.IsNullOrEmpty(Text))
        {
            sb.Append(new string(' ', _indentSize * (indent + 1)));
            sb.AppendLine(Text);
        }

        foreach (var htmlElement in Elements)
        {
            sb.Append(htmlElement.ToStringImp(indent + 1));
        }

        sb.AppendLine($"{i}</{Name}>");

        return sb.ToString();
    }

    public override string ToString()
    {
        return ToStringImp(0);
    }
}

Đây là một class chứa thông tin của một thẻ HTML bao gồm tên của tên thẻ html và text. Chúng ta có phương thức ToStringImp() để in ra. Tiếp theo là class Builder

public class HtmlBuilder
{
    private readonly string rootName;

    private HtmlElement root = new HtmlElement();

    public HtmlBuilder(string name)
    {
        rootName = name;
        root.Name = name;
    }

    public void AddChild(string name, string text)
    {
        var element = new HtmlElement(name, text);
        root.Elements.Add(element);
    }

    public override string ToString()
    {
        return root.ToString();
    }

    public void Clear()
    {
        root = new HtmlElement() { Name = rootName };
    }
}

HtmlBuilder khởi tạo với một constructor nhận tham số là tên của root tag. Builder có phương thức AddChild() để thêm mới một HtmlElement con trong root. Phương thức Clear() để khỏi tạo lại root và đảm bảo vẫn giữ cái name.

var htmlBuilder = new HtmlBuilder("ul");

htmlBuilder.AddChild("li", "Hello");
htmlBuilder.AddChild("li", "World");

Console.WriteLine(htmlBuilder.ToString());

Đoạn code thực thi chương trình ra kết quả như sau:

builder pattern

Xin nhắc lại đây chỉ là một ví dụ để thấy được tổng thể pattern. :)) Ví dụng mở rộng không thật sự tốt.

Fluent Builder

Thỉnh thoảng chúng ta thấy người ta sử dụng StringBuilder theo cách này

str.AppendLine("<ul>")
   .AppendFormat("<li>{0}</li>", word)
   .AppendLine()
   .AppendLine("</ul>");

Nhìn đoạn code trên gọn gàng hơn và có vẻ bờ-rồ hơn. Làm thế éo nào mà họ làm được như vậy? Bạn có thể search với từ khóa Fluent Interface để hiểu rõ hơn. Mình sẽ chỉnh sửa một chút để biến HtmlBuilder thành một Fluent Builder. Sửa lại phương thức AddChild() như sau:

 public HtmlBuilder AddChild(string name, string text)
{
      var element = new HtmlElement(name, text);
      root.Elements.Add(element);
      return this;
}

Và bây giờ chúng ta có thể gọi theo cách bờ rồ hơn: htmlBuilder.AddChild("li", "Hello").AddChild("li", "World"); 😆

Ví dụ khác về Fluent Builer

Giả sử chúng ta có một kịch bản như sau: Có một đối tượng Person cần lưu trữ lại thông tin bao gồm Name và Position. Chúng ta xây dựng 1 builder để set thuộc tính Name. Chúng ta có đoạn chương trình như sau:

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

    public string Position { get; set; }

    public override string ToString()
    {
        return $"Name: {Name} - Position: {Position}";
    }
}

public class PersonInfoBuilder
{
    protected Person person = new Person();

    public PersonInfoBuilder Called(string name)
    {
        person.Name = name;
        return this;
    }

    public Person Build()
    {
        return person;
    }
}

public static void Execute()
{
    var builder = new PersonInfoBuilder();
    var person = builder.Called("Vinh").Build();

    Console.WriteLine(person);
}

Đoạn code hoàn toàn không có gì phức tạp 🙂 Một ngày nào đó nghiệp vụ thay đổi cần set thêm thuộc tính là Position. Để đảm bảo chữ O trong SOLID nên chúng ta không modify PersonInfoBuilder mà sẽ viết thêm 1 class kế thừa lại. Class PersonJobBuilder mới sẽ như sau:

public class PersonJobBuilder : PersonInfoBuilder
{
    public PersonJobBuilder WorkAsA(string position)
    {
        person.Position = position;
        return this;
    }
}

Vấn đề thực sự bắt đầu, chúng ta không thể dùng gọi  PersonJobBuilder theo kiểu Fluent như ví dụ bên trên được nữa.

builder pattern fluent builder

Lý do đơn giản ở đây là phương thức Called() đã trả về kiểu PersonInfoBuildervà nó không biết được mặt mũi phương thức WorkAsA() như thế nào.

Fluent Builder Inheritance với Recursive Generic

Để giải quyết vấn đề đó chúng ta sẽ thay đổi đoạn chương trình như sau:

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

    public string Position { get; set; }

    public DateTime DOB { get; set; } // Add thêm thuộc tính Date of birth

    public override string ToString()
    {
        return $"Name: {Name} - Position: {Position} - DOB:{DOB.ToShortDateString()}";
    }

    public class Builder : PersonDOBBuilder<Builder>
    {
    }

    public static Builder New => new Builder();
}

public abstract class PersonBuilder
{
    protected Person person = new Person();

    public Person Build()
    {
        return person;
    }
}

public class PersonInfoBuilder<SELF> 
     : PersonBuilder where SELF : PersonInfoBuilder<SELF> // SELF ở đây chính là PersonInfoBuilder<SELF>
{
    public SELF Called(string name)
    {
        person.Name = name;
        return (SELF)this; // trình biên dịch sẽ báo lỗi chỗ này. Chúng ta phải cast về SELF 
    }
}

public class PersonJobBuilder<SELF> 
    : PersonInfoBuilder<PersonJobBuilder<SELF>> where SELF : PersonJobBuilder<SELF> 
{
    public SELF WorkAsA(string position)
    {
        person.Position = position;
        return (SELF)this; // trình biên dịch sẽ báo lỗi chỗ này. Chúng ta phải cast về SELF 
    }
}

// Builder mới để chắc chắn khả năng mở rộng của nó.
public class PersonDOBBuilder<SELF> : PersonJobBuilder<PersonDOBBuilder<SELF>> where SELF : PersonDOBBuilder<SELF>
{
    public SELF DOB(DateTime DOB)
    {
        person.DOB = DOB;
        return (SELF)this; // trình biên dịch sẽ báo lỗi chỗ này. Chúng ta phải cast về SELF 
    }
}

public static void Execute()
{
    var person = Person.New
        .Called("Vinh")
        .WorkAsA("Dev")
        .DOB(DateTime.UtcNow)
        .Build();

    Console.WriteLine(person);
}

Nhìn đoạn code trên có vẻ phức tạp. Bình tĩnh tự tin đừng manh động. Hãy cùng phân tích đoạn code trên. Đầu tiên là tạo một class trừu tượng cho Builder để kế thừa mở rộng builder. Tiếp theo là sửa lại PersonInfoBuilder một chút để có khả năng mở rộng bằng việc kế thừa PersonBuilder. Ở đây chúng ta có sử dụng kỹ thuật truyền vào một kiểu Generic mà kiểu Generic đó chính là bản thân cái class đó. Bạn có thể tìm hiểu thêm kỹ thuật đó bằng từ khóa Recursive GenericPersonJobBuilder kế thừa PersonInfoBuilderPersonDOBBuilder kế thừa PersonJobBuilder. tạo thành một mắt xích (chain).

Nhưng vì kỹ thuật này chúng ta sẽ không thể nào áp dụng được cách khai báo builder như sau: var personBuilder = new PersonDOBBuilder<>();Chúng ta không biết kiểu để vào PersonDOBBuilder. Đó là lý do bạn thấy ở class Person có thêm một class lồng vào là Builder kế thừa từ PersonDOBBuilder<Builder> và phương thức khởi tạo New cho Builder. Có vẻ phức tạp nhưng để hiểu rõ thì bạn cứ code ra và chạy thử để hiểu rõ hơn.

Chưa kết thúc đâu nhé. Mình còn một vài ví dụ nữa về Builder pattern nhưng có lẽ bài viết đã quá dài nên mình hẹn gặp lại các bạn vào bài viết sau. Hy vọng các bạn tiếp tục ủng hộ và đóng góp ý kiến cho mình để những bài viết của mình thật sự tốt hơn. Mọi ý kiến thắc mắc các bạn cứ comment bên dưới. Mình sẽ cùng nhau giải quyết   🙂

 

Tài liệu tham khảo:

https://en.wikipedia.org/wiki/Builder_pattern

https://stackoverflow.com/questions/26304527/recursive-generic-and-fluent-interface

https://www.sitepoint.com/self-types-with-javas-generics/

Source code trên github

Leave a Reply