Design-Patterns

Composite

Giới thiệu

Định nghĩa Pattern

Composite Pattern là một mẫu thiết kế thuộc nhóm cấu trúc, được sử dụng để tổ chức các đối tượng vào một cấu trúc cây. Mẫu thiết kế này tạo ra một hệ thống phân cấp cho phép người dùng xử lý các đối tượng đơn lẻ và tổ hợp của chúng một cách thống nhất.

Mục đích

Mục đích chính của Composite Pattern là đơn giản hóa quá trình làm việc với các cấu trúc phức tạp bằng cách cho phép client tương tác với các đối tượng đơn lẻ và tổ hợp theo cùng một cách. Điều này giúp giảm thiểu sự phức tạp khi quản lý và tương tác với cấu trúc cây, làm cho mã nguồn dễ bảo trì và mở rộng hơn.

Ý tưởng chính của Pattern

Ý tưởng cốt lõi của Composite Pattern nằm ở việc cung cấp một interface chung cho cả hai loại đối tượng: đơn lẻ và tổ hợp. Interface này cho phép client tương tác với mỗi đối tượng một cách riêng lẻ hoặc nhóm các đối tượng lại với nhau như một thể thống nhất mà không cần quan tâm đến đặc điểm nội tại của chúng. Kết quả là, client có thể thêm, xóa hoặc thay đổi các đối tượng trong cấu trúc cây một cách linh hoạt mà không cần viết lại code hoặc hiểu biết sâu sắc về cấu trúc nội bộ.

Đặt vấn đề

Khi sử Composite Pattern bạn phải chắc chắn rằng mô hình ứng dụng của bạn có thể biểu hiện bằng sơ đồ cây.

Ví dụ như sau: Trong việc lưu trữ trong máy tính có hai dạng chính: FolderFile. Một Folder thì có thể chứa nhiều FolderFile. Có thể một trong Folder chỉ chứa File và trong File thì chứa nội dụng.

graph TB
    id1(Folder)
    id2(Folder)
    id3(Folder)
    id4(File)
    id5(File)
    id6(File)
    id7(File)
    id1 --> id2
    id1 --> id3
    id1 --> id4
    id2 --> id5
    id3 --> id6
    id3 --> id7

    subgraph traditional_approach
    id1 --> id2
    id2 --> id5
    id1 --> id4
    id1 --> id3
    id3 --> id6
    id3 --> id7
    end

Giờ giả sử ta cần tìm tất cả File trong một Folder. Thử cách tiếp cận thông thường là ta sẽ mở từng Folder con ra và đếm xem co bao nhiêu File vào Folder tiếp theo đếm tiếp. Nhưng trong lập trình nó không hề đơn giản như việc bạn chỉ cần chạy một dòng for. Bạn phải biết trước loại File và Folder mà sẽ duyệt và mực đồ lòng vào nhau. Tất cả điều đó làm cho cách tiếp cận này trở nên khó khăn hơn.

Giải pháp

Chúng ta sẽ sử chung Composite Pattern để thực hiện công việc với Folder và File bằng cách tạo một interrface chung với một phương thức count(Đếm)

classDiagram
    class Component {
        +count() int
    }

    class File {
        +count() int
    }

    class Folder {
        +List~Component~ children
        +count() int
        +add(component Component) void
        +remove(component Component) void
    }

    Component <|-- File: is a
    Component <|-- Folder: is a
    Folder "1" *-- "*" Component: contains

Cái này hoạt động như sau. Đối với File thì chỉ trả về cộng một, Đối với Folder thì nó sẽ duyệt từng item trong Folder đó, bắt từng item đếm sau cùng tới lượt nó tổng hợp lại và trả về tổng số của Folder. Nếu một các item là Folder thì sao? Thì nó sẽ bắt Folder con đó đi đếm các thành item nằm trong Folder con và trả về tổng số.

Nói đến đây các bạn sẽ nói, ô đây là “Rùa Em” (Đệ Quy). Nhưng với Composite nó sẽ có hiệu quả gì ? Nó sẽ giúp bạn làm việc với các thành phần một cách dể dàng, chỉ cần thông qua interface mà không phải xử lý rác rối sâu đến bên trong. Nhưng lời kêu gọi của bạn sẽ được xử lý chạy dần xuống theo cấu trúc cây.

