• 设计模式学习笔记(十一):组合模式


    1 概述

    1.1 引言

    对于树形结构,比如文件目录,一个文件夹中可以包含多个文件夹和文件,而一个文件中不能在包含子文件或者子文件夹,在这里可以称文件夹为容器,称文件为叶子

    在树形结构中,当容器对象(比如文件夹)的某个方法被调用时,将遍历整个文件夹,寻找也包含这个方法的成员对象(容器对象或叶子对象)并调用执行。由于容器对象以及叶子对象在功能上的区别,使用这些对象的代码中必须有区别对待容器对象以及叶子对象,但大多数情况下需要一致性处理它们。

    组合模式为解决此类问题而生,它可以让叶子对象以及容器对象的使用具有一致性。

    1.2 定义

    组合模式:组合多个对象形成树形结构以表示具有“整体-部分”关系的层次结构。组合模式对单个对象(叶子对象)和组合对象(容器对象)的使用具有一致性。

    组合模式又叫“部分-整体”模式,它是一种对象结构型模式。

    1.3 结构图

    在这里插入图片描述

    1.4 角色

    • Component(抽象构件):可以是接口或者抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问以及管理它的子构件的方法,例如增加/删除/获取子构件
    • Leaf(叶子构件):表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为,对于访问以及管理子构件的方法,通常会抛出异常
    • Composite(容器构件):表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括访问以及管理子构件的方法

    2 典型实现

    2.1 步骤

    组合模式的关键是定义了一个抽象构件类,它既可以表示叶子也可以表示容器,客户端针对该抽象构件进行编程,无须知道到底是叶子还是容器,同时容器对象与抽象构件之间需要建立一个聚合关联关系,在容器对象中既可以包含叶子也可以包含容器,以此实现递归组合,形成树形结构。

    因此首先需要定义抽象构件类,通用步骤如下:

    • 定义抽象构件:定义抽象构件类,添加四个基本方法:增加/删除/获取成员+业务方法,可以将抽象构件类定义为抽象类或者接口
    • 定义叶子构件:继承或实现抽象构件类,覆盖或实现具体业务方法,同时对于管理或访问子构件的方法提供异常处理或错误提示
    • 定义容器构件:继承或实现抽象构件类,覆盖或实现抽象构件中的所有方法,一般来说容器构件会包含一个集合私有成员用于保存抽象构件,在业务方法中对这个集合进行遍历从而实现递归调用

    2.2 抽象构件

    抽象构件一般定义如下:

    abstract class Component
    {
        abstract void add(Component c);
        abstract void remove(Component c);
        abstract Component getChild(int i);
        abstract void operation();
    }
    

    2.3 叶子构件

    class Leaf extends Component
    {
        public void add(Component c)
        {
            //叶子构件不能访问该方法
            System.out.println("错误,不能访问添加构件方法!");
        }
    
        public void remove(Component c)
        {
            //叶子构件不能访问该方法
            System.out.println("错误,不能访问删除构件方法!");
        }
    
        public Component getChild(int i)
        {
            //叶子构件不能访问该方法
            System.out.println("错误,不能访问获取构件方法!");
            return null;
        }
    
        public void operation()
        {
            System.out.println("叶子业务方法");
        }
    }
    

    叶子构件只需要覆盖具体业务方法opeartion,对于管理子构件的方法可以提示错误或者抛出异常来处理。

    2.4 容器构件

    class Composite extends Component
    {
        private ArrayList<Component> list = new ArrayList<>();
        
        public void add(Component c)
        {
            list.add(c);
        }
    
        public void remove(Component c)
        {
            list.remove(c);
        }
    
        public Component getChild(int i)
        {
            return list.get(i);
        }
    
        public void operation()
        {
    		list.forEach(Component::operation);
        }
    }
    

    容器构件只需要简单实现管理子构件的方法,对于业务方法一般需要对抽象构件集合进行遍历来实现递归调用。

    2.5 客户端

    客户端针对抽象构件进行编程,根据需要添加叶子或者容器:

    public static void main(String[] args) 
    {
        Component leaf1 = new Leaf();
        Component leaf2 = new Leaf();
        Component composite1 = new Composite();
        Component composite2 = new Composite();
    
        composite1.add(leaf1);
        composite2.add(leaf2);
        composite1.add(composite2);
    
        composite1.operation();
    }
    

    3 实例

    开发一个杀毒软件系统,可以对某个文件夹或单个文件进行杀毒,还能根据文件类型的不同提供不同的杀毒方式,比如文本文件和图像文件的杀毒方式有所差异,使用组合模式对该系统进行设计。

    设计如下:

    • 抽象构件类:AbstractFile
    • 容器构件类:Folder
    • 叶子构件类:ImageFile+TextFile+VideoFile

    代码如下:

    public class Test
    {
        public static void main(String[] args) {
            AbstractFile file1,file2,file3,file4,folder1,folder2;
            file1 = new ImageFile("图像文件1号");
            file2 = new VideoFile("视频文件1号");
            file3 = new TextFile("文本文件1号");
            file4 = new ImageFile("图像文件2号");
    
            folder1 = new Folder("文件夹1");
            folder2 = new Folder("文件夹2");
    
            try
            {
                folder2.add(file1);
                folder2.add(file2);
                folder2.add(file3);
                folder1.add(file4);
                folder1.add(folder2);
            }
            catch(IllegalAccessException e)
            {
                e.printStackTrace();
            }
            
            folder1.killVirus();
            System.out.println();
            folder2.killVirus();
        }
    }
    
    //抽象构件类
    abstract class AbstractFile
    {
        protected String name;
        abstract void add(AbstractFile file) throws IllegalAccessException;
        abstract void remove(AbstractFile file) throws IllegalAccessException;
        abstract AbstractFile getChild(int i) throws IllegalAccessException;
        public void killVirus()
        {
            System.out.println(name+" 杀毒");
        }
    }
    
    //叶子构件类
    class ImageFile extends AbstractFile
    {
        public ImageFile(String name)
        {
            this.name = name;
        }
    
        public void add(AbstractFile c)
        {
            throw new IllegalAccessException("错误,不能访问添加文件方法!");
        }
    
        public void remove(AbstractFile c)
        {
            throw new IllegalAccessException("错误,不能访问删除文件方法!");
        }
    
        public AbstractFile getChild(int i)
        {
            throw new IllegalAccessException("错误,不能访问获取文件方法!");
        }
    }
    
    //叶子构件类
    class TextFile extends AbstractFile
    {
        public TextFile(String name)
        {
            this.name = name;
        }
    
        public void add(AbstractFile c)
        {
            throw new IllegalAccessException("错误,不能访问添加文件方法!");
        }
    
        public void remove(AbstractFile c)
        {
            throw new IllegalAccessException("错误,不能访问删除文件方法!");
        }
    
        public AbstractFile getChild(int i)
        {
            throw new IllegalAccessException("错误,不能访问获取文件方法!");
        }
    }
    
    //叶子构件类
    class VideoFile extends AbstractFile
    {
        public VideoFile(String name)
        {
            this.name = name;
        }
    
    	public void add(AbstractFile c)
        {
            throw new IllegalAccessException("错误,不能访问添加文件方法!");
        }
    
        public void remove(AbstractFile c)
        {
            throw new IllegalAccessException("错误,不能访问删除文件方法!");
        }
    
        public AbstractFile getChild(int i)
        {
            throw new IllegalAccessException("错误,不能访问获取文件方法!");
        }
    }
    
    //容器构件类
    class Folder extends AbstractFile
    {
        private ArrayList<AbstractFile> list = new ArrayList<>();
        public Folder(String name)
        {
            this.name = name;
        }
    
        public void add(AbstractFile c)
        {
            list.add(c);
        }
    
        public void remove(AbstractFile c)
        {
            list.remove(c);
        }
    
        public AbstractFile getChild(int i)
        {
            return list.get(i);
        }
    
        public void killVirus()
        {
            System.out.println("对 "+name+" 进行杀毒");
            list.forEach(AbstractFile::killVirus);
        }
    }
    

    输出如下:
    在这里插入图片描述

    4 透明组合模式与安全组合模式

    4.1 如何简化代码

    尽管组合模式的扩展性好,在上面的例子中增加新的文件类型无须修改原有代码,但是,由于抽象构件类AbstractFile声明了与叶子构件无关的构件管理方法,因此 需要实现这些方法,这样就会带来很多重复性的工作。

    解决方案有两个:

    • 抽象构件提供默认实现:叶子构件中的构件管理方法转移到抽象构件中提供默认实现
    • 抽象构件删除方法:在抽象构件中不提供管理构件的方法

    4.2 默认实现

    如果使用抽象构件提供默认实现的方法,则上述例子代码简化如下:

    abstract class AbstractFile
    {
        protected String name;
        public AbstractFile(String name)
        {
            this.name = name;
        }
        public void add(AbstractFile file) throws IllegalAccessException
        {
            throw new IllegalAccessException("错误,不能访问添加文件方法!");
        }
        public void remove(AbstractFile file) throws IllegalAccessException
        {
            throw new IllegalAccessException("错误,不能访问删除文件方法!");
        }
        public AbstractFile getChild(int i) throws IllegalAccessException
        {
            throw new IllegalAccessException("错误,不能访问获取文件方法!");
        }
        public void killVirus()
        {
            System.out.println(name+" 杀毒");
        }
    }
    
    class ImageFile extends AbstractFile
    {
        public ImageFile(String name)
        {
            super(name);
        }
    }
    
    class TextFile extends AbstractFile
    {
        public TextFile(String name)
        {
            super(name);
        }
    }
    
    class VideoFile extends AbstractFile
    {
        public VideoFile(String name)
        {
            super(name);
        }
    }
    

    在叶子构件中只有构造方法(实际上业务方法应该是抽象的,在叶子构件中实现业务方法,这里的业务方法是killVirus(),这里是进行了简化),这样修改虽然简化了代码,但是总的来说为叶子构件提供这些方法是没有意义的,因为叶子不会再下一个层次的对象,这在编译阶段不会出错 ,但是在运行阶段可能会出错。

    4.3 删除方法

    如果使用抽象构件删除方法的方式进行简化代码,则上述例子简化如下:

    abstract class AbstractFile
    {
        protected String name;
        public AbstractFile(String name)
        {
            this.name = name;
        }
        abstract void killVirus();
    }
    
    class ImageFile extends AbstractFile
    {
        public ImageFile(String name)
        {
            super(name);
        }
        public void killVirus()
        {
            System.out.println("图像文件"+name+"杀毒");
        }
    }
    
    class TextFile extends AbstractFile
    {
        public TextFile(String name)
        {
            super(name);
        }
        public void killVirus()
        {
            System.out.println("文本文件"+name+"杀毒");
        }
    }
    
    class VideoFile extends AbstractFile
    {
        public VideoFile(String name)
        {
            super(name);
        }
        public void killVirus()
        {
            System.out.println("视频文件"+name+"杀毒");
        }
    }
    

    这样做叶子构件就无法访问管理构件的方法了,但是带来的坏处是客户端无法统一针对抽象构件类AbstractFile进行编程,修改之前代码如下:

    AbstractFile file1,file2,file3,file4,folder1,folder2;
    

    由于AbstractFile中删除了管理构件方法,因此客户端需要修改代码如下:

    AbstractFile file1,file2,file3,file4;
    Folder folder1,folder2;
    

    4.4 透明组合模式

    透明组合模式就是第一种解决方案中的方法,在抽象构件中声明所有用于管理构件的方法,这样做的好处是确保所有的构件类都具有相同的接口,客户端可以针对抽象构件进行统一编程,结构图如下:

    在这里插入图片描述

    透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的。叶子对象不可能有下一层次的对象,提供管理构件的方法是没有意义的,在编译阶段不会报错,但是在运行阶段可能会出错。

    4.5 安全组合模式

    安全组合模式就是第二种方法的办法,安全组合模式中,抽象构件没有声明管理构件的方法,而是在容器构件中添加管理构件的方法,这种做法是安全的因为叶子对象不可能调用到这些方法。结构图如下:

    在这里插入图片描述

    安全组合模式的缺点是不够透明,因为叶子构件与容器构件具有不同的方法,管理构件的方法在容器构件中定义,客户端不能完全针对抽象构件进行编程,必须有区别地对待叶子构件与容器构件。

    5 主要优点

    • 层次控制:组合模式可以清楚定义分层次的复杂对象,表示对象的全部或者部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制
    • 一致使用构件:客户端可以一致地使用容器构件或者叶子构件,也就是能针对构件抽象层一致性编程
    • 扩展性好:增加新的容器构件或者叶子构件都很方便,符合开闭原则
    • 有效针对树形结构:组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子构件与容器构件的递归组合,可以形成复杂的树形结构,但控制树形结构却很简单

    6 主要缺点

    • 难以限制构件类型:增加新构件时难以限制构件类型,比如希望容器构件中只有某一特定类型的叶子构件,例如一个只能包含图片的文件夹,使用组合模式时不能依赖类型系统来施加这些约束,需要再运行时进行类型检查来实现,过程较为复杂

    7 适用场景

    • 具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致性对待它们
    • 处理树形结构
    • 系统中能够分离出叶子构件以及容器构件,而且类型不固定,需要增加新的叶子构件或者容器构件

    8 总结

    在这里插入图片描述

    如果觉得文章好看,欢迎点赞。

    同时欢迎关注微信公众号:氷泠之路。

    在这里插入图片描述

  • 相关阅读:
    3 聊天室
    freemaker分页模板
    [三]ajax重要属性
    [二]java运行原理
    [转]freemaker格式化日期
    [二]SpringMvc实践-注解
    win7下禁用ctrl alt del +上下左右键
    [一]初识SpringMVC
    Powerdesigner设置name与code不同时变化
    Ubuntu下缓冲器溢出攻击实验(可以看看问题分析)
  • 原文地址:https://www.cnblogs.com/6b7b5fc3/p/13379456.html
Copyright © 2020-2023  润新知