设计模式之权限树实现

726 阅读15分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

权限和商品中心

开发过权限中心以及商品中心的相关功能,这种需求大多数都是增删改查,没有复杂的逻辑的业务逻辑,比如权限中心一般功能为:权限列表查询、子权限列表查询、添加权限、查询权限、更新权限、删除权限。商品中心可能我们在开发商品分类的时候一般都会存在多级的分类,例如目前商品中心都会分为一级商品分类,二级商品分类和三级商品分类。

很多的操作与展示需要基于树形结构来记录,个体与个体之间有父子关系,这种操作一般在删除时需要判断是否已经被引用或删除是需要递归删除节点下所有子节点。

针对权限的删除操作,我们一般可以分为四步:

1、先删除最底层的子权限,然后逐级往上删除,删除到当前的权限

 

2、删除权限之前,需要判断角色、账号跟权限之间是否关联,如果这个权限还跟有一些角色和账号跟之进行关联,就不能直接删除

 

3、如果存在某一个权限更账号和角色进行了关联,就需要回滚整个事务,恢复之前的已经删除的了某部分的权限。当然,你也可以一次全部判断该删除的权限下所有子权限是否被引用,如果存在引用就不执行操作。

先给大家看看一段伪代码:

public class PriorityService1 implements PriorityService{
    
  	/**
	 * 删除权限
	 * @param id 权限id
	 * @return 处理结果
	 */
    @Override
	public Boolean removePriority(Long id) throws Exception {
        
        // 参数校验
        Preconditions.checkNotNull(id);
        
        Boolean isDelete = true;
		// 根据id查询权限
		List<Priority> childrenPriority = priorityDAO.getPriorityById(id);
        
        //依次对子节点循环进行删除
        for(Priority priority : childrenPriority){
            //判断是否被子子节点是否被账号和角色引用
        	if(priorityDAO.isRelate(priority.getId())){
                isDelete = false;
                break;
            }
        }
        
        //如果子节点中都不存在角色和账号引用,这执行删除操作
        if(isDelete)
        {
        	 for(Priority priority : childrenPriority){
             		priorityDAO.removePriority(priority.getId());
             }
        }
		
		return isDelete;
	}
}

大家觉得以上代码实现removePriority方法有什么问题?

首先大家角色removePriority代码逻辑结构清晰吗?是否一眼就可以看出removePriority这个方法做什么?

priorityDAO.isRelate(priority.getId())大家觉得这个方法有问题没有,isRelate方法需要判断权限与角色、账号是否进行绑定其实需要在底层去关联角色和账号相关的表,priorityDAO.isRelate是priorityDAO的职责吗?

关于DAO的一点点理论

文章开头其实和大家已经说过,今天与大家谈论并不仅仅是设计模式相关,因此我们在这里来谈谈DAO的职责?

对于这种比例概念的东西我们现在网上百度一把:

首先从DO来说起,DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。其实说白了,DAO的处理的对象就是DO,从DAO返回的数据,要么是基本数据类型,要么是DO实体。每个DAO应该有一个主表,围绕这个主表产生DO,同时尽量避免联表。

《高性能Mysql》中有这样一段话:

   无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者尽可能避免对大量数据进行排序。

   如果 ORDER BY 子句中的所有列来都来自关联的第一个表,那么mysql在关联处理第一个表的时候就进行文件排序。除此之外的所有情况,mysql都会先将关联的结果放到一个临时表中,然后在所有的关联都结束后,再进行文件排序。

在这里还需要和大家强调的一个点就是,尽量不要把计算任务放到数据库中去实现,数据库存储数据的组件,而不是计算组件,对于数据库做复杂的查询关联,做排序和分组,其实都属于计算型的任务,一旦数据量较大,就需要消耗大量数据库的资源,针对传统的MYSQL数据库的资源相对业务系统服务资源来说在横向扩容方面来说要差很多,因此,这也是为什么让大家尽量少做复杂查询的底层逻辑。


在这里并不想很多篇幅去聊这个DAO相关的内容,否则就反客为主了,大家理解这个到这里就可以了。

removePriority的业务逻辑定义

我们再来看另外一个点,代码如下:

//大家想想这两块的逻辑是什么? 

for(Priority priority : childrenPriority){
            //判断是否被子子节点是否被账号和角色引用
        	if(priorityDAO.isRelate(priority.getId())){
                isDelete = false;
                break;
            }
}

