设计模式内容分享(十):组合模式
组合模式是一种解决树状问题的结构型模式,再使用过程中需要有较强的层次结构,在实现时要注意树枝节点的特有接口以及含有内部属性 List,List里面放 Component。
目录
一、组合模式是什么
组合模式:又叫作整体-部分(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)角色 */ publicinterface Article { /** * 树枝构件特有的方法: 访问和管理子类的接口 大袋子装小袋子 */ public void add(Article article); /** * 计算价格 */ public Double calculation(); /** * 显示商品 */ public void show(); }
-
树叶构件(Leaf)角色
/** * 树叶构件: 商品 */ publicclass 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() { returnthis.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; /** * 树枝构件: 袋子 */ publicclass 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)角色 */ publicinterface Article { /** * 计算价格 */ public Double calculation(); /** * 显示商品 */ public void show(); }
-
树叶构件(Leaf)角色
/** * 树叶构件: 商品 */ publicclass 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() { returnthis.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; /** * 树枝构件: 袋子 */ publicclass 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。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)