Menu
CoddyTech

Mẫu Composite

Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 96 trên 107.

Mẫu Composite cho phép bạn xử lý các đối tượng riêng lẻ và nhóm đối tượng theo cùng một cách. Trong khi State thay đổi hành vi dựa trên trạng thái bên trong, Composite xây dựng cấu trúc cây, trong đó cả nút lá và nút chứa đều dùng chung một giao diện.

Mẫu này rất phù hợp với các cấu trúc phân cấp như hệ thống tệp, sơ đồ tổ chức hoặc thành phần giao diện người dùng. Trong Go, chúng ta định nghĩa một giao diện chung mà cả các mục riêng lẻ lẫn vùng chứa đều triển khai:

type Component interface {
    GetSize() int
}

type File struct {
    Name string
    Size int
}

func (f File) GetSize() int {
    return f.Size
}

Đối tượng tổng hợp (vùng chứa) chứa các đối tượng con và triển khai cùng một giao diện bằng cách tổng hợp kết quả của chúng:

type Folder struct {
    Name     string
    Children []Component
}

func (f *Folder) Add(c Component) {
    f.Children = append(f.Children, c)
}

func (f Folder) GetSize() int {
    total := 0
    for _, child := range f.Children {
        total += child.GetSize()
    }
    return total
}

Giờ bạn có thể lồng các thư mục bên trong thư mục khác, và gọi GetSize() sẽ hoạt động giống hệt nhau dù đó là một tệp đơn lẻ hay cả một cây thư mục:

docs := &Folder{Name: "docs"}
docs.Add(File{Name: "readme.txt", Size: 100})
docs.Add(File{Name: "notes.txt", Size: 50})

root := &Folder{Name: "root"}
root.Add(docs)
root.Add(File{Name: "config.json", Size: 25})

fmt.Println(root.GetSize())  // 175

Composite phù hợp khi bạn cần biểu diễn cấu trúc phân cấp giữa tổng thể và bộ phận, đồng thời muốn phía sử dụng xử lý các đối tượng riêng lẻ và các tổ hợp theo cùng một cách.

challenge icon

Thử thách

Dễ

Hãy xây dựng hệ thống sơ đồ tổ chức bằng mẫu Composite! Bạn sẽ tạo một cấu trúc trong đó cả nhân viên riêng lẻ lẫn phòng ban, gồm nhân viên hoặc phòng ban khác, đều có thể được xử lý thống nhất. Cách này rất phù hợp để tính tổng lương ở bất kỳ bộ phận nào trong tổ chức.

Bạn sẽ sắp xếp code trong ba tệp:

  • component.go: Định nghĩa giao diện chung mà cả cá nhân lẫn nhóm đều sẽ triển khai.

    Tạo giao diện OrgComponent với hai phương thức:

    • GetSalary() int: trả về tổng lương của thành phần này
    • GetName() string: trả về tên nhân viên hoặc phòng ban
  • organization.go: Triển khai cả kiểu nút lá (nhân viên riêng lẻ) và kiểu tổ hợp (phòng ban).

    Tạo cấu trúc Employee với các trường Name (string) và Salary (int). GetSalary() trả về mức lương của chính nhân viên đó, còn GetName() trả về tên của họ.

    Tạo cấu trúc Department với trường Name (string) và một lát cắt Members chứa các phần tử OrgComponent. Thêm phương thức Add(c OrgComponent) để thêm thành viên. GetSalary() sẽ trả về tổng lương của tất cả thành viên, còn GetName() trả về tên phòng ban.

  • main.go: Xây dựng cấu trúc tổ chức và tính lương.

    Đọc số lượng nhân viên. Với mỗi nhân viên, đọc tên và mức lương của họ để tạo các instance Employee. Sau đó, đọc số lượng phòng ban. Với mỗi phòng ban, đọc tên và số lượng chỉ số thành viên, rồi đọc các chỉ số đó (bắt đầu từ 0, tham chiếu đến nhân viên hoặc phòng ban đã tạo trước đó theo thứ tự tạo).

    Sau khi xây dựng cấu trúc, đọc chỉ số cuối cùng và in tên cùng tổng lương của thành phần đó theo định dạng: [name]: [salary]

Đầu vào sẽ gồm:

  • Số lượng nhân viên, sau đó là tên và mức lương của từng nhân viên trên các dòng riêng
  • Số lượng phòng ban, sau đó là tên, số lượng thành viên và các chỉ số thành viên của từng phòng ban
  • Chỉ số cuối cùng cần truy vấn

Ví dụ, với đầu vào:

3
Alice
50000
Bob
60000
Carol
55000
1
Engineering
3
0
1
2
3

Đầu ra cần là:

Engineering: 165000

Với đầu vào:

4
Alice
50000
Bob
60000
Carol
55000
Dave
70000
2
Dev
2
0
1
QA
2
2
3
4

Đầu ra cần là:

QA: 125000

Với đầu vào:

2
Alice
50000
Bob
60000
0
1

Đầu ra cần là:

Bob: 60000

Lưu ý rằng GetSalary() hoạt động giống hệt nhau dù bạn gọi phương thức này trên một nhân viên riêng lẻ hay cả phòng ban: mẫu Composite cho phép bạn xử lý cá nhân và nhóm thống nhất thông qua cùng một giao diện!

Tự làm thử

package main

import "fmt"

func main() {
	// Đọc số lượng nhân viên
	var numEmployees int
	fmt.Scanln(&numEmployees)

	// Lưu tất cả thành phần (nhân viên và phòng ban) theo thứ tự tạo
	var components []OrgComponent

	// TODO: Đọc tên và lương của từng nhân viên
	// Tạo các instance Employee và thêm vào slice components
	for i := 0; i < numEmployees; i++ {
		var name string
		var salary int
		fmt.Scanln(&name)
		fmt.Scanln(&salary)
		// TODO: Tạo nhân viên và thêm vào components
	}

	// Đọc số lượng phòng ban
	var numDepartments int
	fmt.Scanln(&numDepartments)

	// TODO: Đọc tên, số lượng thành viên và chỉ số thành viên của từng phòng ban
	// Tạo các instance Department, thêm thành viên theo chỉ số rồi thêm vào components
	for i := 0; i < numDepartments; i++ {
		var deptName string
		var memberCount int
		fmt.Scanln(&deptName)
		fmt.Scanln(&memberCount)
		// TODO: Tạo phòng ban, đọc chỉ số thành viên, thêm thành viên rồi thêm vào components
	}

	// Đọc chỉ số cuối cùng cần truy vấn
	var queryIndex int
	fmt.Scanln(&queryIndex)

	// TODO: In ra tên và tổng lương của thành phần theo định dạng: [name]: [salary]
	// Ví dụ: fmt.Printf("%s: %d\n", name, salary)
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch Go online