for(Priority priority : childrenPriority){
             		priorityDAO.removePriority(priority.getId());
  }

大家可以思考上面代码块的逻辑是什么?

第一个for循环的逻辑是判断每一个子节点是否存在角色和权限的引用,第二个for循环的逻辑是循环删除该节点下所有的子权限。

大家再次思考一下,这些逻辑属于PriorityService.removePriority的逻辑吗?

其实从PriorityService.removePriority逻辑是什么?如果那么我们来定义removePriority方法,

PriorityService. removePriority定义:判断删除的priority的id以及子权限是否存在与角色和账户关联,否则就删除该权限以及子权限。


我们再来一段代码,按我们上面对PriorityService. removePriority定义来:

public class PriorityService2 implements PriorityService{
    
  	/**
	 * 删除权限
	 * @param id 权限id
	 * @return 处理结果
	 */
    @Override
	public Boolean removePriority(Long id) throws Exception {
        
        // 参数校验
        Preconditions.checkNotNull(id);      
        //判断权限是否已关联角色和账号,如果被关联着返回true,否则返回false
        Boolean isRelate = PriorityTree.isRelate(priority.getId());
        //如果没有关联,则进行删除
        return isRelate : isRelate :PriorityTree.removePriority(priority.getId());
	}
}

大家觉得for循环的逻辑还有必要出现在PriorityService的removePriority方法中吗?至少目前定义来看for循环的逻辑是没有必要出现在Priority.removePriority方法中。

现在是不是觉得removePriority的方法逻辑更清晰一点了,在这里需要和大家说的是,我们在设计类方法的时候一定要有主方法和辅方法,我们在设计类的时候也需要有主类和辅类,所谓主方法就是一个业务的入口,承载了这个业务的主业务逻辑的生命周期,大家可以想一想,jdk是不是为我们提供了一个main()的入口方法,其实与我们涉及的思想也是类似。

但是我们现在又有了一个新的问题?

大家知道是什么吗?

PriorityTree是个啥类

看代码你会发现一个PriorityTree类,PriorityTree是啥类?是domain对象吗?我们先将这个问题放在这边,后面进行解答。

大家一定会想,上面的代码中的for循环逻辑去哪里,既然它不属于PriorityService,那属于谁?

在这里和大家说说设计中,内聚一些原则?概念的东西,先百度一下:

高内聚低耦合定义:

高内聚低耦合,是软件工程中的概念,是判断软件设计好坏的标准,主要用于程序的面向对象的设计,主要看类的内聚性是否高,耦合度是否低。目的是使程序模块的可重用性、移植性大大增强。通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低。内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事,它描述的是模块内的功能联系;耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。

内聚是指一个模块内的交互程度,耦合是指模块间的交互程度。我们需要尽力做到高内聚低耦合。

其实这边有两个逻辑需要内聚,一是判断一个权限是否已经绑定账号和权限,二是循环删除权限节点以及子节点,而且这两个方法中都存在递归的逻辑。其实这也是for循环的主要逻辑。因此,在这边我们需要设计另外一个类来承载这个逻辑。

大家可以想想一个问题,不管权限也好,商品品类业务,在业务上或UI层面展现的数据结构是什么?

对的,大家可能已经知道答案了,就是树。我们所有操作,不管是增删改查都是围绕这棵树来进行的,因此我们能不能创建一个权限树的类呢?答案是肯定的。

因此我们创建一个权限树的类

public class PriorityTree {

	private Long id;
	private String code;
	private String url;
	private String comment;
	private Integer type;
	private Long parentId;

	private List<Priority> children = new ArrayList<Priority>();
	
	/**
	 * 接收一个操作者(删除、判断是否被引用等等)
	 */
	public <T> T execute(PriorityOperation<T> operation) throws Exception {  
		return operation.doExecute(this);  
	}
    
}

为什么会有execute(PriorityOperation operation),上面其实说了,我们需要对权限树会进行一些复杂的操作,例如:删除权限等等。我们可以将一些复杂的操作从权限树中抽象出来,成为单独操作,如果后期加一些复杂的操作不用修改权限树,只需要创建一个新的操作就可以了。

