组合模式--处理对象间的树形结构关系
组合模式介绍
组合模式 主要用于解决对象之间树形结构的父子关系,典型的运用场景有:网页上的菜单管理(多级菜单);以及父子结构的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设计原则,该模式就不可取。只能说 为了具体的业务需要,往往会做出取舍。这就是所谓的中庸之道,程序设计也是如此。
关于“组合模式”的使用就总结到这里,该模式的关键词“树形结构”、“父子关系”,当有这些字眼在你的需求中时,就可以考虑是否可以使用“组合模式”。
上一篇: 看不懂这个语法了,该如何解决