设计模式-组合模式
一般我们提到组合,其实常见有三种意义:
- 组合优于继承,指进行类的实现时,由于继承会导致类的关系复杂,因此尽量使用组合、委托来替代继承实现
- UML 中类之间的关系,通常与聚合放在一起考察,组合是强依赖关系,部分不能脱离整体存在(公司-部门,公司不在了,部门也销毁了);而聚合是弱依赖,部分可以脱离整体存在(部门-员工,部门被撤销,员工依然存在)
- 组合设计模式,一般是用来处理树形结构数据,暴露一个统一的方法。
案例分析
把人口区划分为省市区,如果要计算省的人口,其实是省内所有市的人口和,市的人口是市内所有区的人口和,当然还可以继续往下划分...
如果要统计省的人口,最简单的实现思路就是:
- 通过省份 id 找到所有的 市
- 遍历市列表,通过 市 id 找到所有的区
- 挨个累加区的人口总数
当然这只是逻辑实现,如果真的在市区列表中 for 循环查找数据库,可能对数据库压力比较大,可以先查询组装为一个 Map<String,List<>>,这样只需要一次查询即可。
那利用组合模式该怎么实现呢?
- 定义一个计算人口数量的接口
java
package com.xsdl;
public interface PopulationNode {
int computePopulation();
}
- 定义省、市、区类分别实现这个接口
java
package com.xsdl;
import lombok.ToString;
import java.util.ArrayList;
import java.util.List;
@ToString
public class Province implements PopulationNode {
private String name;
private List<PopulationNode> cityList = new ArrayList<>();
public Province(String name) {
this.name = name;
}
public void addCity(City city) {
cityList.add(city);
}
@Override
public int computePopulation() {
return cityList.stream().mapToInt(item -> item.computePopulation()).sum();
}
}
注意第 13 行,由于 市 也同样实现了计算人口数量的接口,因此这里可以利用多态(接口引用指向实现对象)接收市。
java
package com.xsdl;
import lombok.ToString;
import java.util.ArrayList;
import java.util.List;
@ToString
public class City implements PopulationNode {
private String name;
private List<PopulationNode> districtList = new ArrayList<>();
public City(String name) {
this.name = name;
}
public void addDistrict(District district) {
districtList.add(district);
}
@Override
public int computePopulation() {
return districtList.stream().mapToInt(item -> item.computePopulation()).sum();
}
}
注意第 13 行,由于 区 也同样实现了计算人口数量的接口,因此这里可以利用多态(接口引用指向实现对象)接收区。
java
package com.xsdl;
import lombok.ToString;
@ToString
public class District implements PopulationNode {
private String name;
private int population;
public District(String name, int population) {
this.name = name;
this.population = population;
}
@Override
public int computePopulation() {
return population;
}
}
假设区就是最低级,区拥有 population 属性存放当前区的人数,区实现的计算人口数接口就是返回这个 population 属性,市是累加区的人数,省是累加市的人数。
由于所有的类都实现了计算人口数的接口,所以所有的对象都可以利用多态接收:
java
package com.xsdl;
import java.util.ArrayList;
import java.util.List;
public class Main {
private static final List<PopulationNode> populationNodeList = new ArrayList<>();
public static void main(String[] args) {
Province province = new Province("A省");
City zz = new City("A1市");
District zyq = new District("中原区", 2000);
District eqq = new District("二七区", 5000);
zz.addDistrict(zyq);
zz.addDistrict(eqq);
City zk = new City("A2市");
District gxq = new District("高新区", 1000);
zk.addDistrict(gxq);
province.addCity(zz);
province.addCity(zk);
populationNodeList.add(province);
for (PopulationNode populationNode : populationNodeList) {
System.out.println(populationNode.computePopulation());
}
}
}
组合模式其实有一些递归的思想,本节点的计算结果依赖内部属性的计算结果,但组合模式还具有可以统一接收(利用多态),统一处理的特点。
拓展分析
除了上面的例子,其实还有很多例子也可以利用组合模式实现:
- 统计某个文件夹中的文件数、目录数、总大小
java
package com.xsdl.file;
public interface Node {
int directoryCount();
int fileCount();
int fileSize();
}
java
package com.xsdl.file;
public class FileNode implements Node {
private String fileId;
private String fileName;
private int fileSize;
public FileNode(String fileId, String fileName, int fileSize) {
this.fileId = fileId;
this.fileName = fileName;
this.fileSize = fileSize;
}
@Override
public int directoryCount() {
return 0;
}
@Override
public int fileCount() {
return 1;
}
@Override
public int fileSize() {
return fileSize;
}
}
java
package com.xsdl.file;
import java.util.ArrayList;
import java.util.List;
public class DirectoryNode implements Node {
private String directoryId;
private String directoryName;
private List<Node> nodeList = new ArrayList<>();
public DirectoryNode(String directoryId, String directoryName) {
this.directoryId = directoryId;
this.directoryName = directoryName;
}
public void addNode(Node node) {
nodeList.add(node);
}
@Override
public int directoryCount() {
return nodeList.stream().mapToInt(item -> item.directoryCount()).sum() + 1;
}
@Override
public int fileCount() {
return nodeList.stream().mapToInt(item -> item.fileCount()).sum();
}
@Override
public int fileSize() {
return nodeList.stream().mapToInt(item -> item.fileSize()).sum();
}
}
java
package com.xsdl.file;
public class Main {
public static void main(String[] args) {
DirectoryNode root = new DirectoryNode("1", "/root");
FileNode rootFile = new FileNode("1-1-1", "/root/file2", 150);
root.addNode(rootFile);
DirectoryNode dir1 = new DirectoryNode("1-1", "/root/dir1");
DirectoryNode dir2 = new DirectoryNode("1-2", "/root/dir2");
FileNode file1 = new FileNode("1-1-1", "/root/dir1/file1", 100);
root.addNode(dir1);
root.addNode(dir2);
dir1.addNode(file1);
System.out.println(root.directoryCount());
System.out.println(root.fileCount());
System.out.println(root.fileSize());
}
}
- 实现一个简单的计算器
以 1+3*(4-2)*(2+3*6)-30
为例,其实可以抽象为 5 种实现,加减乘除和数字本身,加减乘除都有两个节点,数字只有一个节点,共同实现一个计算的接口,加减乘除的实现为 左节点的值 对应操作 右节点的值,数字节点的实现为返回值本身。
首先将中缀表达式转为后缀表达式:[1, 3, 4, 2, -, 2, 3, 6, *, +, *, *, +, 30, -]
利用一个栈来实现,循环遍历这个后缀表达式:
java
1 3 4 2
遇到了 - 号,弹出最外层两个元素封装为 (4-2)并入栈
1 3 (4-2)
2 3 6
遇到了 * 号,弹出最外层两个元素封装为 (6*3)并入栈
1 3 (4-2) 2 (6*3)
遇到了 + 号,弹出最外层两个元素封装为 ((6*3)+2) 并入栈
......
......
注意:栈是一个接口的 List ,例如 List ,共有五个实现: AddExpression、SubExpression、MultiplyExpression、DivisionExpression、NumberExpression
代码实现:组合模式实现一个简单的计算器
总结
组合模式主要用于类树形结构的遍历,所以其实平时用的并不是很多。