组合模式
组合模式是一种结构型设计模式,它的使用形式比较固定,适合用来表示树形结构,或者说具有层级结构的数据。
比如目录和文件,xml等。
What?
将一组对象组织(Compose)成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(在很多设计模式书籍中,“客户端”代指代码的使用者。)可以统一单个对象和组合对象的处理逻辑。
How?
练习1:如何统计目录下的文件数?
以目录和文件为例,给定一个需求:想要统计目录下有多少文件。
文件结构:
如何实现?
首先,我们抽象出一个FileSystemNode.class类,统一表示文件和目录
public abstract class FileSystemNode {
private String path;
public FileSystemNode(String path) {
this.path = path;
}
public String getPath() {
return path;
}
/**
* 统计指定目录下的文件个数
*
* @return
*/
public abstract int countNumOfFiles();
}
再定义文件类
public class File extends FileSystemNode {
public File(String path) {
super(path);
}
@Override
public int countNumOfFiles() {
return 1;
}
}
文件时叶子节点,统计文件的个数,直接返回1即可。
最后定义目录类
public class Directory extends FileSystemNode {
private List<FileSystemNode> subNodes = new ArrayList<>();
public Directory(String path) {
super(path);
}
@Override
public int countNumOfFiles() {
int numOfFiles = 0;
for (FileSystemNode subNode : subNodes) {
numOfFiles += subNode.countNumOfFiles();
}
return numOfFiles;
}
public void addSubNode(FileSystemNode fileOrDir) {
subNodes.add(fileOrDir);
}
/**
* 删除子节点
*/
public void removeSubNode(FileSystemNode fileOrDir) {
int size = subNodes.size();
int i = 0;
for (; i < size; ++i) {
if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
break;
}
}
if (i < size) {
subNodes.remove(i);
}
}
}
目录节点根节点,它维护一个list来存子节点,子节点也是FileSystemNode类。
在统计文件个数countNumOfFiles的时候,如果是文件节点,直接遍历其子节点的countNumOfFiles方法即可。
根节点还维护了添加子节点和删除子节点的方法。
客户端调用:
public static void main(String[] args) {
/**
* /
* /wz/
* /wz/a.txt
* /wz/b.txt
* /wz/movies/
* /wz/movies/c.avi
* /xzg/
* /xzg/docs/
* /xzg/docs/d.txt
*/
Directory fileSystemTree = new Directory("/");
Directory node_wz = new Directory("/wz/");
Directory node_xzg = new Directory("/xzg/");
fileSystemTree.addSubNode(node_wz);
fileSystemTree.addSubNode(node_xzg);
File node_wz_a = new File("/wz/a.txt");
File node_wz_b = new File("/wz/b.txt");
Directory node_wz_movies = new Directory("/wz/movies/");
node_wz.addSubNode(node_wz_a);
node_wz.addSubNode(node_wz_b);
node_wz.addSubNode(node_wz_movies);
File node_wz_movies_c = new File("/wz/movies/c.avi");
node_wz_movies.addSubNode(node_wz_movies_c);
Directory node_xzg_docs = new Directory("/xzg/docs/");
node_xzg.addSubNode(node_xzg_docs);
File node_xzg_docs_d = new File("/xzg/docs/d.txt");
node_xzg_docs.addSubNode(node_xzg_docs_d);
System.out.println("/ files num:" + fileSystemTree.countNumOfFiles());
System.out.println("/wz/ files num:" + node_wz.countNumOfFiles());
}
输出:
/ files num:4
/wz/ files num:3
组合模式把具有层级关系的树节点用一个统一的数据结构表示(FileSystemNode),方便扩展(扩展新的类只要继承或者实现统一的抽象即可),代码可读性强。
练习2:用树结构表示xml或者html
首先抽象出一个节点的概念,表示xml的标签,它可以统一表示xml的标签,文本或者注释
public interface Node {
String toXml();
Node addNode(Node node);
}
文本的表示:
public class TextNode implements Node {
private String text;
public TextNode(String text) {
this.text = text;
}
public String getText() {
return text;
}
@Override
public String toXml() {
return text + "
";
}
@Override
public Node addNode(Node node) {
throw new UnsupportedOperationException();
}
}
文本的toXml,直接返回自身的text内容即可;文本节点不能再addNode,抛异常
标签的表示:
public class ElementNode implements Node {
private String name;
private List<Node> childList = new ArrayList<>();
public ElementNode(String name) {
this.name = name;
}
public String getNodeName() {
return this.name;
}
@Override
public String toXml() {
StringBuilder builder = new StringBuilder();
builder.append("<").append(this.name).append(">
");
for (Node child : this.childList) {
builder.append(child.toXml());
}
builder.append("</").append(this.name).append(">
");
return builder.toString();
}
@Override
public Node addNode(Node node) {
if (null == node) {
return null;
}
this.childList.add(node);
return this;
}
}
根节点需要维护一个子节点的list。 标签的toXml方法直接遍历这个list节点的toXml方法即可。
客户端调用:
public class Test {
public static void main(String[] args) {
ElementNode root = new ElementNode("root");
ElementNode html = new ElementNode("html");
ElementNode head = new ElementNode("head");
ElementNode body = new ElementNode("body");
TextNode textNode = new TextNode("This is a text!");
body.addNode(textNode);
html.addNode(head);
html.addNode(body);
root.addNode(html);
String s = root.toXml();
System.out.println(s);
}
}
输出:
<root>
<html>
<head>
</head>
<body>
This is a text!
</body>
</html>
</root>
如果想增加一个注释节点(CommentNode),只需要实现Node接口即可
总结
组合模式适合于树结构的表示,能够方便树结构的操作(遍历),能够提供更好的扩展性,且代码更加易读。Tomcat的多级容器实现的Container接口也是组合模式的实现。