wangjie_fourth

may the force be with you

0%

设计模式

什么叫做设计模式

在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的通用解决方案。在面向对象中,比较有名的书就是由 GOF出的一本书。

为什么需要设计模式

抵御变化

在实际项目中,往往有很多不确定因素,从而导致代码功能需要频繁变化。比如说:业务上功能的不确定、产品经理考虑的不全面、开发人员的设计不合理,等等。设计模式是想达到未来变化的部分不需要修改之前的源码,以避免回归问题;

提高代码质量

根据特定问题选择合适的设计模式,可以保证的可靠性,毕竟之前遇到这种问题的人也是这样解决的;遵循某种特殊的命名方式来表达处理的问题,能够让代码具有更高的可读性。

设计模式的六大原则

单一职责原则(The Single Responsibility Principle

设计模式讲究一个模块、一个类应该专注完成一种责任;如果需要承担多个功能就需要将其拆分成多个模块。这个在 Spring推荐使用构造器模式注入的原因之一,如果你发现这个类构造器含有多个属性就说明这个类功能有点多,需要将其拆分出来了。

开放封闭原则(The Open Closed Principle)

对扩展开放,对修改封闭。意思就是说在设计某个模块时,要注意其未来功能扩展,以达到后来再新增功能的时候,不去修改之前的代码,而是通过添加新代码来实现。
比如说我们想建一个类,来计算各个形状的面积

1
2
3
4
5
6
7
8
9
class Shape {
public float computeArea(String shapeType) {
if("Star".equals(shapeType)){
return ;
}else if("Square".equals(shapeType)){
return ;
}
}
}

假设,后期,你希望再加一个梯形的面积计算,就需要直接修改这个类:

1
2
3
4
5
6
7
8
9
10
11
class Shape {
public float computeArea(String shapeType) {
if("Star".equals(shapeType)){
return ;
} else if("Square".equals(shapeType)){
return ;
} else if("Cipollettic".equals(shapeType)){
return ;
}
}
}

这里就违背了开放封闭原则的封闭原则,我们不希望后期直接修改这个类来增加功能,而是通过扩展来添加新功能。 那么如何来设计一个具有扩展的方法呢?

1
2
3
4
5
6
7
8
9
class ShapeAreaComputerHelper{
private Shape shape;
public ShapeAreaComputerHelper(final Shape shape){
this.shape = shape;
}
float compueArea(String shapeType){
return shape.computeArea();
}
}

然后在每个图形类中,都实现 computeArea方法:

1
2
3
4
5
class Start extends Shape{
float computeArea(){
return ;
}
}

以后我们再需要添加新的图形,就不需要直接修改 ShapeAreaComputerHelper这个类,而是通过扩展 Shape的方式,来达到计算多个图形面积。

这个原则在模版方法模式、策略模式等是很常见的

这个原则其实更多的是针对通过代码,如果你写的是业务代码的话,是很难达到这一点的。这其实就是需求的问题

里氏替换原则(Liskov Substitution Principle)

一个对象在其出现的任何地方,都可以用子类实例做替换。意思是当我用子类替换基类的位置后,软件功能不受影响,也不会导致程序的错误。比如说:你用一个 List类型的对象,可以任意替换成 ArrayListLinkedList

接口分离原则(The Interface Segregation Principle)

模块与模块之间尽量使用接口,而不是具体的类,而且多个专门接口比使用单一的总体接口要好。 接口实现类不应该被强迫实现他不使用的方法。即,一个类实现的接口中,包含了它不需要的方法。应该将接口拆下来分成更小和更具体的接口,有助于解耦,从而更容易重构与更改。

很抽象,具体好在哪里,为什么好,也还没明白
我现在明白了,关键点是模块。比如说后端代码的DAO通常直接用Mapper层直接替代,这其实是一种不好的做好。这里就直接将DAO具体指向来某个类

依赖倒置原则(The Dependency Inversion Principle)

高层次的模块不应该依赖低层次的模块 ,他们都应该依赖于抽象;抽象不应该依赖于具体的实现,具体实现应该依赖抽象。这个概念在软件工程中很常见,比如说:SpringDI、模版方法的优点、好莱坞法则。
假设你想写一个执行器让其可以在不同数据源上执行。那么它 ExecuteWorker就需要依赖于 Loader接口,Loader的实现类有 SQLLoaderRedisLoaderFileLoader
ExecuteWorker具体实现上不应该直接依赖 Loader的实现,而是依赖 Loader接口。这个就是高层次的模块不应该依赖低层次的模块 ,他们都应该依赖于抽象。
ExecuteWorker中的 Loader中也不应该具体指定实现类,比如:Loader loader = new SQLLoader();,这样抽象 Loader就具体依赖其实现类,这里应该让后面使用者自己觉得使用那种实现类,常见的实现方法就是 DI

合成复用原则

在结构型设计模式中,尽量使用组合,而非继承。
这其实跟软件建模有关系,比如说,你的软件中需要建立手机模型。假设,你仅从手机品牌、手机大小这俩个维度建模。如果,你先用继承的方式实现手机品牌这个维度的话,那就要建立几十个手机品牌实现类,此时你再实现手机大小这个维度的话,那就是建 m * n个实现类。而如果你使用组合的方式来实现的话,实现类仅 m + n个。

设计模式

GOF将设计模式分为三大类,23种:

  • 创建型模式:5种
    工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
  • 结构型模式:7种
    适配器模式、桥接模式、装饰器模式、代理模式、外观模式、组合模式、享元模式
  • 行为型模式:11种
    策略模式、模板方法模式、观察者模式、责任链模式、命令模式、迭代器模式、备忘录模式、访问者模式、解释器模式、状态模式、中介者模式

我理解的设计模式

设计模式往往有一种很虚的感觉,因为在平常开发中,通常都是不规范的。你的需求调研很严谨吗?你的系统设计很合理吗?答案往往都是否定的,这些不确定因素让设计模式的优点显现不是那么明显。
(1)确定什么是变的,什么是不变的
在学习每个设计模式后,要明白哪些是变化的,哪些是不会变化的。根据实际业务场景,我们可以根据不变的部分,选择合适的设计模式。

(2)总体难度往往都是一样的
完成一个事情的难度是一样的,这有点像守恒定律一样。解决某个实际业务场景而使用的设计模式往往是将后期可能变化的地方提取出来,让这部分变化不会影响到之前代码功能。但是如果你使用错误的设计模式,比如说:原本设计那些不变的部分突然变化起来了,而那些设计变化的部分却又不变了。这会使得后期维护变得非常苦难。

(3)解耦
设计模式大概都在解决某些对象过于耦合,想办法让他们解耦。而解耦在面向对象设计中,通常意味着增加一个接口。增加接口后,再使用组合、中间层等概念来具体实现。
比如说:你新增加一个接口后,然后发现其多个实现类有重复代码。就可以把这些重复代码提取成一个 Abstract类。

(4)现实场景往往是更复杂
在现实开发中,通常需要将几个设计模式组合在一起使用。比如说:行为型模式中的对象通常会采用创建型模式来创建;比如说模版方法通常是跟多态一起结合使用的。

(5)重构代码实现
尽量将需要重构的代码抽象成通用的方法,然后提取这个通用的方法。难点往往就是如何将需要重构代码抽象成通用方法。

(6)有些设计模式在业务上是难遇见的

  • 一些设计模式慢慢成为语言标准;比如说:迭代器模式
  • 一些设计模式难点太高,在业务代码上,为了后续维护可能就不这么写;比如说:解释器模式

(7)Context概念

  • 里面可能会存储一些初始化参数
  • 可能会放一些中间产生的数据、状态
  • 注意在多线程的使用;比如是不是要用一些线程安全容器,比如说 CopyOnWriterList

    通常也称为上下文的概念

(8)有些设计模式是很容易识别的

  • 装饰器模式:其内部通常都会有一个原本功能的类,偶尔也能看见它实现相同的接口