Cấu Trúc

classDiagram
  class Component {
    +operation()
  }

  class Leaf {
    +operation()
  }

  class Composite {
    -children List
    +operation()
    +add(Component)
    +remove(Component)
    +getChild(int)
  }

  Component <|-- Leaf : Is a
  Component <|-- Composite : Is a
  Composite "1" *-- "many" Component : Contains

  class Component {
      <<interface>>
      +operation()
  }

Cách triển khai

Bước 1: Xây dựng Component

public abstract class Component {
    protected String name;
    
    public Component(String name) {
        this.name = name;
    }
    
    public abstract void add(Component component);
    public abstract void remove(Component component);
    public abstract void display(int depth);
}

Bước 2: Xây dựng Leaf

public class Leaf extends Component {
    public Leaf(String name) {
        super(name);
    }
    
    @Override
    public void add(Component component) {
        // Không thực hiện bất cứ điều gì
    }
    
    @Override
    public void remove(Component component) {
        // Không thực hiện bất cứ điều gì
    }
    
    @Override
    public void display(int depth) {
        System.out.println("-".repeat(depth) + name);
    }
}

Bước 3: Xây dựng Composite

import java.util.ArrayList;
import java.util.List;

public class Composite extends Component {
    private List<Component> children = new ArrayList<>();

    public Composite(String name) {
        super(name);
    }
    
    @Override
    public void add(Component component) {
        children.add(component);
    }
    
    @Override
    public void remove(Component component) {
        children.remove(component);
    }
    
    @Override
    public void display(int depth) {
        System.out.println("-".repeat(depth) + name);
        for (Component component : children) {
            component.display(depth + 2);
        }
    }
}

Bước 4: Sử dụng mẫu Composite

public class Client {
    public static void main(String[] args) {
        Composite root = new Composite("root");
        root.add(new Leaf("Leaf A"));
        root.add(new Leaf("Leaf B"));
        
        Composite comp = new Composite("Composite X");
        comp.add(new Leaf("Leaf XA"));
        comp.add(new Leaf("Leaf XB"));
        
        root.add(comp);
        
        root.display(1);
    }
}

Kết quả:

-root
--Leaf A
--Leaf B
--Composite X
---Leaf XA
---Leaf XB

Ví dụ áp dụng Composite Pattern

Chúng ta làm ví dụ ở trên nhưng thay đối một ít là ta sẽ xem tổng folder có size bao nhiêu

FileComponent.kt

interface FileComponent {
    fun showProperty()
    fun size() : Long
}

FileLeaf.kt

class FileLeaf : FileComponent {

    private var name:String
    private var size:Long

    constructor(name: String, size: Long) {
        this.name = name
        this.size = size
    }


    override fun showProperty() {
        System.out.println(this.name)
    }

    override fun size(): Long {
        return size;
    }
}

FolderComposite.kt

class FolderComposite : FileComponent{

    private var files: List<FileComponent>

    constructor(files: List<FileComponent>) {
        this.files = files
    }

    override fun showProperty() {
        for (file in this.files)
        {
            file.showProperty()
        }

    }

    override fun size(): Long {
        var total : Long = 0
        for (file in this.files) {
            total += file.size()
        }
        return total
    }

}

Kết quả

file 1
file 2
file 3
Total Size: 27

So sánh Composite Pattern

1. Composite vs Adapter:

2. Composite vs Decorator:

3. Composite vs Facade:

4. Composite vs Proxy:

Kết luận

Composite Pattern là một mẫu thiết kế hữu ích để xây dựng và quản lý cấu trúc phân cấp dưới dạng cây của các đối tượng. Nó cho phép chúng ta làm việc với một nhóm đối tượng như một đối tượng đơn lẻ, mang lại khả năng tổ chức và quản lý phân cấp một cách linh hoạt và thuận tiện.

Ưu điểm:

Nhược điểm:

Hướng dẫn sử dụng: