本文通过老王和小王探讨书房、书架、各类书的管理问题,引出结构型设计模式家族中的一个重要成员——组合模式,本文会给予两种组合模式的典型代码实现,为了加深理解会在第三部分应用中介绍组合模式在源码中的实际运用,最后总结该设计模式学习后的一些思考。
读者可以拉取完整代码到本地进行学习,实现代码均测试通过后上传到码云。
上篇文章中老王给小王买车以后,小王对老王感激涕零,看着老王凌乱的书房,小王提出要帮助老王整理整理他的书架。
小王开始了他的分析。老王平时博览群书,中文、英文、梵文…每个语种占满了书架,而每个语种中又分经济学、计算机学、社会学等等类目。这是典型的分层次结构,将语种比作是图书的子类,类目是语种的子类结构划分。
将图书、语种、类目都看做是组织结构,他们之间没有继承关系,而是一个树形结构,可以更好的实现管理操作。
实际上,小王提出来的设计思路正是结构型设计模式中的组合模式,我们首先看一下组合模式的相关概念,组合模式(Composite Pattern),又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以表示“整体-部分”的层次关系。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
组合模式使得用户对单个对象和组合对象的访问具有一致性,即:组合能让客户以一致的方式处理个别对象以及组合对象。
用大白话解释也就是,在实际应用中将所有图书依照树形模式进行组合,老王寻找书籍时,无论是访问某一类书还是某一个语种的书,使用同样的姿势即可,保证了访问的一致性。
在该模式中应该是有三个角色:
1、Root :这是组合中对象声明接口,在适当情况下,实现所有类共有的接口默认行为,用于访问和管理Root 子部件, Root 可以是抽象类或者接口。
2、Branches:非叶子节点用于存储子部件,在Root接口中实现了 子部件的相关操作。
2、Leaf : 在组合中表示叶子节点,叶子节点没有子节点。
小王分析的头头是道,老王提出来了他的疑问。
当我按语种查找还是按类目查找是使用的方法有时候是不一样的,如果你把所有方法都定义在Root中,在语种或者类目中实现中是无意义的,而且这违背了接口隔离原则。
小王觉得说的对也不对,如果我改成不在Root中定义,那么我在客户端调用的时候就需要判断是枝还是叶了,增加了繁杂的逻辑判断,而且相比另外一种变得不透明了,依赖倒置原则也没有遵守。
两种方式似乎都有缺陷,小王陷入了纠结不知道该如何取舍,老王提出了他的一些见解,没有任何一个设计模式是完全没有缺点的,两种都有各自的好处,在实际的运用中根据条件进行取舍,而正确选择的前提就是要对所有的设计模式充分的把握。
上面两种就对应组合模式中的两个大分类、①透明组合模式、安全组合模式。
①透明组合模式把所有的公共方法都定义在Root中,这样做的好处就是客户端无需分辨是叶子节点(Leaf)和树枝节点(Branches),他们具备完全一致的接口;缺点是叶子节点(Leaf)会继承得到一些它所不需要(管理子类操作的方法)的方法,这与设计模式接口隔离原则相违背。
②安全组合模式的好处是接口定义职责清晰,符合设计模式单一职责原则和接口隔离原则;缺点是客户需要区分树枝节点(Branches)和叶子节点(Leaf),这样才能正确处理各个层次的操作,客户端依赖抽象(Root),违背了依赖倒置原则。
我们把两种的方式实现,读者对比他们之间的区别。
安全模式
Root(根节点):
/**
* @author tcy
* @Date 08-08-2022
*/
public abstract class RootBook {
protected String name;
public RootBook(String name) {
this.name = name;
}
public abstract String operation();
}
Branches(树枝节点)
/**
* @author tcy
* @Date 08-08-2022
*/
public class BranchesLanguages extends RootBook {
private List roots;
public BranchesLanguages(String name) {
super(name);
this.roots = new ArrayList();
}
public String operation() {
StringBuilder builder = new StringBuilder(this.name);
for (RootBook component : this.roots) {
builder.append("\n");
builder.append(component.operation());
}
return builder.toString();
}
public boolean addChild(RootBook component) {
return this.roots.add(component);
}
public boolean removeChild(RootBook component) {
return this.roots.remove(component);
}
public RootBook getChild(int index) {
return this.roots.get(index);
}
}
Leaf(叶子节点)
/**
* @author tcy
* @Date 08-08-2022
*/
public class LeafClassify extends RootBook {
public LeafClassify(String name) {
super(name);
}
@Override
public String operation() {
return this.name;
}
}
客户端:
/**
* @author tcy
* @Date 08-08-2022
*/
public class Client {
public static void main(String[] args) {
System.out.println("安全组合模式...");
// 来一个根节点
BranchesLanguages BranchesRoot = new BranchesLanguages("root/书");
// 来一个树枝节点
BranchesLanguages branchA = new BranchesLanguages("------branchA/英语");
BranchesLanguages branchB = new BranchesLanguages("------branchB/中文");
// 来一个叶子节点
RootBook leafA = new LeafClassify("------leafA/经济学");
RootBook leafB = new LeafClassify("------leafB/计算机学");
RootBook leafC = new LeafClassify("------leafC/法学");
BranchesRoot.addChild(branchA);
BranchesRoot.addChild(leafC);
branchA.addChild(leafA);
branchA.addChild(branchB);
branchB.addChild(leafB);
String result = BranchesRoot.operation();
System.out.println(result);
}
}
透明模式
Root(根节点):
/**
* @author tcy
* @Date 08-08-2022
*/
public abstract class RootBook {
protected String name;
public RootBook(String name) {
this.name = name;
}
public abstract String operation();
public boolean addChild(RootBook component) {
throw new UnsupportedOperationException("addChild not supported!");
}
public boolean removeChild(RootBook component) {
throw new UnsupportedOperationException("removeChild not supported!");
}
public RootBook getChild(int index) {
throw new UnsupportedOperationException("getChild not supported!");
}
}
Branches(树枝节点)
/**
* @author tcy
* @Date 08-08-2022
*/
public class BranchesLanguages extends RootBook {
private List roots;
public BranchesLanguages(String name) {
super(name);
this.roots = new ArrayList();
}
public String operation() {
StringBuilder builder = new StringBuilder(this.name);
for (RootBook component : this.roots) {
builder.append("\n");
builder.append(component.operation());
}
return builder.toString();
}
@Override
public boolean addChild(RootBook component) {
return this.roots.add(component);
}
@Override
public boolean removeChild(RootBook component) {
return this.roots.remove(component);
}
@Override
public RootBook getChild(int index) {
return this.roots.get(index);
}
}
Leaf(叶子节点)
/**
* @author tcy
* @Date 08-08-2022
*/
public class LeafClassify extends RootBook {
public LeafClassify(String name) {
super(name);
}
@Override
public String operation() {
return this.name;
}
}
客户端:
/**
* @author tcy
* @Date 08-08-2022
*/
public class Client {
public static void main(String[] args) {
System.out.println("透明组合模式...");
// 来一个根节点
RootBook BranchesRoot = new BranchesLanguages("root/书");
// 来一个树枝节点
RootBook branchA = new BranchesLanguages("------branchA/英语");
RootBook branchB = new BranchesLanguages("------branchB/汉语");
// 来一个叶子节点
RootBook leafA = new LeafClassify("------leafA/计算机学");
RootBook leafB = new LeafClassify("------leafB/法学");
RootBook leafC = new LeafClassify("------leafC/社会学");
BranchesRoot.addChild(branchA);
BranchesRoot.addChild(leafC);
branchA.addChild(leafA);
branchA.addChild(branchB);
branchB.addChild(leafB);
String result = BranchesRoot.operation();
System.out.println(result);
}
}
使用组合模式的两种实现方法,这样就对老王的书架改造工程就完成了,对凭空捏造出来的需求有些读者看完想必还是云里雾里。我们结合JDK的源码和一些开发常用框架,再次深入源码对组合模式的使用。
通过查询资料可知,组合模式在Jdk中的应用主要是集合类HashMap和Mybtis中的SqlNode。
我们分别看其实现。
在HashMap中有一个父类AbstractMap和一个子类Node。如下图
我们看下源代码:
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
...
public void putAll(Map extends K, ? extends V> m) {
putMapEntries(m, true);
}
...
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
...
}
putAll()方法传入的是Map对象,Map就是一个抽象构件(同时这个构件中只支持健值对的存储格式),而HashMap是一个中间构件,HashMap中的Node节点就是叶子节点。
Node是HashMap中的一个内部类,HashMap的存储节点指的正是Node,读者可以重点看这个类的实现。
在这个实例中,HashMap就是树枝节点,Node就是叶节点,Map就是根节点。
SqlNode是一个接口,主要功能就是构造SQL语句。
public interface SqlNode {
boolean apply(DynamicContext context);
}
SqlNode有一大堆的实现类,我们看其中的MixedSqlNode。
public class MixedSqlNode implements SqlNode {
private final List contents;
public MixedSqlNode(List contents) {
this.contents = contents;
}
@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
}
SqlNode就充当组合模式中的Root,而他的众多子类作用就在于拼接各种类型的SQL,在组合模式的角色中相当于树枝节点。其中在TrimSqlNode 中有一个子类WhereSqlNode就充当组合模式中的树叶节点。
这两个都属于组合模式中的典型例子,读者体会下使用这种模式的好处,和如果不使用组合模式应该怎样实现。
通过这两个例子我们应该可以看到,设计模式的使用中并不是完全遵循各自的角色,更多的是设计模式中的一些变种,读者不深入源码并不能了解到该模式的实现细节。读者需要做的就是尽可能的熟悉设计模式,在自己开发过程中可以“择优录取”。
到这里组合模式也就介绍完了,这种模式的优缺点都非常的明显,优点就在于清楚的定义分层次的结构,在调用时忽略他们之间的差异,方便对整个层次进行控制,但是组合模式会违反依赖倒置原则。
理解是一回事,在实际应用中能正确的使用它就是另外一回事了。
读者要对每种设计模式都能做到心中有数,当我们在实际编程中,在潜意识里有各个设计模式的大体轮廓,参考代入进各种设计模式中,对于简化开发和易于维护性有没有好的帮助,选择一个最优的设计模式。
推荐读者,参考软件设计七大原则 认真阅读往期的文章,认真体会。
创建型设计模式:
一、设计模式之工厂方法和抽象工厂
二、设计模式之单例和原型
三、设计模式之建造者模式
结构型设计模式:
四、设计模式之代理模式
五、设计模式之适配器模式
六、桥接模式