• Java 设计模式——组合模式


    1.概述

    在这里插入图片描述

    (1)大家对于上面这个图片肯定非常熟悉,上图我们可以看做是一个文件系统,对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。可以将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些成员对象即可是容器对象也可以是叶子对象。但是由于容器对象和叶子对象在功能上面的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户而已,它始终希望能够一致的对待容器对象和叶子对象。

    (2)组合模式 (Composite Pattern),又名部分整体模式,是一种结构型模式,它用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式创建了对象组的树形结构。

    2.结构

    组合模式主要包含以下三种角色:

    • 抽象构件 (Component):定义组合中对象的共有方法和属性,并可以提供默认的实现。它可以是抽象类或接口。
    • 容器构件 (Composite):表示组合中的容器节点对象,可以包含其他的子节点。实现了抽象构件的接口,并在容器中存储子节点。
    • 叶子构件 (Leaf):表示组合中的叶子节点对象,它没有子节点。实现了抽象构件的接口。

    3.实现

    【例】软件菜单:如下图,我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。

    在这里插入图片描述

    类图如下:
    在这里插入图片描述
    具体代码如下:

    3.1.抽象构件

    MenuComponent.java

    /*
    	不管是菜单还是菜单项,都应该继承自统一的接口,这里暂时将这个统一的接口称为菜单组件。
    	菜单组件,属于抽象根节点
    */
    public abstract class MenuComponent {
        //菜单组件的名称
        protected String name;
        //菜单组件的层级
        protected int level;
        
        //添加子菜单
        public void add(MenuComponent menuComponent){
            throw new UnsupportedOperationException();
        }
        
        //移除子菜单
        public void remove(MenuComponent menuComponent){
            throw new UnsupportedOperationException();
        }
        
        //获取指定的子菜单
        public MenuComponent getChild(int index){
            throw new UnsupportedOperationException();
        }
        
        //获取菜单或者菜单项的名称
        public String getName(){
            return name;
        }
        
        //打印菜名称的方法(包含子菜单和子菜单项)
        public abstract void print();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    这里的MenuComponent定义为抽象类,因为有一些共有的属性和行为要在该类中实现,Menu和MenuItem类就可以只覆盖自己感兴趣的方法,而不用搭理不需要或者不感兴趣的方法,举例来说,Menu类可以包含子菜单,因此需要覆盖add()、remove()、getChild()方法,但是MenuItem就不应该有这些方法。这里给出的默认实现是抛出异常,也可以根据自己的需要改写默认实现。

    3.2.容器构件

    Menu.java

    //菜单类,属于树枝节点
    public class Menu extends MenuComponent{
        
        //菜单可以有多个子菜单或者子菜单项
        private List<MenuComponent> menuComponentList = new ArrayList<MenuComponent>();
        
        //构造方法
        public Menu(String name,int level){
            this.name = name;
            this.level = level;
        }
        
        @Override
        public void add(MenuComponent menuComponent) {
            menuComponentList.add(menuComponent);
        }
        
        @Override
        public void remove(MenuComponent menuComponent) {
            menuComponentList.remove(menuComponent);
        }
        
        @Override
        public MenuComponent getChild(int index) {
            return menuComponentList.get(index);
        }
        
        @Override
        public void print() {
            //打印菜单名称
            for (int i = 0; i < level; i++) {
                System.out.print("--");
            }
            System.out.println(name);
            
            //打印子菜单或者子菜单项名称
            for (MenuComponent component : menuComponentList) {
                component.print();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    Menu类已经实现了除了getName方法的其他所有方法,因为Menu类具有添加菜单,移除菜单和获取子菜单的功能。

    3.3.叶子节点

    MenuItem.java

    //菜单项类,属于叶子节点
    public class MenuItem extends MenuComponent{
        
        public MenuItem(String name,int level){
            this.name = name;
            this.level = level;
        }
        
        @Override
        public void print() {
            for (int i = 0; i < level; i++) {
                System.out.print("--");
            }
            //打印菜单项的名称
            System.out.println(name);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    MenuItem是菜单项,不能再有子菜单,所以添加菜单,移除菜单和获取子菜单的功能并不能实现。

    3.4.测试

    Client.java

    public class Client {
        public static void main(String[] args) {
            //创建菜单树
            MenuComponent menu1 = new Menu("菜单管理", 2);
            menu1.add(new MenuItem("页面访问", 3));
            menu1.add(new MenuItem("展开访问", 3));
            menu1.add(new MenuItem("编辑访问", 3));
            menu1.add(new MenuItem("删除访问", 3));
            menu1.add(new MenuItem("新增访问", 3));
            
            MenuComponent menu2 = new Menu("权限管理", 2);
            menu2.add(new MenuItem("页面访问", 3));
            menu2.add(new MenuItem("提交保存", 3));
            
            MenuComponent menu3 = new Menu("角色管理", 2);
            menu3.add(new MenuItem("页面访问", 3));
            menu3.add(new MenuItem("新增角色", 3));
            menu3.add(new MenuItem("修改保存", 3));
            
            //创建以及菜单
            MenuComponent component = new Menu("系统管理", 1);
            //将二级菜单添加到一级菜单中
            component.add(menu1);
            component.add(menu2);
            component.add(menu3);
            
            //打印菜单名称(如果有子菜单则一起打印)
            component.print();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    输出结果如下:

    --系统管理
    ----菜单管理
    ------页面访问
    ------展开访问
    ------编辑访问
    ------删除访问
    ------新增访问
    ----权限管理
    ------页面访问
    ------提交保存
    ----角色管理
    ------页面访问
    ------新增角色
    ------修改保存
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    4.分类

    在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式安全组合模式两种形式:

    • 透明组合模式
      • 透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中 MenuComponent 声明了 add()remove()getChild() 方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。
      • 透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)
    • 安全组合模式:在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点 Menu 类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。

    5.使用场景

    (1)组合模式适合以下场景:

    • 表示部分和整体的层次结构:例如,文件系统中的文件和目录之间的关系、菜单和菜单项之间的关系等,都可以使用组合模式来表示。
    • 递归遍历结构:当需要遍历复杂的树形结构,而不管它的具体层次,可以使用组合模式来递归处理整个结构,从而简化代码实现。
    • 具有共性操作的场景:当一个类或一组类具有一些共性操作,但是这些操作可能应用于单个对象或多个对象的集合时,可以使用组合模式来统一处理这些操作,使得代码更加简洁和易于扩展。

    (2)总之,组合模式适用于需要使用树形结构表示对象的场景,能够提供一种优雅的设计和管理方式,提高代码灵活性和可扩展性。

    6.优点

    (1)组合模式的优点包括:

    • 简化客户端代码:组合模式使得客户端可以一致地对待单个对象和组合对象,无需区分它们的类型,从而简化了客户端的代码逻辑。
    • 灵活性和可扩展性:通过组合模式,可以方便地增加、删除和修改组合中的对象,使得整个树形结构具有很高的灵活性和可扩展性。
    • 递归处理:组合模式通过递归方式处理整个树形结构,使得操作变得简单,具有较好的递归性。

    (2)组合模式的缺点包括:

    • 可能引入过多的细节:当树形结构非常复杂时,可能会引入更多的细节和复杂性,增加实现和理解的难度。
    • 不适合所有场景:组合模式更适用于表示"部分-整体"的层次结构,如文件夹包含文件等,对于无需表达层次关系的场景,使用组合模式可能会增加不必要的复杂性。
  • 相关阅读:
    gitlab 16.x - ERR unknown command ‘HELLO‘
    《C陷阱和缺陷》-笔记 (3)
    软件工程毕业设计课题(82)微信小程序毕业设计PHP共享停车位小程序系统设计与实现
    对vue的mixin的理解,有什么应用场景?面试题!!!
    认识函数指针
    软件测试技术复习
    JAVA计算机毕业设计毕业生交流学习平台Mybatis+源码+数据库+lw文档+系统+调试部署
    Android源码设计模式探索与实战【代理模式】
    GUI编程--PySide2--基础1
    C++中的extern “C“的作用
  • 原文地址:https://blog.csdn.net/weixin_43004044/article/details/134372861