尺有所短,寸有所长;不忘初心,方得始终。
一、组合模式是什么
组合模式:又叫作整体-部分(Part-Whole)模式,它将对象组合成树状的层次结构,用来表示整体-部分的关系,使用户对单个对象和组合对象具有一致的访问性,属于结构型模式。
树状结构如下:

由上图可以看出,
-
根节点和树枝节点本质上属于同一种数据类型,可以作为容器使用。 -
叶子节点与树枝节点在语义上不属于用一种类型。在组合模式中,会把树枝节点和叶子节点看作属于同一种数据类型(用统一接口定义),让它们具备一致行为。
组合模式在树型结构中,模糊了简单元素(叶子节点)和复杂元素(树枝节点)的概念,客户程序可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
二、组合模式的适用场景
适用场景
-
需要体现部分与整体的树状层次结构时,可以使用组合模式。
-
希望客户端忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
现实案例:
-
文件夹
文件夹可以有子文件夹和文件。
-
超市购物袋
大袋子装商品和小袋子,小袋子可以装小袋子和商品
-
算术表达式
包括操作数、操作符和另一个操作数,另一个操作数也可以是操作数、操作符和另一个操作数
三、组合模式结构
3.1 组合模式主要角色
-
抽象构件(Component)角色
-
主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。 -
透明式的组合模式中抽象构件还声明访问和管理子类的接口 -
安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成,只定义一些通用的方法。
-
树叶构件(Leaf)角色
在组合中表示叶节点,叶节点没有子节点,用于继承或实现抽象构件基本行为。
-
树枝构件(Composite)角色 / 中间构件
是组合中的分支节点对象,有子节点,用于继承和实现抽象构件基本行为,它的主要作用是存储存储子部件并在Component接口实现与子部件有关的操作。
-
客户端(Client)
通过Component接口操作组合部件的对象。
3.1 组合模式的两种类型
组合模式分为透明式的组合模式和安全式的组合模式。这两种类型的主要区别在于抽象构件(Component)角色上的差别。
-
透明式的组合模式
在透明式的组合模式中,由于抽象构件声明了所有子类中的全部方法,所以客户端无须区别树叶对象和树枝对象,对客户端来说是透明的。
-
透明式缺点
树叶构件本身没有子节点,但是由于继承抽象构件,需要实现树枝构件所特有的行为(比如文件夹的新增子文件夹的方法),此时只能空实现或抛异常。其结构图如下

-
安全式的组合模式
在安全式的组合模式中,将管理子构件的方法移到树枝构件中,抽象构件只定义树枝构件和树叶构件所共同的方法。避免了透明式的组合模式的空实现或抛异常问题。
-
安全式的缺点
由于叶子节点和树枝节点有不同的行为方法,客户端在调用时要知道树叶对象和树枝对象的存在,所以对对于客户端失去了透明性。其结构图如下