因此,我们可以再创建两个类,一个用于判断权限id是否已经关联角色和账号,一个用来删除权限以及子权限,如果以后再有什么比较对于树有复杂的操作, 随着业务发展对于这个权限树复杂度会越来越高, 我们可以扩展一个个新类来进行实现,但是对于这个权限树来说并不会变得复杂。大家一定要理解这种思想,既然我们把这个功能设计成为可扩展的,怎么少得了接口呢?


首先我们定义一个权限树操作接口,代码如下:

/**
 * 权限树操作接口
 *
 */
public interface PriorityOperation<T> implements PriorityOperation<Boolean> { 
 
	/**
	 * 执行这个操作
	 * @param priority 权限
	 * @return 结果
	 * @throws Exception
	 */
	T doExecute(Priority priority) throws Exception;
	
}

定义一个给定权限节点以及子节点是否关联了角色和账号的判断操作,具体代码如下:

/**
 * 检查权限节点以及子节点是否关联了角色和账号
 *
 */
@Component
public class IsRelatedPriorityOperation implements PriorityOperation<Boolean> {  
	/**
	 * 访问权限树节点
	 */
	@Override
	public Boolean doExecute(Priority priority) throws Exception {
		List<PriorityDO> priorityList = priorityDAO.listChildPriorities(priority.getId());
		
		if(priorityList != null && priorityList.size() > 0) {
			for(PriorityDO priorityDO : priorityDOs) {
				Priority priorityNode =BeanCopier.copy(priorityDO,Priority.class);
				priorityNode.execute(this); 
			}
		}
		
        //检查权限节点以及子节点是否关联了角色和账号
		if(relateCheck(priority)) {
			this.relateCheckResult = true;
		}
		
		return this.relateCheckResult;
	}

定义一个针对权限树的删除操作代码如下:

/**
 * 删除权限操作
 *
 */
@Component
public class RemovePriorityOperation implements PriorityOperation<Boolean> { 

	/**
	 * 权限管理DAO组件
	 */
	@Autowired
	private PriorityDAO priorityDAO;
    
	/**
	 * 访问权限树节点
	 * @param node 权限树节点
	 */
	@Override
	public Boolean doExecute(Priority priority) throws Exception {
		List<PriorityDO> priorityList = priorityDAO.listChildPriorities(priority.getId());
		
		if(priorityList != null && priorityList.size() > 0) {
			for(PriorityDO priorityDO : priorityList) {
                //将priorityDO数据对象转化成为PriorityTree操作对象
				Priority priorityNode = BeanCopier.copy(priorityDO,Priority.class);
                //执行删除操作
				priorityNode.execute(this);  
			}
		}

        //删除权限
		priorityDAO.removePriority(node.getId());
		
		return true;
	}

}

我们将PriorityService也来进行相应的调整,具体伪代码如下:

public class PriorityService3 implements PriorityService{
    
