设计模式之组合模式

简介: 设计模式之组合模式

一、介绍

组合模式(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();
}

运行代码,输出如下:

结果1.jpg

以上就是组合模式的案例演示,希望通过本篇文章的阅读,能使各位朋友对组合模式有更深入的理解。

四、案例二

以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等)负责实现功能的具体逻辑。

  • 在忽略标签中各种属性(如:idnamestyle等)等情况下,每一个标签只保留一个最基本功能:添加子标签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图:

组合模式UML图.png

2. 不同标签的实现

上面我们已经对标签的抽象接口中的方法进行分析,结合该案例中的html文本,我们不难知道需要以下标签的实现类:html标签的实现类HtmlNodehead标签的实现类HeadNodebody标签的实现类BodyNodeScriptNodeDivNodeH1NodeH2NodePNode。下面我们分别实现:

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());
    }
}

下面我们看一下输出的结果,如下图所示

结果二.jpg

从输出中可以看到该结果与案例中的html完全一致。而我们只是对该树形结构的根节点进行方法调用,根据组合模式的特点获得整个树形结构的输出结果。

将该文本通过浏览器打开,效果如下图所示

html文本效果.jpg

五、使用场景

组合模式常常适用于树形结构(整体-部分)的数据中,如多级菜单文件夹和文件等等。




纸上得来终觉浅,绝知此事要躬行。

————————我是万万岁,我们下期再见————————

相关文章
|
4月前
|
设计模式
二十三种设计模式全面解析-解密组合模式(Composite Pattern):构建统一而强大的对象结构
二十三种设计模式全面解析-解密组合模式(Composite Pattern):构建统一而强大的对象结构
|
6月前
|
设计模式 存储 安全
结构型设计模式05-组合模式
结构型设计模式05-组合模式
18 0
|
4月前
|
设计模式
二十三种设计模式全面解析-组合模式与享元模式的结合应用:实现对象的共享和高效管理
二十三种设计模式全面解析-组合模式与享元模式的结合应用:实现对象的共享和高效管理
|
4月前
|
设计模式
二十三种设计模式全面解析-组合模式与迭代器模式的结合应用:构建灵活可扩展的对象结构
二十三种设计模式全面解析-组合模式与迭代器模式的结合应用:构建灵活可扩展的对象结构
|
7天前
|
设计模式 Java 容器
【设计模式系列笔记】组合模式
组合模式(Composite Pattern)是一种结构型设计模式,它允许将对象组合成树状结构以表示部分-整体的层次结构。组合模式使得客户端可以统一处理单个对象和对象组合,而无需区分它们的类型。
38 12
|
16天前
|
设计模式 Java
小谈设计模式(20)—组合模式
小谈设计模式(20)—组合模式
|
4月前
|
设计模式 Java
Java设计模式【九】:组合模式
Java设计模式【九】:组合模式
30 0
|
1月前
|
设计模式 存储 安全
【设计模式】组合模式
【设计模式】组合模式
|
2月前
|
设计模式 Java
浅谈设计模式 - 组合模式(十二)
浅谈设计模式 - 组合模式(十二)
51 0
|
3月前
|
设计模式 存储 安全
聊聊Java设计模式-组合模式
组合(Composite)模式,又叫做树形模式,主要用来处理树形结构数据。是将一组对象组织成树形结构,以表示一种“部分-整体”的层次结构。让客户端可以统一单个对象和组合对象的处理逻辑
29 1
聊聊Java设计模式-组合模式