一、介绍
组合模式(Composite Pattern),属于结构型设计模式。组合模式常用于树形的数据结构,比如:多级菜单、部门层级关系、html文本中的dom树。它的特点是使用户对单个对象和组合对象的使用是相同的,也就是说,使用组合模式可以把一个子节点与其父节点统一处理。当我们对一个节点按照某种逻辑进行处理时,与此同时,会以类似递归的形式对其子节点按照相同的逻辑进行处理。
该设计模式主要角色就两种:①抽象接口,②实现类。
二、组合模式中的角色
抽象类
在组合模式中,由于类与类之间的结构是树形结构(即上下级关系),因此我们可以对这些类进行无差别地抽象出一个接口类,用于定义各个类的行为。
实现类
抽象类中定义了行为,因此我们可以对该抽象类定义不同的子类,对该行为实现不同的逻辑。
组合类
对不同的实现类进行实例化后,按照树形的结构对其进行组合。当对一个节点调用抽象类定义的方法时,按照类似递归的方式,也对其所有子孙节点进行调用。以实现对父节点和子节点的统一处理。
三、案例一
我们以公司员工为例,不同的员工可向下管理多个员工,而每一个员工都有一个共同的动作:领工资。
根据该案例描述,我们对员工进行以下定义
public interface Employ {
/**
* 领工资
*/
void getSalary();
/**
* 添加员工
*/
void addEmployee(Employ employ);
/**
* 获取当前员工可管理的员工
*/
List<Employ> children();
}
添加员工实现类(EmployImpl):
public class EmployImpl implements Employ {
// 员工姓名
private String name;
// 员工工资
private String salary;
// 可管理的员工集合
private List<Employ> employList = new ArrayList<>();
// 通过姓名和薪资实例化一个员工
public EmployImpl(String name, String salary) {
this.name = name;
this.salary = salary;
}
// 领工资
@Override
public void getSalary() {
System.out.println("员工姓名:" + name + ",领取工资:" + salary);
// 管理的员工集合也领工资
for (Employ employ : employList) {
employ.getSalary();
}
}
// 添加一个员工
@Override
public void addEmployee(Employ employ) {
employList.add(employ);
}
// 获取管理的员工集合
@Override
public List<Employ> children() {
return employList;
}
@Override
public String toString() {
return name;
}
}
下面我们通过代码进行测试,首先定义了6个员工,然后通过addEmployee()
方法对员工的层级结构进行分配,最后通过查询指定员工可管理的员工集合(children)、并对其中一个员工发工资(getSalary()
)向下逐级发工资。通过这种类似蝴蝶效应的连锁反应演示组合模式的精髓(只不过连锁反应中所有反应都相同罢了)。
public static void main(String[] args) {
Employ tom = new EmployImpl("汤姆", "22");
Employ jerry = new EmployImpl("杰瑞", "33");
Employ jack = new EmployImpl("杰克", "44");
Employ rose = new EmployImpl("肉丝", "55");
Employ diJia = new EmployImpl("迪迦", "66");
Employ taiLuo = new EmployImpl("泰罗", "77");
// 汤姆管理杰瑞、杰克
tom.addEmployee(jerry);
tom.addEmployee(jack);
// 杰瑞管理肉丝
jerry.addEmployee(rose);
// 杰克管理迪迦、泰罗
jack.addEmployee(diJia);
jack.addEmployee(taiLuo);
List<Employ> children1 = tom.children();
System.out.println("汤姆管理的员工:" + children1);
List<Employ> children2 = jerry.children();
System.out.println("杰瑞管理的员工:" + children2);
List<Employ> children3 = jack.children();
System.out.println("杰克管理的员工:" + children3);
List<Employ> children4 = rose.children();
System.out.println("肉丝管理的员工:" + children4);
List<Employ> children5 = diJia.children();
System.out.println("迪迦管理的员工:" + children5);
List<Employ> children6 = taiLuo.children();
System.out.println("泰罗管理的员工:" + children6);
tom.getSalary();
}
运行代码,输出如下:
以上就是组合模式的案例演示,希望通过本篇文章的阅读,能使各位朋友对组合模式有更深入的理解。
四、案例二
以html为例,从根节点<html>
开始,每个节点都可能包含任意个其他节点,这些层层嵌套的节点就构成了一棵dom树,下面是一个简单的dom树,我们利用组合模式实现对html文本的转换。
<html>
<head>
<script></script>
</head>
<body>
<div>
<h1>一级标题</h1>
</div>
<div>
<h2>二级标题</h2>
<p>一个文本</p>
</div>
</body>
</html>
1. 案例分析
从上面html例子中我们看到一个树形结构的html文本,该文本由多个标签组成(如:html、head、script、body、div、h1、h2、p),因此我们可以将这些标签抽象为一个接口类(Node),接口类负责定义标签的功能,其实现类(HtmlNode、BodyNode、PNode等)负责实现功能的具体逻辑。
在忽略标签中各种属性(如:id、name、style等)等情况下,每一个标签只保留一个最基本功能:添加子标签
addNode()
,因为我们要尽量对每一个标签的功能做最简单的处理。为了将不同的标签类转为对应的html文本,例如:将div标签的类DivNode转为对应的html文本
<div></div>
,因此每一个标签实现类应具有一个转为html文本的功能toHtml()
。- 由于每一个标签可能包含多个子标签,我们在实现标签的转为html文本功能时,还需要获取其所有的子标签,将子标签转为html文本后作为当前标签的文本,因此还需要一个获取当前标签所有子标签的功能
children()
。
根据以上分析,我们可以对标签的抽象接口类Node
做出以下定义:
public interface Node {
/** 添加子结点 */
void addNode(Node node);
/** 输出所有子节点 */
List<Node> children();
/** 以当前节点为根节点,转为html文本 */
String toHtml();
}
然后不同的标签可以定义不同的实现类来实现对应的功能逻辑。如div标签对应的实现类DivNode、body比起爱安对应的实现类BodyNode。
我们大致可以对该案例的结构得出以下UML图:
2. 不同标签的实现
上面我们已经对标签的抽象接口中的方法进行分析,结合该案例中的html文本,我们不难知道需要以下标签的实现类:html标签的实现类HtmlNode
、head标签的实现类HeadNode
、body标签的实现类BodyNode
、ScriptNode
、DivNode
、H1Node
、H2Node
、PNode
。下面我们分别实现:
①html标签的实现类HtmlNode
public class HtmlNode implements Node {
private final List<Node> children = new ArrayList<>();
/** 添加子结点 */
@Override
public void addNode(Node node) {
children.add(node);
}
/** 输出所有子节点 */
@Override
public List<Node> children() {
return children;
}
/** 以当前节点为根节点,转为html文本 */
@Override
public String toHtml() {
String start = "<html>";
String end = "</html>";
// 遍历子节点,并将子节点的html文本进行拼接,作为当前标签的文本
StringBuilder sb = new StringBuilder();
for (Node child : children) {
sb.append(child.toHtml());
}
return start + sb.toString() + end;
}
}
②head标签的实现类HeadNode
public class HeadNode implements Node {
private final List<Node> children = new ArrayList<>();
/** 添加子结点 */
@Override
public void addNode(Node node) {
children.add(node);
}
/** 输出所有子节点 */
@Override
public List<Node> children() {
return children;
}
/** 以当前节点为根节点,转为html文本 */
@Override
public String toHtml() {
String start = "<head>";
String end = "</head>";
// 遍历子节点,并将子节点的html文本进行拼接,作为当前标签的文本
StringBuilder sb = new StringBuilder();
for (Node child : children) {
sb.append(child.toHtml());
}
return start + sb.toString() + end;
}
}
③body标签的实现类BodyNode
public class BodyNode implements Node {
private final List<Node> children = new ArrayList<>();
/** 添加子结点 */
@Override
public void addNode(Node node) {
children.add(node);
}
/** 输出所有子节点 */
@Override
public List<Node> children() {
return children;
}
/** 以当前节点为根节点,转为html文本 */
@Override
public String toHtml() {
String start = "<body>";
String end = "</body>";
// 遍历子节点,并将子节点的html文本进行拼接,作为当前标签的文本
StringBuilder sb = new StringBuilder();
for (Node child : children) {
sb.append(child.toHtml());
}
return start + sb.toString() + end;
}
}
④script标签的实现类ScriptNode
script标签不存在子节点,因此在addNode()
方法中做出特别处理,当然也可以直接抛出异常。
public class ScriptNode implements Node {
/** 添加子结点 */
@Override
public void addNode(Node node) {
System.out.println("不允许添加子标签,当前操作被忽略");
}
/** 输出所有子节点 */
@Override
public List<Node> children() {
return new ArrayList<>();
}
/** 以当前节点为根节点,转为html文本 */
@Override
public String toHtml() {
String start = "<script>";
String end = "</script>";
return start + end;
}
}
⑤div标签的实现类DivNode
public class DivNode implements Node {
private final List<Node> children = new ArrayList<>();
/** 添加子结点 */
@Override
public void addNode(Node node) {
children.add(node);
}
/** 输出所有子节点 */
@Override
public List<Node> children() {
return children;
}
/** 以当前节点为根节点,转为html文本 */
@Override
public String toHtml() {
String start = "<div>";
String end = "</div>";
// 遍历子节点,并将子节点的html文本进行拼接,作为当前标签的文本
StringBuilder sb = new StringBuilder();
for (Node child : children) {
sb.append(child.toHtml());
}
return start + sb.toString() + end;
}
}
⑥h1标签的实现类H1Node
h1标签不存在子节点,因此在addNode()
方法中做出特别处理,当然也可以直接抛出异常。并且通常情况下h1标签内包含一段文本作为标题,因此我们通过构造方法定义该标题文本。
public class H1Node implements Node {
private final String text;
public H1Node(String text) {
this.text = text;
}
/** 添加子结点 */
@Override
public void addNode(Node node) {
System.out.println("不允许添加子标签,当前操作被忽略");
}
/** 输出所有子节点 */
@Override
public List<Node> children() {
return new ArrayList<>();
}
/** 以当前节点为根节点,转为html文本 */
@Override
public String toHtml() {
String start = "<h1>";
String end = "</h1>";
return start + text + end;
}
}
⑦h2标签的实现类H2Node
h2标签不存在子节点,因此在addNode()
方法中做出特别处理,当然也可以直接抛出异常。并且通常情况下h2标签内包含一段文本作为标题,因此我们通过构造方法定义该标题文本。
public class H2Node implements Node {
private final String text;
public H2Node(String text) {
this.text = text;
}
/** 添加子结点 */
@Override
public void addNode(Node node) {
System.out.println("不允许添加子标签,当前操作被忽略");
}
/** 输出所有子节点 */
@Override
public List<Node> children() {
return new ArrayList<>();
}
/** 以当前节点为根节点,转为html文本 */
@Override
public String toHtml() {
String start = "<h2>";
String end = "</h2>";
return start + text + end;
}
}
⑧p标签的实现类PNode
p标签不存在子节点,因此在addNode()
方法中做出特别处理,当然也可以直接抛出异常。
public class PNode implements Node {
private final String text;
public PNode(String text) {
this.text = text;
}
/** 添加子结点 */
@Override
public void addNode(Node node) {
System.out.println("不允许添加子标签,当前操作被忽略");
}
/** 输出所有子节点 */
@Override
public List<Node> children() {
return new ArrayList<>();
}
/** 以当前节点为根节点,转为html文本 */
@Override
public String toHtml() {
String start = "<p>";
String end = "</p>";
return start + text + end;
}
}
3. 案例演示
新建一个demo类CompositeTest
,根据案例中示例的html文本,通过实例化不同的标签实现类构造出一致的树形结构。
public class CompositeTest {
public static void main(String[] args) {
// html标签
Node html = new HtmlNode();
// html标签中添加head标签
Node head = new HeadNode();
html.addNode(head);
// head标签中添加script标签
Node script = new ScriptNode();
head.addNode(script);
// html标签中添加body标签
Node body = new BodyNode();
html.addNode(body);
// body标签中添加两个div标签
Node div1 = new DivNode();
Node div2 = new DivNode();
body.addNode(div1);
body.addNode(div2);
// 第一个div中添加一个p标签
div1.addNode(new H1Node("一级标题"));
// 第二个div中添加两个p标签
div2.addNode(new H2Node("二级标题"));
div2.addNode(new PNode("一个文本"));
System.out.println(html.toHtml());
}
}
下面我们看一下输出的结果,如下图所示
从输出中可以看到该结果与案例中的html完全一致。而我们只是对该树形结构的根节点进行方法调用,根据组合模式的特点获得整个树形结构的输出结果。
将该文本通过浏览器打开,效果如下图所示
五、使用场景
组合模式常常适用于树形结构(整体-部分)的数据中,如多级菜单、文件夹和文件等等。
纸上得来终觉浅,绝知此事要躬行。
————————我是万万岁,我们下期再见————————