  	/**
	 * 删除权限
	 * @param id 权限id
	 * @return 处理结果
	 */
    @Override
	public Boolean removePriority(Long id) throws Exception {
        
        // 参数校验
        Preconditions.checkNotNull(id);      
        //判断权限是否已关联角色和账号,如果被关联着返回true,否则返回false
        Boolean isRelate = PriorityTree.execute(new IsRelatedPriorityOperation());
        //如果没有关联,则进行删除
        return isRelate : isRelate :PriorityTree.execute(new RemovePriorityOperation());
	}
}

PriorityService3就是我们最终的一个service代码实现,从逻辑来看要比没有加上for循环逻辑的PriorityService1要简洁很多,因为这个方法是删除业务主入口方法,所以需要逻辑清晰简单,一眼就要看懂这个方法的逻辑。另外,我们从系统的扩展性来看定义了 IsRelatedPriorityOperation和RemovePriorityOperation两个操作类,用来扩展权限树的操作,如果后面业务扩展,需要对权限树有更多的操作,例如:需要将一些特殊节点在界面进行高亮显示, 只要定义一个HightLignhtOperation的类就可以了,不需要改动之前任何的逻辑代码,这样扩展起来是非常方便,提高了代码的扩展性。从代码质量上来看, 也不用影响到之前其他的任何代码,只需要新增一个扩展类就可以了,减少了不必要的BUG的引入和测试工作量,不要看有的同学把自己搞的特别忙,其实有些忙是没有必要的。从代码结构层面来看,从 Operation的个数和命名就可以看出权限树有多少个操作,每个操作之前相互都是耦合的,但是每个操作的业务逻辑都是内聚的,代码可读性以及维护也提高了。

可能有人会认为这样搞本来很简单的功能,写一个类就搞定的事情,现在写了5个类,这样搞不是复杂了吗?有必要吗?是不是过度设计了,但是我想问题大家一个问题,业务在一开始的时候是不是很简单的,没有太多业务逻辑,但是随着业务不断的发展,业务快速的迭代,业务复杂度也会越来越高,你的代码逻辑也会变得越来越复杂,而且会经历过多不同开发风格的开发人员,目前康众ERP就是这样的。大家对于ERP应该都比较熟悉了,对于里面的实现的代码或多或少都会有所了解,可能很多接手的开发人员都会抱怨太难逻辑太复杂了,一个方法太长了根本都不知道如何优化,...抱怨归抱怨,但是建议大家更多的去思考一下为什么会是这样。其实原因有很多,但是最本质的原因我认为对于业务理解以及代码设计层面没有进行深入思考,有的点可能根本没有任何思考。**


其实所谓的设计,就是让代码看上更简单,更合理,更易扩展,更易于维护。大家只要在日常中遵循这个设计原则方向就是对的。

不知道大家有没有听过代码DNA的概念,就是说在业务初期和设计初期,就决定了这个系统的DNA,包括整体架构设计,如果后期出现了大量问题去优化,需要做大的架构调整往往需要付出比重新开发一套该系统更多的成本。

言归正传

可能大家会问,这个系列不是将设计模式吗?上面代码有用到设计模式?

答案是肯定的。

那都用到了什么设计模式呢?

首先看看看看PriorityTree这个类的定义,伪代码如下:

public class PriorityTree {

	private Long id;
	private String code;
	private String url;
	private String comment;
	private Integer type;
	private Long parentId;

	private List<Priority> children = new ArrayList<Priority>();
	
	/**
	 * 接收一个操作者(删除、判断是否被引用等等)
	 */
	public <T> T execute(PriorityOperation<T> operation) throws Exception {  
		return operation.doExecute(this);  
	}
    
}

PriorityTree类就是一个典型的运用了组合模式(composite),private List children = new ArrayList(),其实就是将多个叶子节点组合成为一棵树,组合模式典型使用场景就是树形结构的处理,

将属性结构用一个类组装成为一棵树,针对这棵树所有的操作都是内聚定义到这棵树中的,不会让调用者去care内部属内部递归的逻辑,因为这部分逻辑不属于调用者。

在这里我不是描述组合模式是怎么样的,有什么组件组成,组件之间的关系是又是怎么样的,因为我觉得这样写 出来过于学术化,可能当时能够理解,但是事后会很快忘记,我更习惯于用自己的理解和感悟来进行描述,从我们实际项目具体事例来切入,这样更容易理解和掌握。否则,大家可以自己百度一把,网上都是非常标准的定义,我也就没有写设计模式系列文章了。

组合模式就是这样,也是我对组合模式的理解,其实很简单,大家不要觉得设计模式是什么高大上的东西,也不用觉得设计模式平时好像都用不上,只是大家在写代码的过程中,根本就没有这种思维,没有这种设计思想的理念,希望这边文章能够使大家意识到。

说到设计模式,其实大家不要纠结,是否负责XX原则以及XX标准,如果只是一味的去遵循理论和标准,会让使人很累,而且还达不到好的效果,其实我们学习设计模式真真的目的是掌握其设计模式本质的思想和倡导的理念,就好比练武之人不要局限于具体的招式一样,真真需要掌握的是在外在招式下隐藏的内在魂,也就是我们说的思想,为什么有时候正真高手可以无招胜有招,其实在于本质上已经对于思想灵活贯通,其实我们设计代码也是一样的道理。

接下来我们来聊聊上面使用到的另外一种设计模式-访问者模式。老规矩,先看伪代码:

/**
	 * 接收一个操作者(删除、判断是否被引用等等)
	 */
	public <T> T execute(PriorityOperation<T> operation) throws Exception {  
		return operation.doExecute(this);  
	}

PriorityTree类里面定义了一个execute方法,并且接受一个PriorityOperation对象,PriorityOperation对象doExecute可以访问PriorityTree进行操作。PriorityTree这样就将所有操作通过Operation进行扩展了,可以在任何时候给树形的数据结构增加任何的功能。