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

组合模式介绍

 

组合模式 主要用于解决对象之间树形结构的父子关系,典型的运用场景有:网页上的菜单管理(多级菜单);以及父子结构的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设计原则,该模式就不可取。只能说 为了具体的业务需要,往往会做出取舍。这就是所谓的中庸之道,程序设计也是如此。

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

猜你喜欢

转载自moon-walker.iteye.com/blog/2398633