组合模式

组合模式(Composite Pattern)也称之为整体-部分(Part-Whole)模式。组合模式的核心是通过将单个对象(叶子节点)和组合对象(树枝节点)用相同的接口进行表示,使得单个对象和组合对象的使用具有一致性,组合模式属于结构型模式。
组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,最顶层的节点称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点如下图所示:
组合模式 - 图1
组合模式有两种写法,分别是透明模式安全模式。下面我们会分别示范这两种写法,并对其进行对比分析。

透明模式


下面我们以高考的科目为例,来看看组合模式中的透明模式该如何实现(这里我们需要新建 composite/transparency 目录,相关类创建在 composite/transparency 目录下)。

  • 首先新建一个高考科目的抽象类 GkAbstractCourse.java,里面提供了三个方法。
  1. package composite.transparency;
  2. public abstract class GkAbstractCourse {
  3. public void addChild(GkAbstractCourse course){//添加子节点
  4. System.out.println("不支持添加操作");
  5. }
  6. public String getName() throws Exception {//获取当前节点名称
  7. throw new Exception("不支持获取名称");
  8. }
  9. public void info() throws Exception{//获取高考科目信息
  10. throw new Exception("不支持查询信息操作");
  11. }
  12. }

这个类是抽象类,但是在这里并没有将这些方法定义为抽象方法,而是默认都抛出异常。这么做的原因是假如定义为抽象方法,那么所有的子类都必须重写父类方法。但是这种通过抛异常的方式,如果子类需要用到的功能就重写覆盖父类方法即可,不需要用到的方法直接无需重写。

  • 新增一个叶子节点,普通高考科目类 LeafCource.java 来继承 GkAbstractCourse 类,这个类就是最底层,无法包含其它科目,比如语文,正因为其无法包含其它课程所以不需要重写 addChild 方法。
  1. package composite.transparency;
  2. public class LeafCource extends GkAbstractCourse {
  3. private String name;//课程名称
  4. private String score;//课程分数
  5. public LeafCource(String name, String score) {
  6. this.name = name;
  7. this.score = score;
  8. }
  9. @Override
  10. public String getName(){
  11. return this.name;
  12. }
  13. @Override
  14. public void info() {
  15. System.out.println("课程:" + this.name + ",分数:" + score);
  16. }
  17. }
  • 接下来新建一个树枝类 BranchCource.java,当前类可以包含其它科目的类,比如理综可以包括物理、化学和生物,这个类将父类的 3 个方法全部进行了重写。 ```java package composite.transparency;

import java.util.ArrayList; import java.util.List;

public class BranchCource extends GkAbstractCourse{ private List courseList = new ArrayList<>(); private String name;//科目名称 private int level;//层级

  1. public BranchCource(String name, int level) {
  2. this.name = name;
  3. this.level = level;
  4. }
  5. @Override
  6. public void addChild(GkAbstractCourse course) {//添加子课程
  7. courseList.add(course);
  8. }
  9. @Override
  10. public String getName(){//获取课程名称
  11. return this.name;
  12. }
  13. @Override
  14. public void info() throws Exception {//打印课程信息
  15. System.out.println("课程:" + this.name);
  16. for (GkAbstractCourse course : courseList){
  17. for (int i=0;i<level;i++){//根据层级关系打印空格,这样打印结果可以明显看出层级关系
  18. System.out.print(" ");
  19. }
  20. System.out.print(">");
  21. course.info();
  22. }
  23. }

}

  1. - 最后我们新建一个测试类 TestTransparency.java 来测试一下。
  2. ```java
  3. package composite.transparency;
  4. public class TestTransparency {
  5. public static void main(String[] args) throws Exception {
  6. GkAbstractCourse ywCourse = new LeafCource("语文","150");
  7. GkAbstractCourse sxCourse = new LeafCource("数学","150");
  8. GkAbstractCourse yyCourse = new LeafCource("英语","150");
  9. GkAbstractCourse wlCourse = new LeafCource("物理","110");
  10. GkAbstractCourse hxCourse = new LeafCource("化学","100");
  11. GkAbstractCourse swCourse = new LeafCource("生物","90");
  12. GkAbstractCourse lzCourse = new BranchCource("理综",2);
  13. lzCourse.addChild(wlCourse);
  14. lzCourse.addChild(hxCourse);
  15. lzCourse.addChild(swCourse);
  16. GkAbstractCourse gkCourse = new BranchCource("理科高考",1);
  17. gkCourse.addChild(ywCourse);
  18. gkCourse.addChild(sxCourse);
  19. gkCourse.addChild(yyCourse);
  20. gkCourse.addChild(lzCourse);
  21. gkCourse.info();
  22. swCourse.addChild(ywCourse);//这里会报错,因为生物已经是叶子节点,无法继续添加子节点
  23. }
  24. }

