组合模式可以在需要针对“树形结构”进行操作的应用中使用,例如扫描文件夹、渲染网站导航结构等等。

一、什么是组合模式?

组合模式将一组相似的对象看做一个对象处理,并根据一个树状结构来组合对象,提供一个统一的方法去访问相应的对象,使得用户对单个对象和组合对象的使用具有一致性。

很抽象对吧,那就到了举例子的时候:

  • 公司组织关系树

公司组织关系可能分为部门与人,其中人属于部门,有的人有下属,有的人没有下属。如果我们统一将部门、人抽象为组织节点,就可以方便的统计某个部门下有多少人、财务数据等等,而不用关心当前节点是部门还是人。

  • 操作系统的文件夹与文件

操作系统的文件夹与文件也是典型的树状结构,为了方便递归出文件夹内文件数量或者文件总大小,我们最好设计的时候就将文件夹与文件抽象为文件,这样每个节点都拥有相同的方法添加、删除、查找子元素,而不需要关心当前节点是文件夹或是文件。

这下就明白了吧,就是常见的树形结构。

组合模式有两种实现:安全模式和透明模式

组合模式Composite——树形结构不再头疼_组合模式

  • Component抽象构件角色
    定义参加组合对象的共有方法和属性,可以定义一些默认的行为或属性。
  • Leaf叶子构件
    Leaf叶子构件叶子对象,其下再也没有其他的分支,也就是遍历的最小单位。
  • Composite树枝构件
    树枝对象,它的作用是组合树枝节点和叶子节点形成一个树形结构。组合模式的重点就在树枝构件。

二、安全模式实现

public abstract class Component {
//个体和整体都具有
public void operation(){
//编写业务逻辑
}
}
public class Composite extends Component {
//构件容器
private List<Component> componentArrayList = new ArrayList<Component>();
//增加一个叶子构件或树枝构件
public void add(Component component){
this.componentArrayList.add(component);
}
//删除一个叶子构件或树枝构件
public void remove(Component component){
this.componentArrayList.remove(component);
}
//获得分支下的所有叶子构件和树枝构件
public List<Component> getChildren(){
return this.componentArrayList;
}
}
public class Leaf extends Component {
/*
* 可以覆写父类方法
* public void operation(){
*
* }
*/
}
public class Client {
public static void main(String[] args) {
//创建一个根节点
Composite root = new Composite();
root.operation();
//创建一个树枝构件
Composite branch = new Composite();
//创建一个叶子节点
Leaf leaf = new Leaf();
//建立整体
root.add(branch);
branch.add(leaf);
}

//通过递归遍历树
public static void showTree(Composite root){
for(Component c:root.getChildren()){
if(c instanceof Leaf){ //叶子节点
c.operation();
}else{ //树枝节点
showTree((Composite)c);
}
}
}
}

三、透明模式实现

public abstract class Component {
//个体和整体都具有
public void operation(){
//编写业务逻辑
}
//增加一个叶子构件或树枝构件
public abstract void add(Component component);
//删除一个叶子构件或树枝构件
public abstract void remove(Component component);
//获得分支下的所有叶子构件和树枝构件
public abstract List<Component> getChildren();
}
public class Composite extends Component {
//构件容器
private ArrayList<Component> componentArrayList = new ArrayList<Component>();
//增加一个叶子构件或树枝构件
public void add(Component component){
this.componentArrayList.add(component);
}
//删除一个叶子构件或树枝构件
public void remove(Component component){
this.componentArrayList.remove(component);
}
//获得分支下的所有叶子构件和树枝构件
public List<Component> getChildren(){
return this.componentArrayList;
}
}
public class Leaf extends Component {

public void add(Component component){
//空实现
}

public void remove(Component component){
//空实现
}

public List<Component> getChildren(){
//空实现
return null;
}
}
public class Client {
public static void main(String[] args) {
//创建一个根节点
Composite root = new Composite();
root.operation();
//创建一个树枝构件
Composite branch = new Composite();
//创建一个叶子节点
Leaf leaf = new Leaf();
//建立整体
root.add(branch);
branch.add(leaf);
}

//通过递归遍历树
public static void showTree(Component root){
for(Component c:root.getChildren()){
if(c instanceof Leaf){ //叶子节点
c.operation();
}else{ //树枝节点
showTree(c);
}
}
}
}

四、安全模式和透明模式的区别

上面的实现代码可以看到:

  1. Composite和Client是一样的
  2. 安全模式在抽象组件中只定义一些默认的行为或属性,它是把树枝节点和树叶节点彻底分开
  3. 透明模式是把用来组合使用的方法放到抽象类中,不管叶子对象还是树枝对象都有相同的结构,通过判断确认是叶子节点还是树枝节点,如果处理不当,这个会在运行期出现问题,不是很建议的方式
  4. 安全模式与依赖倒置原则冲突
  5. 透明模式的好处就是它基本遵循了依赖倒转原则,方便系统进行扩展
  6. 安全模式在遍历树形结构的的时候需要进行强制类型转换;在透明模式下,遍历整个树形结构是比较容易的,不用进行强制类型转换。

