组合模式
本课属于 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当你需要表示部分与整体的层次结构,并希望客户端以相同的方式处理单个对象和组合对象时,组合模式是理想的选择。
挑战
简单让我们使用组合模式构建一个组织架构系统吧!你将创建一种结构,让员工个人和部门(其中包含员工或其他部门)都能以统一的方式处理,非常适合计算组织中任意部分的总薪资。
你需要将代码分布在三个文件中:
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)
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Go 在线编译器