组合模式 Composite
组合模式(Composite):将对象组合成树形结构以表示'部分-整体'的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
The Composite Pattern allows clients to compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
组合模式定义看起来高大上的,但其实和代理模式、装饰者模式大差不差。下面看一下 UML 类图就知道了,
组合模式角色如下:
⨳ Component(组件): 定义了叶子和组合对象的公共接口,包括操作方法。它可以是抽象类或接口。
⨳ Leaf (叶子节点): 表示组合中的叶子节点对象,它实现了 Component
接口,但不包含子节点。
⨳ Composite (非叶子节点): 表示组合中的容器对象,它包含子节点,并实现了 Component
接口。Composite
可以包含 Leaf
和其他 Composite
,形成递归结构。
组合模式的 UML
类图是不是很像装饰者模式:
⨳ 叶子节点(Leaf
)和非叶子节点(Composite
)被同一个规范(Comopent
)限定;被装饰者(Concrete Component
)和装饰者(Decorator
)被同一个规范(Comopent
)限定。
⨳ 非叶子节点(Composite
)持有 Comopent
的引用,装饰者(Decorator
)也持有 Component
的引用。
不同的是,装饰者(Decorator
)持有 Comopent
的引用,非叶子节点(Composite
)持有 Comopent_s
的引用,这就意味着非叶子节点不能完成对树叶方法的增强(装饰),而仅仅是作为 一个存储 Comopent
的容器存在的。
被装饰者和装饰者都是 Comopent
,就可以让 Decorator
装饰 Decorator
,让 Decorator
装饰 其他装饰着 Decorator
的 Decorator
。。。你以为你看到的是一个装饰者,因为装饰者的嵌套,往里深看,发现里面连着一串的装饰者,看到头才发现那唯一一个被装饰者。
树叶和组合者都是 Comopent
,就可以让 Composite
组合 Composite
,让 Composite
组合 其他组合着 Composite
的 Composite
。。。你以为你看到的是一个组合者,因为组合者的嵌套,往里深看,发现里面已经展成了一棵参天大树,顺着其中一条枝干看下去,才会发现挂在上面的几片树叶。
下面看一下基本代码实现。
基本实现
组件 Component
Component
为组合中的对象声明接口,在适当情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理 Component
的子部件。
java
public abstract class Component {
protected String name;
public Component(String name){
this.name = name;
}
public abstract void add(Component component);
public abstract void remove(Component component);
public abstract void display(int depth);
}
叶子节点 Leaf
Leaf
在组合中表示叶节点对象,叶节点没有子节点。
java
public class Leaf extends Component {
public Leaf(String name){
super(name);
}
@Override
public void add(Component component) {
System.out.println("叶子节点不允许添加子节点");
}
@Override
public void remove(Component component) {
System.out.println("叶子节点没有子节点,无需删除");
}
@Override
public void display(int depth) {
StringBuilder sb = new StringBuilder();
for(int i=0;i<depth;i++){
sb.append("-");
}
sb.append(name);
System.out.println(sb.toString());
}
}
非叶子节点 Composite
Composite
定义有枝节点行为,用来存储子部件,在 Component
接口中实现与子部件有关的操作,比如增加和删除。
java
public class Composite extends Component {
private List<Component> children = new ArrayList<Component>();
public Composite(String name){
super(name);
}
@Override
public void add(Component component) {
children.add(component);
}
@Override
public void remove(Component component) {
children.remove(component);
}
@Override
public void display(int depth) {
StringBuilder sb = new StringBuilder();
for(int i=0;i<depth;i++){
sb.append("-");
}
sb.append(name);
System.out.println(sb.toString());
for(Component c: children){
c.display(depth+2);
}
}
}
你可能有这样的疑问:为什么 Leaf类 当中也有 add
和 remove
,树叶不是不可以再长分枝吗?
这种方式叫做透明方式,也就是说在 Component
中声明所有用来管理子对象的方法,其中包括 add
、remove
等。这样实现 Component
接口的所有子类都具备了 add
和 remove
。这样做的好处就是叶节点和枝节点对于外界没有区别,它们具备完全一致的行为接口。
客户端 Client
java
// 生成树根 root
Component root = new Composite("root");
// 树根上长出两片叶子
root.add(new Leaf("Leaf A"));
root.add(new Leaf("Leaf B"));
// 树根上又长出分支 Composite X
Component comp = new Composite("Composite X");
comp.add(new Leaf("Leaf XA"));
comp.add(new Leaf("Leaf XB"));
root.add(comp);
// 分支 Composite X 又长出分支
Component comp2 = new Composite("Composite XY");
comp2.add(new Leaf("Leaf XYA"));
comp2.add(new Leaf("Leaf XYB"));
comp.add(comp2);
// 显示树状结构
root.display(1);
输出结果如下:
js
-root
---Leaf A
---Leaf B
---Composite X
-----Leaf XA
-----Leaf XB
-----Composite XY
-------Leaf XYA
-------Leaf XYB
源码赏析
Mybatis 之 SqlNode
在 MyBatis 中,SQL 映射文件(Mapper XML 文件)中的 SQL 语句被表示为一个树形结构,而 SqlNode
就是这个树形结构中的节点。
SqlNode
接口定义了 apply
方法,用于将节点中表示的 SQL 片段应用到 SQL 语句中。而 SqlNode
的具体实现类可以是叶子节点,也可以是包含其他节点的容器节点,这样就形成了一个树形结构。
叶子节点(Leaf Nodes):
⨳ StaticTextSqlNode
:表示静态的 SQL 片段,如 SELECT * FROM table_name
中的 SELECT * FROM table_name
。
⨳ TextSqlNode
:表示动态生成的 SQL 片段,例如带有动态参数的 SQL 片段,如 WHERE id = #{id}
中的 id = #{id}
。
非叶子节点(Composite Nodes):
⨳ IfSqlNode
:表示带有条件判断的 SQL 片段,例如 <if test="condition">...</if>
中的 <if test="condition">...</if>
。
⨳ ChooseSqlNode
:表示带有条件选择的 SQL 片段,类似于 Java 中的 switch
语句。
⨳ ForEachSqlNode
:表示带有循环处理的 SQL 片段,用于处理集合或数组的循环操作,如 <foreach item="item" index="index" collection="list" open="(" separator="," close=")">...</foreach>
。
这些节点中,叶子节点通常表示单一的 SQL 片段,而非叶子节点则是包含其他 SqlNode
的容器,可以根据特定的条件或循环来组合和处理内部的 SQL 片段。通过这种组合方式,MyBatis 可以构建复杂的 SQL 查询语句,并且灵活地根据配置生成最终的 SQL 语句。
总结
当你需要表示对象的部分-整体层次结构时,例如树形菜单、文件系统等,组合模式就很有用了。它使得你可以统一处理叶子节点和容器节点。
优点如下:
⨳ 操作的统一性:当你希望对对象和对象集合实施相同的操作时,组合模式可以简化代码。例如,在图形编辑器中,你可以对单个图形对象或图形对象组进行相同的操作(例如移动、旋转)。
⨳ 简化客户端代码:客户端代码只需面对统一的接口,无需关心对象是单个对象还是对象组合。
⨳ 可扩展性:通过组合模式,可以轻松地添加新的组合对象和叶子对象,而不会影响现有代码。
缺点如下:
⨳ 叶子节点限制:叶子节点和组合节点的行为可能不同,如果在设计上没有考虑好这一点,可能会导致一些限制和困惑。
⨳ 不容易限制类型:组合模式使得所有的节点都具有相同的接口,这意味着不容易限制某些类型的对象能够作为容器节点的子节点。
总的来说,组合模式适用于具有层次结构的场景,并且能够提供一种统一的方式来处理单个对象和对象组合。