`

组合模式--处理对象间的树形结构关系

阅读更多

组合模式介绍

 

组合模式 主要用于解决对象之间树形结构的父子关系,典型的运用场景有:网页上的菜单管理(多级菜单);以及父子结构的xml文件解析等(比如Dom4J)。组合模式一般会结合“迭代器模式”一起使用,解决复杂的树形结构的对象关系问题。

 

组合模式的类图很简单,但却是威力非常强大的一种设计模式:



 

 

该模式只有三个角色:

A、Component 抽象的接口(也可以是抽象类),定义一些公共的方法;

B、MenuItem 具体的菜单项;

C、Menu 具体的菜单(或子菜单),内部有一个集合 包含多个MenuItem(菜单项)或者子Menu(子菜单)。

 

就这个类图,还看不出这个模式的强大之处,下面以一个实际典型的场景进行讲解。

 

菜单权限管理

 

现在的大型电商网站,都会有自己的管理后端系统 用于各种数据的管理:比如用户、商品、权限管理等。这个管理系统里 会有很多菜单项,以及一些管理员角色类型 不同的角色类型会有不同的菜单权限。先抛开角色,来看看一个简化版的菜单列表:



 

可以看到这个菜单列表有三级,其中“红色虚线框”代表的是具体的“菜单项”(MenuItem),“黑色实现框”代表的是具体的“子菜单”(Menu)。

 

再来看角色,现在我们要求不同的角色看到的菜单列表不一样(每个用户与角色关联),作为示例 这里只设计两种角色:超级管理员和普通管理员。超级管理员具备上述菜单列表所有查看权限,一般分配给“研发”,用于排查问题;普通管理员 一般分配给“运营人员”,他们不需要“缓存管理”、“菜单管理”等,只需要一些业务相关的功能,比如 对某个店铺的上下线等,他们登陆系统后看到的菜单类别是这样:



 

 

具体的需求已经分析完毕,是时候让“组合模式”登场了,对于这种树形结构的对象关系,是“组合模式”的典型运用场景。下面来看使用组合模式,如何实现,由于组合模式有三种角色,这里就分三步来讲解:

 

1、抽象类Component,定义了一些菜单或者菜单项的公共抽象方法,以及部分已实现的公共方法:

public abstract class Component {
    //角色列表
    List<String> roles = new ArrayList<String>();
 
    //菜单名称
    private String name;
 
    //判断是叶子节点 还是目录节点
    public abstract boolean hasChildren();
 
    //添加子节点
    public void addChildren(Component component){
        throw new UnsupportedOperationException();
    }
 
    //添加角色
    public void addRole(String role){
        this.roles.add(role);
    }
 
    //判断菜单或者菜单项是否有指定“角色”
    public boolean hasRole(String role){
        return this.roles.contains(role);
    }
 
    //打印指定角色的 菜单列表
    public abstract void getMenuByRole(String role);
 
    //打印所有的菜单项
    public void printMenu(){
        throw new UnsupportedOperationException();
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}
 

 

主要成员变量或者方法说明:

 

Stirng name成员变量:所有的菜单或者菜单项,都有名称name字段,这里把这个字段提取到Component类中;

 

List<String> roles 成员变量:代表每个菜单(或者菜单项)对应的角色列表,如果这个列表中包含某个角色,表示该角色具备该菜单(或者菜单项)的访问权限,对应的方法有“添加角色”方法addRole 和“判断角色方法”hasRole;

 

addChildren方法:如果是“菜单”类型,需要实现该方法,添加“菜单”或者“菜单项”到自己的列表中。

 

2、“菜单”实现类Menu

/**
 * 菜单,根节点或者分支节点,可以包含子菜单或者菜单项
 * Created by gantianxing on 2017/11/3.
 */
public class Menu extends Component{
    //子菜单(或者菜单项)列表
    List<Component> childrens = new ArrayList<Component>();
 
    public Menu(String name){
        this.setName(name);
    }
 
    @Override
    public boolean hasChildren() {
        return true;
    }
 
    @Override
    public void addChildren(Component component){
        childrens.add(component);
    }
 
    @Override
    public void getMenuByRole(String role) {
        if(hasRole(role)){
            System.out.println("开始打印:"+this.getName());
        }
 
        Iterator<Component> iterator = childrens.iterator();
        while (iterator.hasNext()){
            Component children = iterator.next();
            children.getMenuByRole(role);
        }
 
 
    }
 
    @Override
    public void printMenu(){
        System.out.println("开始打印:" + this.getName());
        Iterator<Component> iterator = childrens.iterator();
        while (iterator.hasNext()){
            Component children = iterator.next();
            children.printMenu();
        }
    }
 
}
 

 

主要成员变量或方法说明:

 

List<Component> childrens 成员变量:该菜单下面所属的子“菜单”或者“菜单项”列表。

 

addChildren方法:往List<Component> childrens中添加子“菜单”或者“菜单项”。

 

getMenuByRole方法:获取指定角色的菜单列表,这里会调用List的迭代器,“递归”调用自己的所有Children的getMenuByRole方法(这里其实使用了另一个模式“迭代器模式”)。

 

3、 “菜单项”实现类MenuItem

public class MenuItem extends Component {
    //菜单项链接
    private String url;
 
    public MenuItem(String name,String url){
        this.setName(name);
        this.setUrl(url);
    }
 
    @Override
    public boolean hasChildren() {
        return false;
    }
 
    @Override
    public void getMenuByRole(String role) {
        if(hasRole(role)){
            System.out.println("菜单名称:"+this.getName()+" 菜单链接:"+this.getUrl());
        }
    }
 
    @Override
      public void printMenu(){
        System.out.println("菜单名称:"+this.getName()+" 菜单链接:"+this.getUrl());
    }
 
    public String getUrl() {
        return url;
    }
 
    public void setUrl(String url) {
        this.url = url;
    }
}
 

 

主要成员和方法说明:

 

String url成员变量:每个具体的“菜单项”,都对应一个可以点击的“链接”地址。

 

getMenuByRole方法:如果该“菜单项”对应的角色列表中 包含指定的角色,就打印该菜单项。

 

好了,一个简单的“菜单权限管理系统”采用“组合模式”已经实现了,只是真实的场景中可能会多一些“菜单”和“角色”而已。

 

下面来看下测试方法,见证奇迹的时候:

public static void main(String[] args) {
        //创建“缓存管理” 子菜单
        MenuItem pageCache = new MenuItem("页面缓存","/cache/pageCache.html");//页面缓存
        MenuItem dateCache = new MenuItem("数据缓存","/cache/dataCache.html");//数据缓存
        Menu cache = new Menu("缓存管理"); //缓存管理
        cache.addChildren(pageCache);
        cache.addChildren(dateCache);
 
        //创建"运营管理"子菜单
        MenuItem actManager = new MenuItem("活动管理","/manager/actManager.html");//活动管理
        MenuItem shopManager = new MenuItem("店铺管理","/manager/shopManager.html");//店铺管理
        Menu manager = new Menu("运营管理");
        manager.addChildren(actManager);
        manager.addChildren(shopManager);
 
        //创建"用户管理"子菜单
        MenuItem superManager = new MenuItem("管理员管理","/user/supermanager.html");//管理员管理
        MenuItem supplierManager = new MenuItem("供应商管理","/user/supplierManager.html");//供应商管理
        Menu user = new Menu("用户管理");
        user.addChildren(superManager);
        user.addChildren(supplierManager);
 
        //创建“菜单管理” 菜单项
        MenuItem menuManager = new MenuItem("菜单管理","/menuManager.html");
 
        //创建顶级菜单
        Menu background = new Menu("管理后台");
        background.addChildren(cache);
        background.addChildren(manager);
        background.addChildren(user);
        background.addChildren(menuManager);
 
        //打印所有的菜单
        System.out.println("----------所有菜单列表-----------");
        background.printMenu();
 
        //给每个菜单和菜单项授予 超级管理员角色
        pageCache.addRole(SUPER_ROLE);
        dateCache.addRole(SUPER_ROLE);
        cache.addRole(SUPER_ROLE);
        actManager.addRole(SUPER_ROLE);
        shopManager.addRole(SUPER_ROLE);
        manager.addRole(SUPER_ROLE);
        superManager.addRole(SUPER_ROLE);
        supplierManager.addRole(SUPER_ROLE);
        user.addRole(SUPER_ROLE);
        menuManager.addRole(SUPER_ROLE);
        background.addRole(SUPER_ROLE);
 
        //给部分菜单添加 “普通管理员角色”
        actManager.addRole(NORMAL_ROLE);
        shopManager.addRole(NORMAL_ROLE);
        manager.addRole(NORMAL_ROLE);
        supplierManager.addRole(NORMAL_ROLE);
        user.addRole(NORMAL_ROLE);
        background.addRole(NORMAL_ROLE);
 
        //打印"供应商管理员"菜单列表
        System.out.println("----------普通管理员菜单列表-----------");
        background.getMenuByRole(NORMAL_ROLE);
 
        //打印"超级管理员"菜单列表(结果与所有菜单列表一样)
        System.out.println("----------超级管理员菜单列表-----------");
        background.getMenuByRole(SUPER_ROLE);
 
    }
 

 

具体代码逻辑很简单,都是一些数据的初始化,如果还不明白看代码注释即可,最后构建一个顶级菜单:background。

 

执行mian方法,查看结果,见证奇迹的时候:

----------所有菜单列表-----------
开始打印:管理后台
开始打印:缓存管理
菜单名称:页面缓存 菜单链接:/cache/pageCache.html
菜单名称:数据缓存 菜单链接:/cache/dataCache.html
开始打印:运营管理
菜单名称:活动管理 菜单链接:/manager/actManager.html
菜单名称:店铺管理 菜单链接:/manager/shopManager.html
开始打印:用户管理
菜单名称:管理员管理 菜单链接:/user/supermanager.html
菜单名称:供应商管理 菜单链接:/user/supplierManager.html
菜单名称:菜单管理 菜单链接:/menuManager.html
----------普通管理员菜单列表-----------
开始打印:管理后台
开始打印:运营管理
菜单名称:活动管理 菜单链接:/manager/actManager.html
菜单名称:店铺管理 菜单链接:/manager/shopManager.html
开始打印:用户管理
菜单名称:供应商管理 菜单链接:/user/supplierManager.html
----------超级管理员菜单列表-----------
开始打印:管理后台
开始打印:缓存管理
菜单名称:页面缓存 菜单链接:/cache/pageCache.html
菜单名称:数据缓存 菜单链接:/cache/dataCache.html
开始打印:运营管理
菜单名称:活动管理 菜单链接:/manager/actManager.html
菜单名称:店铺管理 菜单链接:/manager/shopManager.html
开始打印:用户管理
菜单名称:管理员管理 菜单链接:/user/supermanager.html
菜单名称:供应商管理 菜单链接:/user/supplierManager.html
菜单名称:菜单管理 菜单链接:/menuManager.html

 

 

运用结果分为三部分:所有的菜单列表;“普通管理员”角色的菜单列表;“超级管理员”的菜单列表(和所有菜单列表相同)。这里我们重点看下“普通管理员”角色的菜单列表:

----------普通管理员菜单列表-----------
开始打印:管理后台
开始打印:运营管理
菜单名称:活动管理 菜单链接:/manager/actManager.html
菜单名称:店铺管理 菜单链接:/manager/shopManager.html
开始打印:用户管理
菜单名称:供应商管理 菜单链接:/user/supplierManager.html

对比上述“普通管理员”菜单列表展示需求 是完全吻合的:



 

 

如果要新增其他角色和菜单,“组合模式”对应的三个类,无需做任何改动。可以看到“组合模式”是满足OO设计模式中的“开闭原则”的。

 

在真实环境中的运用

 

在真实环境中的运用上述代码,会做一些调整:

 

1、在真实的环境中,首先是通过“菜单管理”创建“菜单”或者“菜单项”,以及分配其对应的“角色列表”,然后保存到数据库中。

 

2、在“用户管理”中分配用户对应的“角色”。

 

3、菜单只与角色挂钩,而不是具体用户,也就是说每个角色对应的”菜单列表”是固定的,这时可以根据不同的角色初始化出多个“顶级菜单”对象(使用组合模式),放入缓存中。

 

4、具体的用户(普通管理员或者超级管理员)登陆成功后,根据不同的角色,获取缓存中不同的“顶级菜单”对象返回给前端页面即可,无需每次都查询数据库。

 

5、前端页面遍历“顶级菜单”对象进行展示。

 

通过上述流程,即可完成不同的角色展示“不同的菜单列表”。

 

最后再简单提一下关于“菜单权限”验证,实现起来也很简单:

1、在后端系统创建一个拦截器,获取登陆用户的角色信息。

2、判断用户访问的链接(一个MenuItem对象)的角色列表中,是否包含步骤1中的角色(调用其hasRole方法即可)。如果包含 则验证通过,否则验证失败 返回非法访问。

 

当然,如果使用Spring mvc的话,也可以结合Spring Security进行权限验证 即:菜单的管理和展示使用“组合模式”,菜单的权限验证使用Spring Security。对Spring Security感兴趣的,可以点击这里

 

小结

 

组合模式提供一个树形结构的组合对象,可以同时容纳个体对象和子组合对象,并且允许客户端大多数情况下操作个体对象和组合对象一视同仁(透明性);但个体与组合始终有区别(安全性),比如在个体上就不能执行add方法,这时需要根据具体情况做取舍。支持“开闭原则”,但缺违背了“单一责任原则”:既要执行菜单相关操作,又要管理层次结构。

 

不能说“组合模式“违背了部分OO设计原则,该模式就不可取。只能说 为了具体的业务需要,往往会做出取舍。这就是所谓的中庸之道,程序设计也是如此。

 

关于“组合模式”的使用就总结到这里,该模式的关键词“树形结构”、“父子关系”,当有这些字眼在你的需求中时,就可以考虑是否可以使用“组合模式”。

  • 大小: 8.9 KB
  • 大小: 32.9 KB
  • 大小: 16.3 KB
0
0
分享到:
评论

相关推荐

    组合模式-------树形模式

    这种模式将对象组织成树形结构,允许用户对部分或整个树进行操作,简化了处理复杂对象集合的逻辑。 在组合模式中,有三个主要角色: 1. **组件(Component)**:定义了公共接口,供叶子节点和组合节点共享。组件...

    组合模式-空军指挥系统.zip

    组合模式是一种对象结构型设计模式,它允许我们创建表示部分-整体层次结构的树形对象。在空军指挥系统中,这种模式的应用可以帮助我们构建一个灵活、可扩展的组织架构,其中每个部分(如飞机、飞行编队或基地)都...

    Python 程序语言设计模式思路-结构型模式:组合模式:将对象组合成树形结构

    组合模式作为一种强大的设计模式,通过将对象组合成树形结构,可以表示“部分-整体”的层次结构,并统一地处理单个对象和组合对象,简化了客户端代码的复杂性,从而提高代码的灵活性和可扩展性。在实际开发中,它在...

    java常用设计模式-组合模式

    组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构以表示“部分-整体”的层次结构。这种模式使得客户端可以统一对待单个对象和对象组合。在组合模式中,有两种基本类型的对象:叶...

    组合模式-五子棋代码.zip

    组合模式是一种对象结构型设计模式,它允许我们以树形结构来表示部分-整体关系,使得我们可以像处理单个对象一样处理整个集合。在五子棋游戏的实现中,组合模式的应用尤为关键,因为它帮助我们将棋盘上的棋子和棋盘...

    设计模式-组合模式

    组合模式是一种软件设计模式,属于结构型模式,它允许我们以树形结构来表示部分与整体的关系,使得我们可以像处理单个对象一样处理整个集合。在iOS开发中,这种模式非常有用,特别是在处理数据结构或者UI组件时,...

    23种 设计模式---面向对象的基本原则

    - 组合模式(Composite):将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。 - 装饰模式(Decorator):动态地给一个对象添加一些额外的职责,提供比继承更...

    设计模式专题之(九)组合模式---设计模式组合模式示例代码(python--c++)

    组合模式是一种结构型设计模式,它允许我们使用树形结构来表示部分与整体的关系,使得我们可以在统一的接口下处理单个对象和对象的组合。这个模式在处理类似目录树或者组织架构这样的数据时非常有用。在组合模式中,...

    设计模式-可复用面向对象软件的基础(中英文+src code)

    - 组合模式(Composite)将对象组合成树形结构,可以以一致的方式处理单个对象和组合。 - 桥接模式(Bridge)将抽象部分与实现部分分离,使它们可以独立变化。 3. **行为型模式** 关注对象之间的责任分配和通信。...

    2 组合模式-课程内容.rar

    组合模式是一种设计模式,它属于行为设计模式,主要用于将对象组织成树形结构,使得用户可以对单个对象和组合对象进行一致的操作。这个模式在处理具有部分整体关系的复杂对象时特别有用,例如文件系统、菜单结构或者...

    设计模式 - 可复用面向对象软件的基础(高清版)

    组合模式(Composite)允许将对象组成树形结构,表示部分-整体关系;外观模式(Facade)提供一个统一的接口,简化子系统之间的交互。 3. 行为型模式:这类模式关注对象之间的交互和职责分配。例如,策略模式...

    设计模式--组合模式java例子

    组合模式是一种结构型设计模式,它允许我们使用树形结构来表示部分-整体关系,使得我们能够像处理单个对象一样处理整个集合。在Java中,这种模式的应用可以帮助我们简化对复杂对象层次结构的操作,同时保持接口的...

    C#面向对象设计模式纵横谈(9):Composite 组合模式(结构型模式)

    在C#编程中,组合模式常常被用来处理树形结构的数据,使得客户端代码能够统一地处理单个对象和对象的集合。 在组合模式中,我们定义了两个主要的角色:Component(组件)和Composite(组合)。Component 是一个抽象...

    设计模式-组合模式(讲解及其实现代码)

    组合模式是一种结构型设计模式,它允许我们使用树形结构来表示部分与整体的关系,使得我们能够以统一的方式来处理单个对象和对象的组合。在组合模式中,单个对象和组合对象都被视为同一类型,这使得客户端代码可以对...

    c++-设计模式之组合模式(Composite Pattern)

    组合模式使得客户端可以以统一的方式对待单个对象和组合对象,这种模式常用于需要处理树形结构的数据,如文件系统、图形界面等。 组合模式的组成 1、组件接口(Component):定义叶子和组合对象的共同接口。 2、...

    zkk950815#design-pattern-java-1#树形结构的处理——组合模式(四)1

    而且无论客户端如何定义叶子构件对象都无法调用到这些方法,不需要做任何错误和异常处理,容器构件再根据需要增加访问和管理成员的方法,但这时候也存在一个问题:客户端不

    设计模式_组合模式.zip

    组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。 这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。 我们通过...

    设计模式-组合实体

    在众多设计模式中,组合实体(Composite)模式是一种结构型模式,它允许我们以树形结构来表示部分与整体的关系,使得用户可以一致地处理单个对象和对象组合。在iOS开发中,组合实体模式尤其有用,因为它简化了复杂...

    c#代码介绍23种设计模式-10组合模式(附代码)

    组合模式允许你将对象组合成树形结构来表现”部分-整体“的层次结构,使得客户以一致的方式处理单个对象以及对象的组合 组合模式实现的最关键的地方是——简单对象和复合对象必须实现相同的接口。这就是组合模式能够...

    2 组合模式-MOOC课程内容.pdf

    它的主要目的是将对象组合成树形结构以表示部分与整体的层次关系。使用组合模式可以使客户端以统一的方式处理单个对象以及组合对象。 组合模式的关键参与者包括: 1. Client(客户端):客户端通过Component接口...

Global site tag (gtag.js) - Google Analytics