「补课」进行时:设计模式(14)——组合模式
1. 前文汇总
2. 某东的菜单
前段时间双十一,不知道各位的战果如何,反正我是屯了两盒口罩凑个数。
电商平台为我们提供的方便快捷的搜索框入口,我想大多数人在使用的时候应该都会使用这个入口,但其实电商平台还为我们提供了另一个入口,就是它的分类体系,如下:
我简单抽象一下:
- 服装
- 男装
- 衬衣
- 夹克
- 女装
- 裙子
- 套装
可以看到,这是一个树结构,在前端实现一个这种菜单树可以选用 ZTree 插件,做过前端的都知道。
下面,用 Java 代码实现一下,输出一下上面的这个树状结构:
观察这个树状结构,可以看到节点分为三种类型:
根节点 树枝节点 叶子节点(没有子节点)
根节点和树枝节点的构造是比较类似的,都是可以有子节点,这两个节点可以抽象成一个对象。
首先定义一个叶子节点:
public class Leaf {
// 叶子对象的名字
private String name;
// 构造方法
public Leaf(String name) {
this.name = name;
}
// 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
public void printStruct(String preStr) {
System.out.println(preStr + " - " + name);
}
}
接着定义一个组合对象:
public class Composite {
// 组合对象集合
private Collection childComposite = new ArrayList<>();
// 叶子对象集合
private Collection childLeaf = new ArrayList<>();
// 组合对象名称
private String name;
// 构造函数
public Composite(String name) {
this.name = name;
}
// 向组合对象加入被它包含的其它组合对象
public void addComposite(Composite c){
this.childComposite.add(c);
}
// 向组合对象加入被它包含的叶子对象
public void addLeaf(Leaf leaf){
this.childLeaf.add(leaf);
}
// 输出自身结构
public void printStruct(String preStr){
System.out.println(preStr + " + " + this.name);
preStr+=" ";
for(Leaf leaf : childLeaf){
leaf.printStruct(preStr);
}
for(Composite c : childComposite){
c.printStruct(preStr);
}
}
}
来一个测试类:
public class Test {
public static void main(String[] args) {
//定义所有的组合对象
Composite root = new Composite("服装");
Composite c1 = new Composite("男装");
Composite c2 = new Composite("女装");
//定义所有的叶子对象
Leaf leaf1 = new Leaf("衬衣");
Leaf leaf2 = new Leaf("夹克");
Leaf leaf3 = new Leaf("裙子");
Leaf leaf4 = new Leaf("套装");
//按照树的结构来组合组合对象和叶子对象
root.addComposite(c1);
root.addComposite(c2);
c1.addLeaf(leaf1);
c1.addLeaf(leaf2);
c2.addLeaf(leaf3);
c2.addLeaf(leaf4);
//调用根对象的输出功能来输出整棵树
root.printStruct("");
}
}
上面的这种实现方案,虽然能实现了我们希望看到的功能,但是有一个很明显的问题:那就是必须区分组合对象和叶子对象,并进行有区别的对待。
3. 组合模式
3.1 定义
组合模式(Composite Pattern)也叫合成模式,有时又叫做部分-整体模式(Part-Whole),主要是用来描述部分与整体的关系,其定义如下:
Compose objects into tree structures to represent part-wholehierarchies.Composite lets clients treat individual objects and compositionsof objects uniformly.(将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。)
3.2 通用类图
Component 抽象构件角色: 定义参加组合对象的共有方法和属性,可以定义一些默认的行为或属性。 Leaf 叶子构件: 叶子对象,其下再也没有其他的分支,也就是遍历的最小单位。 Composite 树枝构件: 树枝对象,它的作用是组合树枝节点和叶子节点形成一个树形结构。
3.3 通用代码
public abstract class Component {
// 整体和个体都共享的逻辑
void doSomething() {
// 具体的业务逻辑
}
}
public class Leaf extends Component {
// 可以复写父类的方法
@Override
void doSomething() {
super.doSomething();
}
}
public class Composite extends Component {
// 构建容器
private ArrayList componentArrayList = new ArrayList<>();
// 增加一个叶子构件或者树枝构件
public void add(Component component) {
this.componentArrayList.add(component);
}
// 删除一个叶子构件或者树枝构件
public void remove(Component component) {
this.componentArrayList.remove(component);
}
// 获得分支下所有叶子构件或者树枝构件
public ArrayList getChildren() {
return this.componentArrayList;
}
}
3.4 优点
高层模块调用简单:
一棵树形机构中的所有节点都是 Component ,局部和整体对调用者来说没有任何区别,也就是说,高层模块不必关心自己处理的是单个对象还是整个组合结构,简化了高层模块的代码。
节点自由增加:
使用了组合模式后,我们可以看看,如果想增加一个树枝节点、树叶节点是不是都很容易,只要找到它的父节点就成,非常容易扩展,符合开闭原则,对以后的维护非常有利。
4. 示例改进(安全模式)
把最上面的示例修改成组合模式,首先需要定义一个抽象组件:
public abstract class Component {
// 输出组件名称
abstract void printStruct(String preStr);
}
叶子节点:
public class Leaf extends Component {
private String name;
public Leaf(String name) {
this.name = name;
}
@Override
void printStruct(String preStr) {
System.out.println(preStr + " - " + name);
}
}
树枝节点:
public class Composite extends Component{
// 组合对象集合
private Collection childComponents;
// 组合对象的名字
private String name;
public Composite(String name) {
this.name = name;
}
public void addChild(Component child) {
if (this.childComponents == null) {
this.childComponents = new ArrayList<>();
}
this.childComponents.add(child);
}
void removeChild(Component child) {
this.childComponents.remove(child);
}
Collection getChildren() {
return this.childComponents;
}
@Override
void printStruct(String preStr) {
System.out.println(preStr + " + " + this.name);
if (this.childComponents != null) {
preStr+=" ";
for(Component c : this.childComponents){
//递归输出每个子对象
c.printStruct(preStr);
}
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
// 定义根节点
Composite root = new Composite("服装");
// 创建两个树枝节点
Composite c1 = new Composite("男装");
Composite c2 = new Composite("女装");
// 定义所有的叶子对象
Leaf leaf1 = new Leaf("衬衣");
Leaf leaf2 = new Leaf("夹克");
Leaf leaf3 = new Leaf("裙子");
Leaf leaf4 = new Leaf("套装");
// 按照树的结构来组合组合对象和叶子对象
root.addChild(c1);
root.addChild(c2);
c1.addChild(leaf1);
c1.addChild(leaf2);
c2.addChild(leaf3);
c2.addChild(leaf4);
// 调用根对象的输出功能来输出整棵树
root.printStruct("");
}
}
5. 透明模式
组合模式有两种不同的实现,安全模式和透明模式,上面的示例是安全模式,下面是透明模式的通用类图:
和上面的那个安全模式的通用类图对比一下区别,就非常明显了,只是单纯的把几个方法 addChild()
、 removeChild()
、 getChildren()
几个方法放到了抽象类中。
在透明模式中不管叶子对象还是树枝对象都有相同的结构,通过判断是否存在子节点来判断叶子节点还是树枝节点,如果处理不当,这个会在运行期出现问题。
而在安全模式中,它是把树枝节点和树叶节点彻底分开,树枝节点单独拥有用来组合的方法,这种方案比较安全。
抽象角色:
public abstract class Component {
// 输出组件名称
abstract void printStruct(String preStr);
// 向组合对象中加入组件对象
abstract void addChild(Component child);
// 从组合对象中移出某个组件对象
abstract void removeChild(Component child);
// 返回组件对象
abstract Collection getChildren();
}
叶子节点:
public class Leaf extends Component {
private String name;
public Leaf(String name) {
this.name = name;
}
// 向组合对象中加入组件对象
@Deprecated
public void addChild(Component child) {
// 缺省实现,如果子类未实现此功能,由父类抛出异常
throw new UnsupportedOperationException("对象不支持这个功能");
}
// 从组合对象中移出某个组件对象
@Deprecated
public void removeChild(Component child){
// 缺省实现,如果子类未实现此功能,由父类抛出异常
throw new UnsupportedOperationException("对象不支持这个功能");
}
@Deprecated
Collection getChildren() {
throw new UnsupportedOperationException("对象不支持这个功能");
}
@Override
void printStruct(String preStr) {
System.out.println(preStr + " - " + name);
}
}
这里使用 Deprecated 注解是为了在编译期告诉调用者,这方法我有,可以调用,但是已经失效了,如果一定要调用,那么在运行期会抛出 UnsupportedOperationException
的错误。
树枝节点:
public class Composite extends Component {
// 组合对象集合
private Collection childComponents;
// 组合对象的名字
private String name;
public Composite(String name) {
this.name = name;
}
public void addChild(Component child) {
if (this.childComponents == null) {
this.childComponents = new ArrayList<>();
}
this.childComponents.add(child);
}
@Override
void removeChild(Component child) {
this.childComponents.remove(child);
}
@Override
Collection getChildren() {
return this.childComponents;
}
@Override
void printStruct(String preStr) {
System.out.println(preStr + " + " + this.name);
if (this.childComponents != null) {
preStr+=" ";
for(Component c : this.childComponents){
//递归输出每个子对象
c.printStruct(preStr);
}
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
//定义所有的组合对象
Component root = new Composite("服装");
Component c1 = new Composite("男装");
Component c2 = new Composite("女装");
//定义所有的叶子对象
Component leaf1 = new Leaf("衬衣");
Component leaf2 = new Leaf("夹克");
Component leaf3 = new Leaf("裙子");
Component leaf4 = new Leaf("套装");
//按照树的结构来组合组合对象和叶子对象
root.addChild(c1);
root.addChild(c2);
c1.addChild(leaf1);
c1.addChild(leaf2);
c2.addChild(leaf3);
c2.addChild(leaf4);
//调用根对象的输出功能来输出整棵树
root.printStruct("");
}
}
6. 参考
https://www.jianshu.com/p/dead42334033