Menu
CoddyTech

组合模式

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 96 课,共 107 课。

组合模式让你可以用统一的方式处理单个对象和对象组。状态模式根据内部状态改变行为,而组合模式会构建树形结构,其中叶节点和容器共用同一个接口。

这种模式非常适合文件系统、组织结构图或 UI 组件等层次结构。在 Go 中,我们定义一个通用接口,由单个项目和容器共同实现:

type Component interface {
    GetSize() int
}

type File struct {
    Name string
    Size int
}

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

组合对象(容器)包含子对象,并通过汇总它们的结果来实现相同的接口:

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
}

现在,你可以在文件夹中嵌套文件夹,而且无论是单个文件还是整个目录树,调用 GetSize() 的效果都一样:

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

当你需要表示部分与整体的层次结构,并希望客户端以相同的方式处理单个对象和组合对象时,组合模式是理想的选择。

challenge icon

挑战

简单

让我们使用组合模式构建一个组织架构系统吧!你将创建一种结构,让员工个人和部门(其中包含员工或其他部门)都能以统一的方式处理,非常适合计算组织中任意部分的总薪资。

你需要将代码分布在三个文件中:

  • component.go:定义个人和群组都会实现的通用接口。

    创建一个 OrgComponent 接口,包含两个方法:

    • GetSalary() int:返回此组件的总薪资
    • GetName() string:返回员工或部门的名称
  • organization.go:实现叶子节点(员工个人)和组合节点(部门)类型。

    创建一个 Employee 结构体,包含 Name(字符串)和 Salary(整数)字段。它的 GetSalary() 返回员工自己的薪资,GetName() 返回员工的姓名。

    创建一个 Department 结构体,包含 Name(字符串)字段,以及一个用于存放 OrgComponent 项的 Members 切片。添加一个 Add(c OrgComponent) 方法来添加成员。它的 GetSalary() 应返回所有成员的薪资总和,GetName() 返回部门名称。

  • main.go:构建组织结构并计算薪资。

    读取员工人数。对每位员工,读取姓名和薪资,并创建 Employee 实例。然后读取部门数量。对每个部门,读取部门名称和成员索引数量,接着读取这些索引(从 0 开始,按创建顺序指向之前创建的员工或部门)。

    构建完结构后,读取最后一个索引,并按以下格式打印该组件的名称和总薪资:[name]: [salary]

输入内容如下:

  • 员工人数,然后是每位员工的姓名和薪资,每项各占一行
  • 部门数量,然后是每个部门的名称、成员数量和成员索引
  • 要查询的最后一个索引

例如,给定:

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

输出应为:

Engineering: 165000

再例如,给定:

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

输出应为:

QA: 125000

再例如,给定:

2
Alice
50000
Bob
60000
0
1

输出应为:

Bob: 60000

注意,无论是在单个员工还是整个部门上调用 GetSalary(),它的工作方式都完全相同:组合模式让你通过同一个接口统一处理个人和群组!

亲手试试

package main

import "fmt"

func main() {
	// 读取员工数量
	var numEmployees int
	fmt.Scanln(&numEmployees)

	// 按创建顺序将所有组件(员工和部门)存入 components
	var components []OrgComponent

	// TODO: 读取每位员工的姓名和工资
	// 创建 Employee 实例,并将其添加到 components 切片中
	for i := 0; i < numEmployees; i++ {
		var name string
		var salary int
		fmt.Scanln(&name)
		fmt.Scanln(&salary)
		// TODO: 创建员工并添加到 components
	}

	// 读取部门数量
	var numDepartments int
	fmt.Scanln(&numDepartments)

	// TODO: 读取每个部门的名称、成员数量和成员索引
	// 创建 Department 实例,按索引添加成员,再添加到 components
	for i := 0; i < numDepartments; i++ {
		var deptName string
		var memberCount int
		fmt.Scanln(&deptName)
		fmt.Scanln(&memberCount)
		// TODO: 创建部门,读取成员索引,添加成员,再添加到 components
	}

	// 读取最后要查询的索引
	var queryIndex int
	fmt.Scanln(&queryIndex)

	// TODO: 按以下格式输出组件的名称和工资总额:[name]: [salary]
	// 示例:fmt.Printf("%s: %d\n", name, salary)
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器