组合模式详解
简介
组合模式(Composite)是针对由多个节点对象(部分)组成的树形结构的对象(整体)而发展出的一种结构型设计模式,它能够使客户端在操作整体对象或者其下的每个节点对象时做出统一的响应,保证树形结构对象使用方法的一致性,使客户端不必关注对象的整体或部分,最终达到对象复杂的层次结构与客户端解耦的目的。
组合模式的核心思想是将对象看作是一个树形结构,其中每个节点可以是一个单独的对象(叶子节点)或者一个包含其他节点的容器(组合节点)。叶子节点和组合节点都实现了相同的接口,这样客户端就可以对它们进行一致的操作,而不需要关心它们的具体类型。
组合模式有以下几个角色:
- Component(组件接口):所有复合节点与叶节点的高层抽象,定义出需要对组件操作的接口标准。对应本章例程中的抽象节点类,具体使用接口还是抽象类需根据具体场景而定。
- Composite(复合组件):包含多个子组件对象(可以是复合组件或叶端组件)的复合型组件,并实现组件接口中定义的操作方法。对应本章例程中作为“根节点/枝节点”的文件夹类。
- Leaf(叶端组件):不包含子组件的终端组件,同样实现组件接口中定义的操作方法。对应本章例程中作为“叶节点”的文件类
。 - Client(客户端):按所需的层级关系部署相关对象并操作组件接口所定义的接口,即可遍历树结构上的所有组件。
好处和坏处
组合模式的好处有:
- 可以将对象组合成树形结构,表示整体-部分的层次关系,符合人们的直觉。
- 可以统一处理单个对象和对象组合,简化了客户端的代码逻辑,提高了系统的可复用性。
- 可以遵循开闭原则,扩展性高,增加新的节点类型时不需要修改原有代码。
组合模式的坏处有:
- 可以使设计变得过于抽象,不利于理解和维护。
- 可以违反单一职责原则,让叶子节点和组合节点具有相同的接口,导致叶子节点出现不必要的方法。
- 可以导致递归调用过深,影响系统的性能。
应用场景
组合模式是一种将对象组合成树形结构的设计模式,它可以表示整体-部分的层次关系,并且提供了一致的接口来操作单个对象和对象组合。应用场景有:
- 当需要表示一个对象整体与部分的层次结构时,可以使用组合模式来实现树形结构。例如,文件系统中的文件与文件夹、组织机构中的部门与员工、商品分类中的类别与商品等。
- 当需要统一处理单个对象和对象组合时,可以使用组合模式来实现多态性。例如,图形界面中的简单控件与容器控件、菜单系统中的菜单项与子菜单、报表系统中的单元格与表格等。
- 当需要将对象的创建和使用分离时,可以使用组合模式来实现依赖注入。例如,Spring框架中的Bean对象与BeanFactory对象、测试框架中的测试用例与测试套件等。
Java 代码示例
假设我们有一个文件系统,其中有两种类型的文件:文本文件和文件夹。文本文件是叶子节点,文件夹是组合节点,可以包含其他文件。我们想要使用组合模式来实现文件系统的层次结构,并且提供一个打印文件路径的方法。代码如下:
定义抽象组件
public interface File {
// 获取文件名称
String getName();
// 添加子文件
void add(File file);
// 删除子文件
void remove(File file);
// 获取子文件
List<File> getChildren();
// 打印文件路径
void printPath(int space);
}
定义叶子节点
public class TextFile implements File {
private String name;
public TextFile(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void add(File file) {
throw new UnsupportedOperationException("Text file cannot add child file");
}
@Override
public void remove(File file) {
throw new UnsupportedOperationException("Text file cannot remove child file");
}
@Override
public List<File> getChildren() {
throw new UnsupportedOperationException("Text file has no child file");
}
@Override
public void printPath(int space) {
StringBuilder sp = new StringBuilder();
for (int i = 0; i < space; i++) {
sp.append(" ");
}
System.out.println(sp + name);
}
}
定义组合节点
public class Folder implements File {
private String name;
private List<File> children;
public Folder(String name) {
this.name = name;
children = new ArrayList<>();
}
@Override
public String getName() {
return name;
}
@Override
public void add(File file) {
children.add(file);
}
@Override
public void remove(File file) {
children.remove(file);
}
@Override
public List<File> getChildren() {
return children;
}
@Override
public void printPath(int space) {
StringBuilder sp = new StringBuilder();
for (int i = 0; i < space; i++) {
sp.append(" ");
}
System.out.println(sp + name);
space += 2;
for (File child : children) {
child.printPath(space);
}
}
}
客户端代码
public class Client {
public static void main(String[] args) {
// 创建一个根文件夹,并添加两个文本文件和一个子文件夹
File root = new Folder("root");
root.add(new TextFile("a.txt"));
root.add(new TextFile("b.txt"));
File subFolder = new Folder("subFolder");
root.add(subFolder);
// 在子文件夹中添加两个文本文件
subFolder.add(new TextFile("c.txt"));
subFolder.add(new TextFile("d.txt"));
// 打印根文件夹的路径
root.printPath(0);
}
}
输出结果:
root
a.txt
b.txt
subFolder
c.txt
d.txt
Go 代码示例
package main
// importing fmt package
import (
"fmt"
)
// IComposite interface
type IComposite interface {
perform()
}
// Leaflet struct
type Leaflet struct {
name string
}
// Leaflet class method perform
func (leaf *Leaflet) perform() {
fmt.Println("Leaflet " + leaf.name)
}
// Branch struct
type Branch struct {
leafs []Leaflet
name string
branches []Branch
}
// Branch class method perform
func (branch *Branch) perform() {
fmt.Println("Branch: " + branch.name)
for _, leaf := range branch.leafs {
leaf.perform()
}
for _, branch := range branch.branches {
branch.perform()
}
}
// Branch class method add leaflet
func (branch *Branch) add(leaf Leaflet) {
branch.leafs = append(branch.leafs, leaf)
}
//Branch class method addBranch branch
func (branch *Branch) addBranch(newBranch Branch) {
branch.branches = append(branch.branches, newBranch)
}
//Branch class method getLeaflets
func (branch *Branch) getLeaflets() []Leaflet {
return branch.leafs
}
// main method
func main() {
var branch = &Branch{name: "branch 1"}
var leaf1 = Leaflet{name: "leaf 1"}
var leaf2 = Leaflet{name: "leaf 2"}
var branch2 = Branch{name: "branch 2"}
branch.add(leaf1)
branch.add(leaf2)
branch.addBranch(branch2)
branch.perform()
}
输出结果:
G:\GoLang\examples>go run composite.go
Branch: branch 1
Leaflet leaf 1
Leaflet leaf 2
Branch: branch 2
Spring 代码示例
Spring 框架也可以使用组合模式来实现对象的层次结构,它提供了一个注解叫做 @Component
,它可以用来标注一个类是一个组件,即一个可被Spring管理的Bean对象。@Component
注解有一个属性叫做value,它可以用来指定组件的名称。我们可以使用 @Component
注解来标注我们的文件类,然后在配置文件或注解中声明这些组件,Spring 就会自动创建和管理这些组件对象。
假设我们有一个文件系统,其中有两种类型的文件:文本文件和文件夹。文本文件是叶子节点,文件夹是组合节点,可以包含其他文件。我们想要使用组合模式来实现文件系统的层次结构,并且提供一个打印文件路径的方法。我们可以使用 @Component
注解来实现,代码如下:
抽象组件不用改造
public interface File {
// 获取文件名称
String getName();
// 添加子文件
void add(File file);
// 删除子文件
void remove(File file);
// 获取子文件
List<File> getChildren();
// 打印文件路径
void printPath();
}
叶子节点添加 @Component("a.txt")
注解
@Component("a.txt")
public class TextFile implements File {
private String name;
public TextFile() {
this.name = "a.txt";
}
@Override
public String getName() {
return name;
}
@Override
public void add(File file) {
throw new UnsupportedOperationException("Text file cannot add child file");
}
@Override
public void remove(File file) {
throw new UnsupportedOperationException("Text file cannot remove child file");
}
@Override
public List<File> getChildren() {
throw new UnsupportedOperationException("Text file has no child file");
}
@Override
public void printPath() {
System.out.println(name);
}
}
组合节点添加 @Component("root")
注解
@Component("root")
public class Folder implements File {
private String name;
private List<File> children;
// 通过@Autowired注解自动注入所有类型为File的Bean对象到children集合中
@Autowired
public Folder(List<File> children) {
this.name = "root";
this.children = children;
}
@Override
public String getName() {
return name;
}
@Override
public void add(File file) {
children.add(file);
}
@Override
public void remove(File file) {
children.remove(file);
}
@Override
public List<File> getChildren() {
return children;
}
@Override
public void printPath() {
System.out.println(name);
for (File child : children) {
child.printPath();
}
}
}
SpringBoot 测试代码
@Slf4j
@SpringBootTest
class SpringBootTest {
@Autowired
private Folder folder;
@Test
void test() {
folder.printPath();
}
}
输出结果:
root
a.txt
总结
组合模式是一种常用的结构型设计模式,它可以将对象组合成树形结构,并且提供了一致的接口来操作单个对象和对象组合,是一种值得学习和掌握的设计模式。
关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、高效开发工具等,您的关注将是我的更新动力!