面向对象编程中,对象之间存在着种种关系,比如has-a, has-a关系又叫组合关系,比如公司和部门之间,集体和个人之间等等。公司一般都会有呈树状的组织架构,有时候,希望使用树状结构来表达整体和部分之间的关系,使客户能够不加以区分地处理结构中的每一个对象,这时候就会用到----组合模式。
1.组合模式
组合模式(Composite pattern) 描述一组对象,这些对象以相同方式作为同一类对象的单个实例进行处理。
组合模式的意图在于一致地处理整体和部分,所以,并不一定只要是hasA关系就可以使用组合模式的,只有“组件”和“零件”之间存在类似的行为时且具有明显的树形结构时,使用组合模式才是合适的。以下是组合模式的UML类图:
上图中,Client直接面向抽象接口Component来处理元素,不必区分到底是Leaf节点还是Composite节点。
Leaf节点: 叶子结点, 没有子节点,直接实现抽象接口Component的display()方法,作为终止方法;
Composite节点:复合节点,有一个或多个子节点,它持有一个列表来保存其子节点,将请求转发给每一个子节点。
组合模式的树形结构,与解释器模式类似,参考解释器模式。
2.透明模式和安全模式
透明模式:Component中定义子节点相关操作的抽象方法(add、remove等),使客户端一致地处理Leaf和Composite对象;但是由于Leaf节点没有子节点,所以这种方式类型安全问题需要注意。
安全模式:子节点相关操作的方法(add,remove等),只定义在Composite对象中,保证了类型安全;但是需要客户端分别处理两种对象,也就失去了一致性。
一般,相比类型安全来说,组合模式更加强调一致性。
下面的例子使用透明模式为例。
3.实现XML文档创建
3.1 设计
组合模式一般应用于有明显层级结构的对象上,比如公司的组织架构,目录结构等等。XML(可扩展标记语言)是开发中常常用来作为配置文件来使用,本例中我尝试应用组合模式来实现一个简单的XML结构的文档,文档根元素名称为root, 子元素可随意命名,子元素可包含文本,结构如下:
<root> <A> <A1> 组合模式就是好! <A1/> <A/> <B> 对待每个对象都一样! <B/> <root/>
文档有明显的层次结构,元素可分为两类:1.复合元素(包活根元素), 2.文本, 设计类图如下:
DocumentHelper是一个工具类,提供了一系列静态方法来创建、修改、打印文档。
3.2代码实现
文档抽象类, 使用透明模式,定义了子节点相关的方法add,remove, 定义子类统一需要实现的方法display(int indent), indent是为了打印美观而设置的缩进。
abstract class Document { public abstract void add(Document document); public abstract void remove(Document document); public abstract String display(int indent); }
叶子结点---文本元素的实现, 注意到文本对象由于没有子节点,所以add,remove方法不支持;
display方法返回该文本元素的文本字符串,包含缩进。
class Text extends Document { private String content; public Text(String content) { this.content = content; } @Override public void add(Document document) { throw new UnsupportedOperationException("不支持次操作"); } @Override public void remove(Document document) { throw new UnsupportedOperationException("不支持次操作"); } @Override public String display(int indent) { StringBuilder builder = new StringBuilder(); //缩进距离 for (int i = 0; i < indent; i++) { builder.append(" "); } //叶子元素直接输出内容 builder.append(content + "\n"); return builder.toString(); } }
复合节点对象实现,Element对象包含子节点,维护一个List,存放其子节点,并重写add,remove方法来设置子节点;
display方法中,实现了<E>xxxx</E>这种形式的结构,实现方式是在<></>之间,调用了其子节点的display方法。
class Element extends Document { private String name; private List<Document> documents = new ArrayList<>(); public Element(String name) { this.name = name; } @Override public void add(Document document) { documents.add(document); } @Override public void remove(Document document) { documents.remove(document); } @Override public String display(int indent) { StringBuilder builder = new StringBuilder(); //缩进距离 for (int i = 0; i < indent; i++) { builder.append(" "); } //元素开始标签 builder.append("<" + name + ">\n"); //子元素 for (Document document : documents) { builder.append(document.display(indent + 2)); } //结束标签缩进距离 for (int i = 0; i < indent; i++) { builder.append(" "); } //元素结束标签 builder.append("<" + name + "/>\n"); return builder.toString(); } }
工具类,帮助生成、修改、打印文档的元素
class DocumentHelper { public static Document createRootDocument() { Document root = new Element("root"); return root; } public static Document createElement(String name) { return new Element(name); } public static Document createText(String content) { return new Text(content); } public static void printDocument(Document root) { String xml = root.display(0); System.out.println(xml); } }
客户端生成文档并打印:
public class MyXml { public static void main(String[] args) { //创建根元素 Document root = DocumentHelper.createRootDocument(); //创建两个子元素A合B Document a = DocumentHelper.createElement("A"); Document b = DocumentHelper.createElement("B"); //存入子元素到根元素 root.add(a); root.add(b); //在子元素A下再创建一个子元素A1 Document a1 = DocumentHelper.createElement("A1"); a.add(a1); //在子元素A1下创建文本 Document text = DocumentHelper.createText("组合模式就是好!"); a1.add(text); //在B元素下创建文本 Document text2 = DocumentHelper.createText("对待每个对象都一样!"); b.add(text2); //显示xml DocumentHelper.printDocument(root); } }
输出结果
<root> <A> <A1> 组合模式就是好! <A1/> <A/> <B> 对待每个对象都一样! <B/> <root/>
多亏了多态,树形结构中的每一个对象都能够以相同的处理方式对待,因为我们只面向接口而不依赖实现,这是依赖倒置原则的体现啊。
4.总结
组合模式解决一下问题:
- 怎样表示整体-部分层级结构,从而使客户能一致地作用于层级中的每个对象;
- 整体-部分层级结构如何表示成树状结构。
具体实现的方式:
- 定义一个Component接口,Leaf和Composite实现, Composite中维护一个List用于保存子节点。
- Leaf对象直接实现客户请求,Composite对象将请求转发给子节点。
在透明模式和安全模式的选择中,组合模式更加强调一致性,从而透明模式更具有“普世价值”。