浅谈设计模式(开篇)

104 阅读20分钟

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

目录

  1. 什么是设计模式
  2. 为什么要学习设计模式
  3. 设计模式之权限树实现

1、什么是设计模式

在介绍设计模式之前,我们首先快速回顾一下面向对象的几个原则

1.1、6大设计原则

开闭原则(Open Close Principle)对扩展开放,对修改关闭
在代码层面而言就是在你有新的需求的时候,你应当增加新的对象来实现,而不是修改原来的对象。想要达到这样的效果,我们需要使用接口和抽象类。开闭原则是根本原则,它是面向对象设计的终极目标。

下面6个原则可以看做是开闭原则的实现方法。

1.1.1、单一职责原则 (Single Responsiblity Principle)

一个实体(一个类或者一个功能模块) 只负责一个功能领域中的相应职责

堆积木时, 到底是一块积木比较容易利用, 还是多块积木拼接起来的"一大块" 更容易利用?

此外,承担了过多的责任,也就是可能会因为多个原因需要修改这段代码

随之而来的是不稳定性以及维护成本的增加。

1.1.2、里氏代换原则(Liskov Substitution Principle)

里氏代换原则是对开闭原则的补充。子类可以扩展父类的功能,但不能改变原有父类的功能
实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

1.1.3、依赖倒转原则(Dependence Inversion Principle)面向接口编程

这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。

上层模块不应该依赖下层模块,两者应依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

1.1.4、接口隔离原则(Interface Segregation Principle)建立单一接口

当一个接口太大时,我们需要将它分割成一些更细小的接口,接口调用者仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色。
使用专门的接口,而不是大而全统一的接口,不要强迫客户端程序依赖不需要的方法。

1.1.5、迪米特法则(Law of  Demeter Principle),又称最少知道原则(Demeter Principle)

一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。

有一个形象的说法"不要和“陌生人”说话、只与你的直接朋友通信"。

1.1.6、合成复用原则(Composite Reuse Principle)

尽量使用合成/聚合的方式,而不是使用继承。复用一个类有两种常用形式,继承和组合

尽量使用组合,而不是继承来达到复用的目的,因为继承子类可以覆盖父类的方法,将细节暴露给子类

而且会建立强耦合关系,是一种静态关系,不能再运行时更改等等弊端

1.2、设计模式

我们知道对于很多数学问题,经常会有多种不同的解法,

而且这其中可能会有一种比较通用简便高效的方法,

我们在遇到类似的问题或者同一性质的问题时,也往往采用这一种通用的解法。

对于软件开发人员, 在软件开发过程中, 面临的一般问题的解决方案就是设计模式(准确的说是OOP中)

当然,如同数学的解题思路一样,设计模式并不是公式一样的存在,

设计模式代表了最佳的实践

是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的宝贵经验

是解决问题的思路

总之,设计模式是一种思想思想思想

大部分设计模式要解决的都是代码的可扩展性问题。

关于设计模式与设计原则,设计模式是设计原则的具体化形式,是针对于某些特定场景的具体化解决方案

具体到类/接口的设计组织逻辑

既然是原则的具体化形式,那么必然,按照原则的合理组合运用以及问题的场景,其实可以延伸出来更多的设计模式

我们都知道,常用的有23种设计模式,按照特点可以将其分为三大类型:创建型结构型行为型 

创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。

结构型模式:把类或对象结合在一起形成一个更大的结构。

行为型模式:类和对象如何交互,及划分责任和算法。

我是分隔线……,上面都是一些理论知识,大家可以从相关书籍或网络上找更详细的介绍。

