一、什么是组合模式
组合模式(Composite Pattern)是一种结构型模式(Structural Pattern),它主要解决的是如何将对象组合成树状以表示"部分-整体"的层次结构,并且可以对整个树进行统一的操作,如遍历、添加、删除等。组合模式的核心思想是将对象结构(树形结构)中的对象(包括叶子节点和容器节点)都看作同一类型的对象,从而使得客户端可以一致地处理单个对象和对象组合。组合模式主要由以下四个角色组成:
Component(组件接口):定义了组合中所有对象的通用接口,可以是抽象类或接口。它声明了用于访问和管理子组件的方法,包括添加、删除、获取子组件等。
Leaf(叶子节点):实现Component接口,代表树结构中的叶节点,没有子节点。叶子节点通常表示不可再分的对象。
Composite(容器节点):实现Component接口,包含子组件(Component对象),并提供管理子组件的方法(如添加、删除、遍历等)。容器节点可以包含多个子节点,这些子节点可以是叶子节点,也可以是其他容器节点。
Client(客户端):通过Component接口与对象结构进行交互,无需关心处理的是单个对象还是对象组合,可以一致地对待整体和部分。
二、组合模式的应用场景
组合模式在实际项目中应用非常广泛,主要适用于以下场景:
树形结构处理:如文件系统、XML/HTML文档结构、组织架构、菜单系统等。
递归算法实现:组合模式可以简化递归算法的实现,如遍历树形结构、计算树结构的某些属性等。
一致的处理方式:当需要对单个对象和对象组合进行相同操作时,使用组合模式可以使处理逻辑更加一致。
动态组合:当系统需要在运行时动态地添加或删除树形结构的节点时,组合模式可以提供更好的灵活性。
三、组合模式示例
以我们常见的人员组织树为例,一个公司内部会有多个部门,部门内部又可以进一步划分出其他部门,而人员则作为每个部门的叶子节点,所以,人员组织树很明显就很适合采用组合模式来实现。首先,我们定义一个抽象的组件类Entry,该类包含一个私有的属性name,并定义了抽象的方法添加、移除组件,另外还定义了一个方法用于在当前部门下查找某个用户的相对路径,其代码实现为:
java
public abstract class Entry {
private String name;
public abstract void add(Entry entry);
public abstract void remove(Entry entry);
/**
* 查找并返回某用户在某部门下的相对路径集合
*/
public abstract List<String> find(String currPath, String name);
public List<String> find(String name){
return find("", name);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后分别定义部门类Dept和人员类User,分别继承抽象类Entry,部门类内部定义子部门列表entries,并且部门类和人员类分别实现抽象类定义的方法:
部门类:
java
public class Dept extends Entry{
private final List<Entry> entries = new ArrayList<>();
public Dept(String name) {
this.setName(name);
}
@Override
public void add(Entry entry) {
entries.add(entry);
}
@Override
public void remove(Entry entry) {
entries.remove(entry);
}
@Override
public List<String> find(String currPath, String name) {
List<String> result = new ArrayList<>();
String nextPath = currPath + getName() + "/";
for (Entry entry : entries){
result.addAll(entry.find(nextPath, name));
}
return result;
}
}
人员类:
java
public class User extends Entry{
public User(String name) {
this.setName(name);
}
@Override
public void add(Entry entry) {
}
@Override
public void remove(Entry entry) {
}
@Override
public List<String> find(String currPath, String name) {
if(this.getName().contains(name)){
return Collections.singletonList(currPath + this.getName());
}else{
return new ArrayList<>();
}
}
}
通过如上代码,我们便以组合模式实现了一个简单人员部门树结构,接下来我们写一个客户端类进行测试:
java
public class Client {
public static void main(String[] args) {
Dept root = new Dept("root");
Dept dept1 = new Dept("dept1");
Dept dept2 = new Dept("dept2");
Dept dept3 = new Dept("dept3");
User user1 = new User("user1");
User user2 = new User("user2");
User user3 = new User("user3");
User user4 = new User("user4");
User user5 = new User("user5");
dept1.add(user1);
dept2.add(user2);
dept2.add(user4);
dept2.add(user5);
dept3.add(user3);
dept3.add(user4);
root.add(dept1);
root.add(dept2);
dept2.add(dept3);
System.out.println(root.find("user4"));
System.out.println(dept2.find("user4"));
System.out.println(dept3.find("user4"));
}
}
在以上Client类中,我们构造了一个简单的人员部门树结构:
通过find方法,分别在root、dept2、dept3下查找name为user4的用户,并打印其相对路径,执行结果为:
四、优化
通过上述示例,我们已经实现了一个简单的组合模式,但是可以发现,由于添加、删除这种用于管理部门结构的方法是定义在抽象的Entry类中的,所有的构件类都有相同的方法,这种实现方式称为透明组合模式 。而User类内部不可能再包含成员对象了,所以其add和remove方法实际上是没有意义的,在后续使用该组合模式的实现的时候,如果错误地使用了add和remove的方法的话,就可能产生一些预期外的错误,所以这种实现方式是不安全的。为了避免这种问题,我们可以将add和remove方法挪到Dept类中,这样就避免了错误地使用构件的方法产生的不安全的问题,而这种实现方式可以称为安全组合模 式。
而安全组合模式也不是一定强于透明组合模式的,二者各有优劣,在透明模式中,由于其安全性不足,在使用时可能会产生一些问题;而在安全模式中,由于其add和remove方法未在Entry类中定义,导致在开发时Dept和User类必须区别地对待,无法完全针对抽象编程,不够透明。简单来说,二者在安全和透明性上各有取舍,在使用组合模式时,要具体分析使用场景,选择合适的实现方式。
下面给出安全组合模式的实例代码:
Entry类:
java
public abstract class Entry {
private String name;
/**
* 查找并返回某用户在某部门下的相对路径集合
*/
public abstract List<String> find(String currPath, String name);
public List<String> find(String name){
return find("", name);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Dept类:
java
public class Dept extends Entry {
private final List<Entry> entries = new ArrayList<>();
public Dept(String name) {
this.setName(name);
}
public void add(Entry entry) {
entries.add(entry);
}
public void remove(Entry entry) {
entries.remove(entry);
}
@Override
public List<String> find(String currPath, String name) {
List<String> result = new ArrayList<>();
String nextPath = currPath + getName() + "/";
for (Entry entry : entries){
result.addAll(entry.find(nextPath, name));
}
return result;
}
}
User类:
java
public class User extends Entry {
public User(String name) {
this.setName(name);
}
@Override
public List<String> find(String currPath, String name) {
if(this.getName().contains(name)){
return Collections.singletonList(currPath + this.getName());
}else{
return new ArrayList<>();
}
}
}
Client类作为测试类,不需进行改动:
java
public class Client {
public static void main(String[] args) {
Dept root = new Dept("root");
Dept dept1 = new Dept("dept1");
Dept dept2 = new Dept("dept2");
Dept dept3 = new Dept("dept3");
User user1 = new User("user1");
User user2 = new User("user2");
User user3 = new User("user3");
User user4 = new User("user4");
User user5 = new User("user5");
dept1.add(user1);
dept2.add(user2);
dept2.add(user4);
dept2.add(user5);
dept3.add(user3);
dept3.add(user4);
root.add(dept1);
root.add(dept2);
dept2.add(dept3);
System.out.println(root.find("user4"));
System.out.println(dept2.find("user4"));
System.out.println(dept3.find("user4"));
}
}
安全组合模式的实现的测试结果和透明组合模式的实现的测试结果是一致的: