结构篇-组合模式

Posted zhixuChen333

tags:

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

文章目录


组合模式(Composite)是针对由多个节点对象(部分)组成的树形结构的对象(整体)而发展出的一种结构型设计模式,它能够使客户端在操作整体对象或者其下的每个节点对象时做出统一的响应,保证树形结构对象使用方法的一致性,使客户端不必关注对象的整体或部分,最终达到对象复杂的层次结构与客户端解耦的目的。


提示:以下是本篇文章正文内容,下面案例可供参考

一、叉树结构

在现实世界中,某些具有从属关系的事物之间存在着一定的相似性。大家一定见过蕨类植物的叶子吧。从宏观上看,这只是一片普通的叶子,当继续观察其中一个分支的时候,我们会发现这个分支其实又是一片全新的叶子,当我们再继续观察这片新叶子的一个分支的时候,又会得到相同的结果。

不管是二叉树还是多叉树,道理都是一样的。无论数据元素是“根”“枝”,还是“叶”,甚至是整体的树,都具有类似的结构。具体来讲,除了叶节点没有子节点,其他节点都具有本级对象包含多个次级子对象的结构特征。所以,我们完全没有必要为每个节点对象定义不同的类(如为字、词、句、段、节、章……等每个节点都定义一个类),否则会造成代码冗余。我们可以用组合模式来表达“部分/整体”的层次结构,提取并抽象其相同的部分,特殊化其不同的部分,以提高系统的可复用性与可扩展性,最终达到以不变应万变的目的。

二、文件系统

通过对叉树结构的观察,我们发现,无论拿出哪一个“部分”,其与“整体”的结构都是类似的,所以首先我们需要模糊根、枝、叶之间的差异,以实现节点的统一。下面开始代码实战部分,我们就以类似于树结构的文件系统的目录结构为例。

1. 抽象节点类Node

文件系统从根目录“C:”开始分支,其下级可以包含“文件夹”或者“文件”,其中文件夹属于“枝”节点,其下级可以继续存放子文件夹或文件,而文件则属于“叶”节点,其下级不再有任何子节点。基于此前的分析,我们可以定义一个抽象的“节点”类来模糊“文件夹”与“文件”。

public abstract class Node 
    // 节点命名
    protected String name;

    public Node(String name) 
        this.name = name;
    

    //添加下级子节点方法
    protected abstract void add(Node child);

说明:

  1. 文件夹或文件都有一个名字,所以在构造方法中接收并初始化已定义的节点名,否则不允许节点被创建,这也是可以固化下来的逻辑。对于如何实现代码添加子节点方法add(Node child)暂时还不能确定,所以我们声明其为抽象方法,模糊此行为并留给子类去实现。需要注意的是,对于抽象节点类Node的抽象方法其实还可以更加丰富,例如“删除节点”“获取节点”等,这里为了简化代码只声明了“添加节点”方法。

2.文件夹类

public class Folder extends Node
    // 文件夹可以包含子节点(子文件夹或者文件)
    private List<Node> childrenNodes = new ArrayList<>();

    public Folder(String name) 
        super(name);
    

    @Override
    protected void add(Node child) 
        // 可以添加子节点
        childrenNodes.add(child);
    

说明:

  1. 首先,文件夹类继承了抽象节点类Node,并在第3行定义了一个次级节点列表List,此处的泛型Node既可以是文件夹又可以是文件,也就是说,文件夹下级可以包含任意多个文件夹或者文件。

3.文件类

public class File extends Node

    public File(String name) 
        super(name);
    

    @Override
    protected void add(Node child) 
        System.out.println("不能添加子节点");
    


说明:

  1. 除了添加子节点方法add(Node child),文件类与文件夹类的代码大同小异。如之前提到的,文件属于“叶”节点,不能再将这种结构延续下去,所以我们在方法输出一个错误消息,告知用户“不能添加子节点”。其实更好的方式是以抛出异常的形式来确保此处逻辑的正确性,外部如果捕获到该异常则可以做出相应的处理。

4.客户端类

public class Client 
    public static void main(String[] args) 
        Node d = new Folder("D盘");

        Node doc = new Folder("文档");
        doc.add(new File("简历.doc"));
        doc.add(new File("项目介绍.ppt"));

        d.add(doc);

        Node music = new Folder("音乐");

        Node jay = new Folder("周杰伦");
        jay.add(new File("轨迹.mp3"));
        jay.add(new File("说好不哭.mp3"));
        jay.add(new File("半岛铁盒.mp3"));

        Node eason = new Folder("陈奕迅");
        eason.add(new File("听阴天说什么"));
        eason.add(new File("淘汰"));

        music.add(jay);
        music.add(eason);

        d.add(music);

    

说明:

  1. 正如我们规划文件时常做的操作,用户以“D盘”文件夹作为根节点构建了目录树,接着创建了“文档”和“音乐”两个文件夹作为“枝”节点,再将相应类型的文件分别置于相应的目录下,其中对音乐文件多加了一级文件夹来区分歌手,以便日后分类管理、查找。如此一来,只要能持有根节点对象“D盘”,就能延伸出整个目录。