执行 javac composite/transparency/*.java 命令进行编译,然后再执行 java composite.transparency.TestTransparency 命令运行测试类(大家一定要自己动手运行哦,只有自己实际去运行了才会更能体会其中的思想)。
组合模式 - 图2
上面就是一个透明模式的写法,之所以称之为透明模式就是因为父类将所有方法都定义好了,对各个子类完全透明。这样做的好处是客户端无需分辨当前对象是属于树枝节点还是叶子节点,因为它们具备了完全一致的接口,不过缺点就是叶子节点得到了一些不属于它的方法,比如上面的 addChild 方法,这个很明显违背了接口隔离性原则。
而组合模式只是规定了系统各个层次的最基础的一致性行为,而把组合(树节点)本身的方法(如树枝节点管理子类的 addChild 等方法)放到自身当中。接下来我们再看看安全模式应该如何实现。

安全模式示例

这里我们新建一个 composite/safe 目录,相关类创建在 composite/safe 目录下。

  • 首先还是新建一个顶层的抽象课程类 GkAbstractCourse.java,不过这次只定义了一个通用的查询课程信息的方法,而且是抽象方法,同时又多具备了课程名字和课程分数两个属性。
  1. package composite.safe;
  2. public abstract class GkAbstractCourse {
  3. protected String name;//课程名称
  4. protected String score;//课程分数
  5. public GkAbstractCourse(String name, String score) {
  6. this.name = name;
  7. this.score = score;
  8. }
  9. public abstract void info();//获取课程信息
  10. }
  • 新增一个叶子节点,普通高考科目类 LeafCource.java 来继承 GkAbstractCourse 类,这个类就是最底层,无法包含其它科目,比如语文,所以它没什么其它特殊的自定义方法。 ```java package composite.safe;

public class LeafCource extends GkAbstractCourse { public LeafCource(String name, String score) { super(name,score); }

  1. @Override
  2. public void info() {//获取课程信息
  3. System.out.println("课程:" + this.name + ",分数:" + this.score);
  4. }

}

  1. - 接下来新建一个树枝类 BranchCource.java,当前类可以包含其它科目的类,比如理综可以包括物理、化学和生物,所以这个类额外自定义了一个层级属性和一个添加子课程 addChild 方法。
  2. ```java
  3. package composite.safe;
  4. import java.util.ArrayList;
  5. import java.util.List;
  6. public class BranchCource extends GkAbstractCourse{
  7. private List<GkAbstractCourse> courseList = new ArrayList<>();//子课程
  8. private int level;//层级
  9. public BranchCource(String name, String score, int level) {
  10. super(name,score);
  11. this.level = level;
  12. }
  13. public void addChild(GkAbstractCourse course) {//添加子课程
  14. courseList.add(course);
  15. }
  16. @Override
  17. public void info() {//打印课程信息
  18. System.out.println("课程:" + this.name + ",分数:" + this.score);
  19. for (GkAbstractCourse course : courseList){
  20. for (int i=0;i<level;i++){//根据层级关系打印空格,这样打印结果可以明显看出层级关系
  21. System.out.print(" ");
  22. }
  23. System.out.print(">");
  24. course.info();
  25. }
  26. }
  27. }
  • 最后我们新建一个测试类 TestSafe.java 来测试一下。
  1. package composite.safe;
  2. public class TestSafe {
  3. public static void main(String[] args) throws Exception {
  4. LeafCource ywCourse = new LeafCource("语文","150");
  5. LeafCource sxCourse = new LeafCource("数学","150");
  6. LeafCource yyCourse = new LeafCource("英语","150");
  7. LeafCource wlCourse = new LeafCource("物理","110");
  8. LeafCource hxCourse = new LeafCource("化学","100");
  9. LeafCource swCourse = new LeafCource("生物","90");
  10. BranchCource lzCourse = new BranchCource("理综","300",2);
  11. lzCourse.addChild(wlCourse);
  12. lzCourse.addChild(hxCourse);
  13. lzCourse.addChild(swCourse);
  14. BranchCource gkCourse = new BranchCource("理科高考","750",1);
  15. gkCourse.addChild(ywCourse);
  16. gkCourse.addChild(sxCourse);
  17. gkCourse.addChild(yyCourse);
  18. gkCourse.addChild(lzCourse);
  19. gkCourse.info();
  20. }
  21. }

执行 javac composite/safe/*.java 命令进行编译,然后再执行 java composite.safe.TestSafe 命令运行测试类(大家一定要自己动手运行哦,只有自己实际去运行了才会更能体会其中的思想)。
组合模式 - 图3
这里和透明方式不一样,叶子节点不具备 addChild 功能,所以无法调用,而上面的示例中时可以被调用,但是调用之后显示不支持,这就是这两种写法最大的区别。

组合模式适用场景

组合模式一般应用在有层级关系的场景,最经典的就是树形菜单、文件和文件夹的管理等。

组合模式优点

组合模式清楚的定义了分层次的复杂对象,让客户端可以忽略层次的差异,方便对整个层次进行动态控制。

组合模式缺点

组合模式的叶子和树枝的声明是实现类而不是接口,违反了依赖倒置原则,而且组合模式会使设计更加抽象不好理解。