2、为什么要学习设计模式

  • 应对面试中的设计模式相关问题;
    学习设计模式最功利、最直接的目的,可能就是应对面试了。不管你是前端工程师、后端工程师,还是全栈工程师,在求职面试中,设计模式问题是被问得频率比较高的一类问题。

  • 告别写被人吐槽的烂代码;
    我们经常说,“Talk is cheap,show me the code。”实际上,代码能力是一个程序员最基础的能力,是基本功,是展示一个程序员基础素养的最直接的衡量标准。你写的代码,实际上就是你名片。

  • 提高复杂代码的设计和开发能力;
    我们大部分工程师比较熟悉的都是编程语言、工具、框架这些东西,因为每天的工作就是在框架里根据业务需求,填充代码。相对来说,这样的工作并不需要你具备很强的代码设计能力,只要单纯地能理解业务,翻译成代码就可以了。
    但是如果开发一个跟业务无关的比较通用的功能模块,面对这样稍微复杂的代码设计和开发,如何分层、分模块?应该怎么划分类?每个类应该具有哪些属性、方法?怎么设计类之间的交互?该用继承还是组合?该使用接口还是抽象类?怎样做到解耦、高内聚低耦合?
    这时就需要一些设计模式相关的知识的了解和积累。

  • 让读源码、学框架事半功倍;
    大家在实际开发的过程中,都有查看源码的经历,不知道有没有遇到看不懂、看不下去的情况。实际上,这个问题的原因很简单,那就是你积累的基本功还不够,你的能力还不足以看懂这些代码。
    一些优秀的开源项目、框架、中间件,代码量、类的个数都会比较多,类结构、类之间的关系极其复杂,常常调用来调用去。所以,为了保证代码的扩展性、灵活性、可维护性等,代码中会使用到很多设计模式、设计原则。如果你不懂这些设计模式、原则,在看代码的时候,你可能就会琢磨不透作者的设计思路,对于一些很明显的设计思路,你可能要花费很多时间才能参悟。
    相反,如果你对设计模式、原则、思想非常了解,一眼就能参透作者的设计思路、设计初衷,你就会很轻松的读懂源码,定位问题。

  • 为你的职场发展做铺垫。
    普通的开发工程师,只需要把框架、开发工具、编程语言用熟练,再做几个项目练练手,基本上就能应付平时的开发工作了。但是,如果你不想一辈子做一个低级的码农,想成长为技术专家、大牛、技术 leader,那就需要掌握设计模式,设计原则相关的基本功了。
    我们看一些大牛写的代码,或者优秀的开源项目,代码写得都非常的优美,质量都很高。而质量低的代码会导致线上 bug 频发,排查困难。整个团队都陷在成天修改无意义的低级 bug、在烂代码中添补丁的事情中。而一个设计良好、易维护的系统,可以解放我们的时间,让我们做些更加有意义、更能提高自己和团队能力的事情。

我是分隔线……,下面来点干货

3、设计模式之权限树实现

3.1、权限和商品中心

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

大家想想权限、商品分类它们操作都有哪些共同点?与一些其他客户信息等有什么区别呢?

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

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

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的职责吗?

3.2、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()的入口方法,其实与我们涉及的思想也是类似。

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

大家知道是什么吗?

3.3、PriorityTree是个啥类

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

大家一定会想,上面的代码中的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的个数和命名就可以看出权限树有多少个操作,每个操作之前相互都是耦合的,但是每个操作的业务逻辑都是内聚的,代码可读性以及维护也提高了。


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

3.4、言归正传-设计模式

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

答案是肯定的。

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

首先看看看看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进行扩展了,可以在任何时候给树形的数据结构增加任何的功能。

在这里大家可以想想后面如果自己设计一个文件访问类,一个商品类目的访问类如何来更好的实现代码的扩展性和灵活性。在这里就不再一一赘述了。

最后我们在总结一下,今天通过权限的删除功能给大家介绍了一下组合和访问者两种设计模式,阐述了其中的一些主要思想,让大家通过实际项目例子来理解设计模式,告诉大家设计模式并不是奢侈品或仅仅只是为了面试的鸡肋技能,只是大家认识还不够深入而已。