产生上面的区别的根本原因是透明模式的Composite定义了更多的方法,叶子节点和非叶子节点都去实现,这样叶子节点和非叶子节点的区分度就不高了,但是兼容性更强。

 五、安全模式下代码实例

现有下面的文件夹树形关系:

组合模式Composite——树形结构不再头疼_组合模式_02

使用组合的安全模式实现下:

@ToString
public abstract class FolderMode {
private String name;
private String path;
private String size;

public FolderMode(String name, String path, String size) {
this.name = name;
this.path = path;
this.size = size;
}
}
/**
* 非叶子文件夹
*/
public class FolderNonLeaf extends FolderMode {
private List<FolderMode> list=new ArrayList<>();

public FolderNonLeaf(String name, String path, String size) {
super(name, path, size);
}

public void addFolder(FolderMode folder){
this.list.add(folder);
}

public List<FolderMode> getList(){
return this.list;
}
}
public class FolderLeaf extends FolderMode {
public FolderLeaf(String name, String path, String size) {
super(name, path, size);
}
}
public class Client {
public static void main(String[] args) {
FolderNonLeaf folderNonLeaf = getFolders();
showTree(folderNonLeaf);
}

private static FolderNonLeaf getFolders() {
// 第一层根目录
FolderNonLeaf root = new FolderNonLeaf("文件夹1", "1", "1");
// 文件夹1的两个子文件夹
FolderNonLeaf second_A = new FolderNonLeaf("文件夹1-1", "1/1-1", "6");
FolderNonLeaf second_B = new FolderNonLeaf("文件夹1-2", "1/1-2", "6");
// second_A的三个子文件夹
FolderNonLeaf third_second_A_A = new FolderNonLeaf("文件夹1-1-1", "1/1-1/1-1-1", "40");
FolderNonLeaf third_second_A_B = new FolderNonLeaf("文件夹1-1-2", "1/1-1/1-1-2", "40");
FolderNonLeaf third_second_A_C = new FolderNonLeaf("文件夹1-1-3", "1/1-1/1-1-3", "30");
// second_B的两个子文件夹
FolderNonLeaf third_second_B_A = new FolderNonLeaf("文件夹1-2-1", "1/1-1/1-2-1", "30");
FolderNonLeaf third_second_B_B = new FolderNonLeaf("文件夹1-2-2", "1/1-1/1-2-2", "30");
// third_second_A_A子文件夹
FolderNonLeaf fourth_third_second_A_A_A = new FolderNonLeaf("文件夹1-1-1-1", "1/1-1/1-1-1/1-1-1-1", "60");

// 开始组装树状族谱
// 第二层
root.addFolder(second_A);
root.addFolder(second_B);
// 第三层
second_A.addFolder(third_second_A_A);
second_A.addFolder(third_second_A_B);
second_A.addFolder(third_second_A_C);
second_B.addFolder(third_second_B_A);
second_B.addFolder(third_second_B_B);
// 第四层
third_second_A_A.addFolder(fourth_third_second_A_A_A);
return root;
}

//通过递归遍历树
private static void showTree(FolderNonLeaf root) {
System.out.println(root.toString());
for(FolderMode folder:root.getList()){
// 叶子节点
if(folder instanceof FolderLeaf){
System.out.println(folder.toString());
}else{
showTree((FolderNonLeaf) folder);
}
}
}
}
FolderMode(name=文件夹1, path=1, size=1)
FolderMode(name=文件夹1-1, path=1/1-1, size=6)
FolderMode(name=文件夹1-1-1, path=1/1-1/1-1-1, size=40)
FolderMode(name=文件夹1-1-1-1, path=1/1-1/1-1-1/1-1-1-1, size=60)
FolderMode(name=文件夹1-1-2, path=1/1-1/1-1-2, size=40)
FolderMode(name=文件夹1-1-3, path=1/1-1/1-1-3, size=30)
FolderMode(name=文件夹1-2, path=1/1-2, size=6)
FolderMode(name=文件夹1-2-1, path=1/1-1/1-2-1, size=30)
FolderMode(name=文件夹1-2-2, path=1/1-1/1-2-2, size=30)

这里把叶子结点和非叶子节点的共同部分抽出来放在 FolderMode 中。

六、适用场景

优点:

  • 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让高层模块忽略了层次的差异,方便对整个层次结构进行控制。
  • 高层模块可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了高层模块的代码。
  • 在组合模式中增加了新的枝干构件和叶子构件都很方便,无须对现有类库进行任何修改,符合 "开闭原则"。
  • 组合模式为树状结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和树干对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

缺点:

早新增构件时不好对树干中的构件类型进行限制,不能依赖类型系统来施加这些约束,因为在大多数情况下,它们都来自于想听的抽象层,此时,必须进行类型检查来实现,这个实现过程较为复杂。

适用场景:

  • 只要是树形结构或者只要是要体现局部和整体的关系的时候,而且这种关系还可能比较深,就要考虑一下组合模式。
  • 从一个整体中能够独立出部分模块或功能的场景。
  • 维护和展示部分-整体关系的场景。