三、目录树展示

1.添加展示方法

目录树虽已构建完成,但要体现出组合模式的优势还在于如何运用这个树结构。假如用户现在要查看当前根目录下的所有子目录及文件,这就需要分级展示整棵目录树,正如Windows系统的“tree”命令所实现的。要模拟这种树形展示方式,我们就得在输出节点名称(文件夹名/文件名)之前加上数个空格以表示不同层级,但具体加几个空格还是个未知数,需要根据具体的节点级别而定。而作为抽象节点类则不应考虑这些细节,而应先把这个未知数作为参数变量传入,我们来修改抽象节点类Node并加入展示方法,

public abstract class Node 
    // 节点命名
    protected String name;

    public Node(String name) 
        this.name = name;
    

    //添加下级子节点方法
    protected abstract void add(Node child);

    protected void tree(int space) 
        for (int i = 0; i < space; i++) 
            //先循环输出space个空格
            System.out.print(" ");
        
        //接着再输出自己的名字
        System.out.println(name);
    

    protected void tree()
        this.tree(0);
    

说明:

  1. 空格偏移量这个必传参数可能让用户非常困惑,或许我们可以为抽象节点类添加一个无参的展示方法“tree()”,在其内部调用“tree(0)”,如此一来就不再需要用户传入偏移量了,使用起来更加方便。
  2. 作为末端节点的文件类只需要输出space个空格再加上自己的名称即可,所以文件类直接继承使用父类的展示方法,不用改写。

2.改写文件夹类

文件夹类就比较特殊了,它不仅要先输出自己的名字,还要换行再逐个输出子节点的名字,并且要保证空格逐级递增。

public class Folder extends Node 
    // 文件夹可以包含子节点(子文件夹或者文件)
    private List<Node> childrenNodes = new ArrayList<>();

    public Folder(String name) 
        super(name);
    

    @Override
    protected void add(Node child) 
        // 可以添加子节点
        childrenNodes.add(child);
    

    @Override
    protected void tree(int space) 
        // 调用父类通用的tree
        super.tree(space);
        // 在循环子节点前,空格数要加1
        space++;
        for (Node childrenNode : childrenNodes) 
            childrenNode.tree(space);
        
    

说明:

  1. 文件夹类重写并覆盖了父类的tree()方法,首先调用父类的通用tree()方法输出本文件夹的名字。接下来的逻辑就非常有意思了,对于下一级的子节点我们需要依次输出,但前提是要把当前的空格数加1,如此一来子节点的位置会往右偏移一格,这样才能看起来像树形结构一样错落有致。可以看到,在循环体中我们直接调用了子节点的展示方法并把“加1”后的空格数传递给它即可完成展示。至于当前文件夹下的子节点到底是“文件夹”还是“文件”,我们完全不必操心,因为子节点们会使用自己的展示逻辑。如果它们还有下一级子节点,则与此处逻辑相同,继续循环,把逐级递增的空格数传递下去,直至抵达叶节点为止——始于“文件夹”而终于“文件”,非常完美的递归逻辑。

3.输出结果

直接在客户端中的D盘文件夹调用tree()方法及可

public class Client 
    public static void main(String[] args) 
        Node d = new Folder("D盘");
        ...
        ...
        ...
        d.tree();

    

输出结果:
D盘
 文档
  简历.doc
  项目介绍.ppt
 音乐
  周杰伦
   轨迹.mp3
   说好不哭.mp3
   半岛铁盒.mp3
  陈奕迅.mp3
   听阴天说什么.mp3
   淘汰.mp3

总结

提示:这里对文章进行总结:

  1. 组合模式将树形结构的特点发挥得淋漓尽致,作为最高层级抽象的抽象节点类(接口)泛化了所有节点类,使任何“整体”或“部分”达成统一,枝(根)节点与叶节点的多态化实现以及组合关系进一步勾勒出的树形结构,最终使用户操作一触即发,由“根”到“枝”再到“叶”,逐级递归,自动生成。
  2. 抽象工厂方法模式的各角色定义如下。
  • Component(组件接口):所有复合节点与叶节点的高层抽象,定义出需要对组件操作的接口标准。对应本章例程中的抽象节点类,具体使用接口还是抽象类需根据具体场景而定。
  • Composite(复合组件):包含多个子组件对象(可以是复合组件或叶端组件)的复合型组件,并实现组件接口中定义的操作方法。对应本章例程中作为“根节点/枝节点”的文件夹类。
  • Leaf(叶端组件):不包含子组件的终端组件,同样实现组件接口中定义的操作方法。对应本章例程中作为“叶节点”的文件类。
  • Client(客户端):按所需的层级关系部署相关对象并操作组件接口所定义的接口,即可遍历树结构上的所有组件。

以上是关于结构篇-组合模式的主要内容,如果未能解决你的问题,请参考以下文章

结构篇-组合模式

Java进阶篇设计模式之六 ----- 组合模式和过滤器模式

从零开始学习Java设计模式 | 结构型模式篇:组合模式

从零开始学习Java设计模式 | 结构型模式篇:组合模式

Java 设计模式——组合模式

Java 设计模式——组合模式