四、组合模式实现方式
组合模式实现的前提:确保应用的核心模型能够以树状结构表示,并将其分解为简单元素和容器,容器必须能够同时包含简单元素和子容器
-
定义一个接口或抽象类作为抽象构件角色,声明组件接口及其一系列方法。
-
定义一个叶节点类表示简单元素。实现抽象构件角色, 程序中可以有多个不同的叶节点类。
叶节点可以作为一个接口,以不同的叶子结点类去实现接口
-
定义一个树枝节点(容器)类表示复杂元素,实现抽象构件角色。
-
在该类中创建一个数组成员变量来存储对于其子元素的引用。 该数组必须能够同时保存叶节点和容器, 因此必须将其声明为组合接口类型
-
树枝节点也可以作为一个接口,以不同的树枝节点类去实现接口
五、组合模式的两种实现
【案例】用组合模式实现在超市购物后,显示并计算所选商品信息与总价。
【案例说明】张三在超市购物,购物清单如下
1号小袋子装了2 包芒果干(单价15.8元),1包薯片(单价9.8元) 2号小袋子装了3 包山楂(单价7.8元),2包牛肉脯(单价19.8元) 中型袋子装了1号小袋子,1盒巧克力(单价39.8元) 大型袋子装了中型袋子,2号小袋子,1箱牛奶(单价79.8元) 【大袋子的东西】
{
1箱牛奶(单价79.8元)
2号小袋子{
3 包山楂(单价7.8元
2包牛肉脯(单价19.8元)
}
中型袋子:{
1盒巧克力(单价39.8元)
1号小袋子:{
2 包芒果干(单价15.8元)
1包薯片(单价9.8元)
}
}
}
案例结构图如下

5.1 透明式的组合模式
透明式的组合模式中抽象构件还声明访问和管理子类的接口
-
抽象构件(Component)角色
/**
* 抽象构件(Component)角色
*/
public interface Article {
/**
* 树枝构件特有的方法: 访问和管理子类的接口 大袋子装小袋子
*/
public void add(Article article);
/**
* 计算价格
*/
public Double calculation();
/**
* 显示商品
*/
public void show();
} -
树叶构件(Leaf)角色
/**
* 树叶构件: 商品
*/
public class Goods implements Article {
/**
* 商品名称
*/
private String name;
/**
* 购买数量
*/
private Integer quantity;
/**
* 商品单价
*/
private Double unitPrice;
public Goods(String name, Integer quantity, Double unitPrice) {
this.name = name;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
/**
* 树枝构件特有的方法
* 在树叶构件中是能空实现或者抛异常
*/
@Override
public void add(Article article) {
}
@Override
public Double calculation() {
return this.unitPrice * this.quantity;
}
@Override
public void show() {
System.out.println(name + ": (数量:" + quantity + ",单价:" + unitPrice + "元)," +
"合计:"+this.unitPrice * this.quantity+"元");
}
} -
树枝构件(Composite)角色 / 中间构件
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
/**
* 树枝构件: 袋子
*/
public class Bag implements Article{
/**
* 袋子名字
*/
private String name;
public Bag(String name) {
this.name = name;
}
/**
* 袋子中的商品
*/
private List<Article> bags = new ArrayList<Article>();
/**
* 往袋子中添加袋子或者商品
*/
@Override
public void add(Article article) {
bags.add(article);
}
@Override
public Double calculation() {
AtomicReference<Double> sum = new AtomicReference<>(0.0);
bags.forEach(e->{
sum.updateAndGet(v -> v + e.calculation());
});
return sum.get();
}
@Override
public void show() {
bags.forEach(Article::show);
}
} -
客户端代码实现
public static void main(String[] args) throws Exception {
Article smallOneBag = new Bag("1号小袋子");
smallOneBag.add(new Goods("芒果干", 2, 15.8));
smallOneBag.add(new Goods("薯片", 1, 9.8));
Article smallTwoBag = new Bag("2号小袋子");
smallTwoBag.add(new Goods("山楂", 3, 7.8));
smallTwoBag.add(new Goods("牛肉脯", 2, 19.8));
Article mediumBag = new Bag("中袋子");
mediumBag.add(new Goods("巧克力", 1, 39.8));
mediumBag.add(smallOneBag);
Article BigBag = new Bag("大袋子");
BigBag.add(new Goods("牛奶", 1, 79.8));
BigBag.add(mediumBag);
BigBag.add(smallTwoBag);
System.out.println("张三选购的商品有:");
BigBag.show();
Double sum = BigBag.calculation();
System.out.println("要支付的总价是:" + sum + "元");
}以上客户端代码中 new Bag(),new Goods()的引用都是Article,无须区别树叶对象和树枝对象,对客户端来说是透明的,此时Article调用add()是空实现或抛异常的(案例是空实现)。
-
案例结果输出

5.2 安全式的组合模式
安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成,只定义一些通用的方法。
-
抽象构件(Component)角色
/**
* 抽象构件(Component)角色
*/
public interface Article {
/**
* 计算价格
*/
public Double calculation();
/**
* 显示商品
*/
public void show();
} -
树叶构件(Leaf)角色
/**
* 树叶构件: 商品
*/
public class Goods implements Article {
/**
* 商品名称
*/
private String name;
/**
* 购买数量
*/
private Integer quantity;
/**
* 商品单价
*/
private Double unitPrice;
public Goods(String name, Integer quantity, Double unitPrice) {
this.name = name;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
@Override
public Double calculation() {
return this.unitPrice * this.quantity;
}
@Override
public void show() {
System.out.println(name + ": (数量:" + quantity + ",单价:" + unitPrice + "元)," +
"合计:"+this.unitPrice * this.quantity+"元");
}
} -
树枝构件(Composite)角色 / 中间构件
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
/**
* 树枝构件: 袋子
*/
public class Bag implements Article{
/**
* 袋子名字
*/
private String name;
public Bag(String name) {
this.name = name;
}
/**
* 袋子中的商品
*/
private List<Article> bags = new ArrayList<Article>();
/**
* 树枝构件特有的方法: 访问和管理子类的接口 大袋子装小袋子
* 往袋子中添加袋子或者商品
*/
@Override
public void add(Article article) {
bags.add(article);
}
@Override
public Double calculation() {
AtomicReference<Double> sum = new AtomicReference<>(0.0);
bags.forEach(e->{
sum.updateAndGet(v -> v + e.calculation());
});
return sum.get();
}
@Override
public void show() {
bags.forEach(Article::show);
}
} -
客户端代码实现
public static void main(String[] args) throws Exception {
Bag smallOneBag = new Bag("1号小袋子");
smallOneBag.add(new Goods("芒果干", 2, 15.8));
smallOneBag.add(new Goods("薯片", 1, 9.8));
Bag smallTwoBag = new Bag("2号小袋子");
smallTwoBag.add(new Goods("山楂", 3, 7.8));
smallTwoBag.add(new Goods("牛肉脯", 2, 19.8));
Bag mediumBag = new Bag("中袋子");
mediumBag.add(new Goods("巧克力", 1, 39.8));
mediumBag.add(smallOneBag);
Bag BigBag = new Bag("大袋子");
BigBag.add(new Goods("牛奶", 1, 79.8));
BigBag.add(mediumBag);
BigBag.add(smallTwoBag);
System.out.println("张三选购的商品有:");
BigBag.show();
Double sum = BigBag.calculation();
System.out.println("要支付的总价是:" + sum + "元");
}以上客户端代码中 new Bag(),new Goods()的引用都是Bag,Goods,客户端在调用时要知道树叶对象和树枝对象的存在。此时只有Bag才能调用add()。
-
案例结果输出

5.3 组合模式的扩展
在实际开发过程中,可以对树叶节点和树枝节点分别进行抽象,通过继承的方式让不同的树叶节点和树枝节点子类来实现行为。

六、组合模式的优缺点
优点
-
可以利用多态和递归机制更方便地使用复杂树结构
-
开闭原则:在组合体内加入新的对象,客户端不会更改源代码,可以一致地处理单个对象和组合对象。
缺点
-
设计较复杂,需要明确类之间的层次关系; -
不容易限制容器中的构件; -
不容易用继承的方法来增加构件的新功能;
七、组合模式和其他模式的关联
-
创建复杂组合树时使用生成器模式,使构造步骤以递归的方式运行。 -
责任链模式一般和组合模式结合使用。 -
迭代器模式可以用来遍历组合树。 -
访问者模式可以用来对整个组合树执行操作。 -
享元模式可以用来实现组合树的共享叶节点以节省内存。 -
组合模式和装饰模式的结构图很相似, 两者都依赖递归组合来组织无限数量的对象。 -
装饰模式只有一个子组件,并且为被封装对象添加了额外的职责 -
组合模式只是操作对子节点的原有行为得到结果。
八、总结
组合模式是一种解决树状问题的结构型模式,再使用过程中需要有较强的层次结构,在实现时要注意树枝节点的特有接口以及含有内部属性 List,List里面放 Component。
原文始发于微信公众号(星河之码):设计模式(10):组合模式
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/27158.html