设计模式---组合模式

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式---组合模式相关的知识,希望对你有一定的参考价值。


引言

树形结构不论在生活中或者是开发中都是一种非常常见的结构,一个容器对象(如文件夹)下可以存放多种不同的叶子对象或者容器对象,容器对象与叶子对象之间属性差别可能非常大。

由于容器对象和叶子对象在功能上的区别,在使用这些对象的代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下我们希望一致地处理它们,因为对于这些对象的区别对待将会使得程序非常复杂。

组合模式为解决此类问题而诞生,它可以让叶子对象和容器对象的使用具有一致性。


组合模式介绍

组合多个对象形成树形结构以表示具有 “整体—部分” 关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为 “整体—部分”(Part-Whole) 模式,它是一种对象结构型模式。

由于在软件开发中存在大量的树形结构,因此组合模式是一种使用频率较高的结构型设计模式,

在XML解析、组织结构树处理、文件系统设计等领域,组合模式都得到了广泛应用。


角色

  • Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。
  • Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。
  • Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。

组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。


模式结构


示例代码


典型的抽象构件角色代码:

1 public abstract class Component
2 {
3     public abstract void add(Component c);
4     public abstract void remove(Component c);
5     public abstract Component getChild(int i);
6     public abstract void operation(); 
7 } 

典型的叶子构件角色代码:

 1 public class Leaf extends Component
 2 {
 3     public void add(Component c)
 4     { //异常处理或错误提示 }    
 5         
 6     public void remove(Component c)
 7     { //异常处理或错误提示 }
 8     
 9     public Component getChild(int i)
10     { //异常处理或错误提示 }
11     
12     public void operation()
13     {
14         //实现代码
15     } 
16 } 

典型的容器构件角色代码:


 1 public class Composite extends Component
 2 {
 3     private ArrayList list = new ArrayList();
 4     
 5     public void add(Component c)
 6     {
 7         list.add(c);
 8     }
 9     
10     public void remove(Component c)
11     {
12         list.remove(c);
13     }
14     
15     public Component getChild(int i)
16     {
17         (Component)list.get(i);
18     }
19     
20     public void operation()
21     {
22         for(Object obj:list)
23         {
24             ((Component)obj).operation();
25         }
26     }     
27 } 

水果盘

在水果盘(Plate)中有一些水果,如苹果(Apple)、香蕉(Banana)、梨子(Pear),当然大水果盘中还可以有小水果盘,现需要对盘中的水果进行遍历(吃),当然如果对一个水果盘执行“吃”方法,实际上就是吃其中的水果。使用组合模式模拟该场景 。

 1 //抽象构建
 2 public abstract class MyElement
 3 {
 4     public abstract void eat();
 5 }
 6 
 7 //容器构建
 8 import java.util.*;
 9 
10 public class Plate extends MyElement
11 {
12     private ArrayList list=new ArrayList();
13     
14     public void add(MyElement element)
15     {
16        list.add(element);    
17     }
18     
19     public void delete(MyElement element)
20     {
21         list.remove(element);
22     }
23 
24     public void eat()
25     {
26         for(Object object:list)
27         {
28             ((MyElement)object).eat();    //递归
29         }
30     }
31 }
32 
33 //叶子构建
34 public class Apple extends MyElement
35 {
36     public void eat()
37     {
38         System.out.println("吃苹果!");
39     }
40 }
41 
42 //叶子构建
43 public class Banana extends MyElement
44 {
45     public void eat()
46     {
47         System.out.println("吃香蕉!");
48     }
49 }
50 
51 //叶子构建
52 public class Pear extends MyElement
53 {
54     public void eat()
55     {
56         System.out.println("吃梨子!");
57     }
58 }
59 
60 //客户端
61 public class Client
62 {
63     public static void main(String a[])
64     {
65         MyElement obj1,obj2,obj3,obj4,obj5;
66         Plate plate1,plate2,plate3;
67         
68         obj1=new Apple();
69         obj2=new Pear();
70         plate1=new Plate();
71         plate1.add(obj1);
72         plate1.add(obj2);
73         
74         obj3=new Banana();
75         obj4=new Banana();
76         plate2=new Plate();
77         plate2.add(obj3);
78         plate2.add(obj4);
79         
80         obj5=new Apple();
81         plate3=new Plate();
82         plate3.add(plate1);
83         plate3.add(plate2);
84         plate3.add(obj5);
85             
86         plate3.eat();
87     }
88 }


文件浏览

我们来实现一个简单的目录树,有文件夹和文件两种类型,首先需要一个抽象构件类,声明了文件夹类和文件类需要的方法

public abstract class Component {

    public String getName() {
        throw new UnsupportedOperationException("不支持获取名称操作");
    }

    public void add(Component component) {
        throw new UnsupportedOperationException("不支持添加操作");
    }

    public void remove(Component component) {
        throw new UnsupportedOperationException("不支持删除操作");
    }

    public void print() {
        throw new UnsupportedOperationException("不支持打印操作");
    }

    public String getContent() {
        throw new UnsupportedOperationException("不支持获取内容操作");
    }
}

实现一个文件夹类 Folder,继承 Component,定义一个 List<Component> 类型的componentList属性,用来存储该文件夹下的文件和子文件夹,并实现 getName、add、remove、print等方法

public class Folder extends Component {
    private String name;
    private List<Component> componentList = new ArrayList<Component>();
      public Integer level;

    public Folder(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void add(Component component) {
        this.componentList.add(component);
    }

    @Override
    public void remove(Component component) {
        this.componentList.remove(component);
    }

     @Override
    public void print() {
        System.out.println(this.getName());
        if (this.level == null) {
            this.level = 1;
        }
        String prefix = "";
        for (int i = 0; i < this.level; i++) {
            prefix += "\\t- ";
        }
        for (Component component : this.componentList) {
            if (component instanceof Folder){
                ((Folder)component).level = this.level + 1;
            }
            System.out.print(prefix);
            component.print();
        }
        this.level = null;
    }
}

文件类 File,继承Component父类,实现 getName、print、getContent等方法

public class File extends Component {
    private String name;
    private String content;

    public File(String name, String content) {
        this.name = name;
        this.content = content;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void print() {
        System.out.println(this.getName());
    }

    @Override
    public String getContent() {
        return this.content;
    }
}

我们来测试一下

public class Test {
    public static void main(String[] args) {
        Folder DSFolder = new Folder("设计模式资料");
        File note1 = new File("组合模式笔记.md", "组合模式组合多个对象形成树形结构以表示具有 \\"整体—部分\\" 关系的层次结构");
        File note2 = new File("工厂方法模式.md", "工厂方法模式定义一个用于创建对象的接口,让子类决定将哪一个类实例化。");
        DSFolder.add(note1);
        DSFolder.add(note2);

        Folder codeFolder = new Folder("样例代码");
        File readme = new File("README.md", "# 设计模式示例代码项目");
        Folder srcFolder = new Folder("src");
        File code1 = new File("组合模式示例.java", "这是组合模式的示例代码");

        srcFolder.add(code1);
        codeFolder.add(readme);
        codeFolder.add(srcFolder);
        DSFolder.add(codeFolder);

        DSFolder.print();
    }
}

输出结果

设计模式资料
	- 组合模式笔记.md
	- 工厂方法模式.md
	- 样例代码
	- 	- README.md
	- 	- src
	- 	- 	- 组合模式示例.java

在这里父类 Component 是一个抽象构件类,Folder 类是一个容器构件类,File 是一个叶子构件类,FolderFile 继承了 ComponentFolderComponent 又是聚合关系


更复杂的组合模式

透明与安全

在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式。

透明组合模式

透明组合模式中,抽象构件角色中声明了所有用于管理成员对象的方法,譬如在示例中 Component 声明了 add、remove 方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。

透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)


安全组合模式

在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在容器构件 Composite 类中声明并实现这些方法

安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。

在实际应用中 java.awt 和 swing 中的组合模式即为安全组合模式。


组合模式总结

优点

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

缺点

  • 使得设计更加复杂,客户端需要花更多时间理清类之间的层次关系。
  • 在增加新构件时很难对容器中的构件类型进行限制。

适用场景

  • 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
  • 在一个使用面向对象语言开发的系统中需要处理一个树形结构。
  • 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。

应用

XML文档解析

 1 <?xml version="1.0"?>
 2   <books>
 3     <book>
 4         <author>Carson</author>
 5         <price format="dollar">31.95</price>
 6         <pubdate>05/01/2001</pubdate>
 7     </book>
 8     <pubinfo>
 9         <publisher>MSPress</publisher>
10         <state>WA</state>
11     </pubinfo>
12   </books> 

文件

操作系统中的目录结构是一个树形结构,因此在对文件和文件夹进行操作时可以应用组合模式,例如杀毒软件在查毒或杀毒时,既可以针对一个具体文件,也可以针对一个目录。如果是对目录查毒或杀毒,将递归处理目录中的每一个子目录和文件。


HashMap

HashMap 提供 putAll 的方法,可以将另一个 Map 对象放入自己的存储空间中,如果有相同的 key 值则会覆盖之前的 key 值所对应的 value 值

public class Test {
    public static void main(String[] args) {
        Map<String, Integer> map1 = new HashMap<String, Integer>();
        map1.put("aa", 1);
        map1.put("bb", 2);
        map1.put("cc", 3);
        System.out.println("map1: " + map1);

        Map<String, Integer> map2 = new LinkedMap();
        map2.put("cc", 4);
        map2.put("dd", 5);
        System.out.println("map2: " + map2);

        map1.putAll(map2);
        System.out.println("map1.putAll(map2): " + map1);
    }
}

输出结果

map1: {aa=1, bb=2, cc=3}
map2: {cc=4, dd=5}
map1.putAll(map2): {aa=1, bb=2, cc=4, dd=5}

查看 putAll 源码

public void putAll(Map是否有在单个活动中处理多个片段的 Android 设计模式?

尝试使用片段保存夜间模式状态

用java代码实现组合模式

组合设计模式

23种设计模式之组合模式代码实例

设计模式之组合模式