Chào các bạn, trong bài viết này, chúng ta sẽ đi tìm hiểu về một design pattern khá thú vị, đó là Composite pattern. Trong tiếng anh thì từ composite có nghĩa là làm một hỗn hợp, tức là một thứ gì đó được tạo từ rất nhiều thành phần hỗn tạp. Vậy cụ thể design pattern này là gì, được ứng dụng như thế nào thì bây giờ chúng ta sẽ cùng tìm hiểu

1. Composite Pattern là gì?

Composite Pattern là một mẫu cấu trúc (Structural Pattern).

Composite Pattern cho phép tương tác với tất cả các đối tượng tương tự nhau giống như là các đối tượng đơn hoặc collections.

Nếu bạn đã làm quen với cấu trúc dữ liệu dạng tree, mỗi thành phần trên tree chúng ta có thể gọi là các Nodes, mỗi node sẽ có duy nhất một node cha và có không hoặc nhiều node con. Trường hợp node mà không có node con thì được gọi là lá

Điều này tương tự đối với Composite Pattern, những element có con được gọi là Nodes, những element không có con được gọi là Leafs.

Trong thực tế thì chúng ta gặp nhiều bài toán cần dùng đến cấu trúc như vậy như hệ thống các thư mục, file, hệ thống các thẻ tag trong html, xml,…

Hình minh họa cho composite design pattern

2. Các thành phần cơ bản

Composite Pattern bao gồm các thành phần:

  • Component protocol
  • Leaf
  • Composite
  • Client

Hình minh họa các thành phần cơ bản trong composite pattern

Hình minh họa thành phần của Composite Pattern

Trong biểu đồ trên chúng ta có thể quan sát thấy:

  • Component:
    • Khai báo interface hoặc abtract chung cho các thành phần đối tượng.
    • Chứa các method thao tác chung của các thành phần đối tượng.
  • Leaf:
    • Biểu diễn các đối tượng lá trong thành phần đối tượng.
  • Composite:
    • Định nghĩa một thao tác cho các thành phần có thành phần con.
    • Lưu trữ các thành con.
    • Thực thi sự quản lý các thành phần con của Component.
  • Client:
    • Điều khiển các đối tượng trong Composite thông qua các method trong Component.

3. Ví dụ demo

Đặt vấn đề:

Chúng ta sẽ mô phỏng lại cấu trúc phân cấp của 1 file html. Một file html sẽ bắt đầu bằng thẻ html tiếp sau đó là tập hợp các thẻ khác, mỗi thẻ có thể là parent tag hoặc child tag tùy vào tính huống cụ thể. Mục đích của chúng ta là tạo ra cấu trúc phân cấp và hiển thị cấu trúc phân cấp này ra ngoài màn hình

Trong ví dụ này chúng ta sẽ sử dụng interface để khai báo các phương thức được dùng trong các composite và leaf.

component.go

package composite

type hmtlTag interface {
    SetStartTag(string)
    SetEndTag(string)
    GenerateHtml()
}

Trong interface hmtlTag chúng ta liệt kê các method đều sẽ được dùng bởi các lớp composite và leaf.

Bây giờ chúng ta sẽ tạo đối tượng composite như sau

composite.go

package composite

import "fmt"

type HtmlParentElement struct {
    StartTag string
    EndTag   string
    Tags     []hmtlTag
}

func (tag *HtmlParentElement) SetStartTag(name string) {
    tag.StartTag = name
}

func (tag *HtmlParentElement) SetEndTag(name string) {
    tag.EndTag = name
}

func (tag *HtmlParentElement) AddChildTag(htmlTag hmtlTag) {
    tag.Tags = append(tag.Tags, htmlTag)
}

func (tag *HtmlParentElement) GenerateHtml() {
    fmt.Println(tag.StartTag)
    for _, t := range tag.Tags {
        t.GenerateHtml()
    }
    fmt.Println(tag.EndTag)
}

HtmlParentElement là đối tượng composite và chúng ta cần khai báo các method để implement interface hmtlTag

leaf.go

package composite

import "fmt"

type HtmlElement struct {
    TagBody  string
    StartTag string
    EndTag   string
}

func (tag *HtmlElement) SetStartTag(name string) {
    tag.StartTag = name
}

func (tag *HtmlElement) SetEndTag(name string) {
    tag.EndTag = name
}

func (tag *HtmlElement) SetTagBody(content string) {
    tag.TagBody = content
}

func (tag *HtmlElement) GenerateHtml() {
    fmt.Printf("%s%s%s\n", tag.StartTag, tag.TagBody, tag.EndTag)
}

HtmlElement là đối tượng leaf. Tương tự như với HtmlParentElement, chúng ta cũng cần khai báo các method để implement interface hmtlTag

main.go

package main

import (
    "composite-pattern/composite"
)

func main() {
    // Tạo thẻ html
    htmlTag := composite.HtmlParentElement {
        StartTag: "<html>",
        EndTag: "</html>",
    }

    // Tạo thẻ body
    var bodyTag = composite.HtmlParentElement {
        StartTag: "<body>",
        EndTag: "</body>",
    }
    htmlTag.AddChildTag(&bodyTag)

    // Tạo thẻ heading
    var headingTag = composite.HtmlElement {
        StartTag: "<h2>",
        EndTag: "</h2>",
    }
    headingTag.SetTagBody("Xin chào các bạn")
    bodyTag.AddChildTag(&headingTag)

    // Tạo thẻ para
    var paraTag = composite.HtmlElement {
        StartTag: "<p>",
        EndTag: "</p>",
    }
    paraTag.SetTagBody("Các bạn có khỏe không")
    bodyTag.AddChildTag(&paraTag)

    htmlTag.GenerateHtml()
}

Trong file main.go chúng ta tạo ra 1 số thẻ html, body, h2, p. Trong đó body nằm trong html, h2p nằm trong body

Bây giờ chạy go run main.go và kiểm tra kết quả nào

Kết quả chúng ta có như sau:

<html>
<body>
<h2>Xin chào các bạn</h2>
<p>Các bạn có khỏe không</p>
</body>
</html>

Khi nào dùng Composite Pattern

  • Khi chúng ta muốn tạo ra các đối tượng trong các cấu trúc cây để biểu diễn hệ thống phân lớp.
  • Áp dụng khi nhóm đối tượng phải hoạt động như một đối tượng duy nhất

Ưu điểm

  • Giúp chúng ta có thể làm việc với các cấu trúc cây phức tạp trở lên thuận tiện hơn
  • Tuân thủ quy tắc "Open/Closed Principle" : Chúng ta có thể dễ dàng thêm một đối tượng mới vào ứng dụng mà không cần phải thay đổi code hiện có

Nhược điểm

  • Chúng ta khó có thể cung cấp interface cho các lớp mà chức năng của nó khác nhau quá nhiều