常用设计模式

OOP基本设计原则

  • 单一职责原则(Single Responsibility Principle(SRP)),尽量保持类的功能单一。

  • 里氏替换原则(Liskov Substitution Principle)

    里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:

    • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
    • 子类中可以增加自己特有的方法。
    • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
    • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
  • 依赖倒置原则(Dependence Inversion Principle),抽象不应该依赖细节,细节应该依赖于抽象,针对接口编程,而不是针对实现编程。例如A模块调用B模块,在实现A的时候,应该保证A无需关心B模块的具体实现,只需要知道B模块提供的接口就可以完成A模块的编码。

  • 接口隔离原则(Interface Segregation Principle),避免出现一个接口中包含太多方法的情况,否则会导致有的client依赖它并不需要用到的接口方法。

  • 迪米特原则(最少知识原则 Principle of Least Knowledge),实现模块A/类A的时候,应该尽量减少A对其他模块的依赖,也就是要做到高内聚、低耦合。

  • 开闭原则(OpenClosed Design Principle),模块、类、方法/函数应当是对扩展(新功能)开放,对修改闭合。即如果模块A是基于该原则实现的,那么外部在扩展A的功能的时候,可以做到不修改A原有的代码。
    例如,实现一个播放器,可以提取出这个播放器基本的功能,以接口形式暴露,然后提供默认的播放器实现这个接口,如果外部需要接入自己的播放器,则只需要实现接口即可,无需再修改原先播放器模块的代码。

    开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。

    其它的5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。

  • 多用组合,慎用继承,除非有明显继承关系的,否则都优先考虑组合来实现。

  • DRY (Don’t repeat yourself),抽象成可复用的代码块,如果有两处以上相同的代码块,考虑把它们抽象成一个单独的方法;或者多次使用了 硬编码的值,考虑把它们设置成公共常量。但是需要注意的是,可被抽取的代码块除了代码实现类似,还应该在业务上是用于处理类似问题的,否则可能会因为之后的业务差异越来越大,代码不再可复用。

  • 封装经常修改的代码,在代码迭代过程中,注意有些代码模块的变动频率明显高于其他,则这一部分代码可以被单独提取出来。

策略模式

使用场景:有多个类似的类/实例,唯一的区别是某些行为的不一致。

目标:封装一系列的算法,使他们可以动态得配置到目标类中。

注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。

观察者模式

使用场景:一个对象的状态被多个对象所依赖,该对象的状态的改变会影响到多个对象。

目标:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

装饰器模式

使用场景:需要给一个类添加额外的功能或者属性。相比于直接使用继承,组合的形式更灵活。

目标:在不修改基础类的前提下扩展基础类的功能,修改基础类通用接口的实现。

注意事项:避免装饰层数过多

具体实现:

工厂模式

使用场景:我们明确地计划不同条件下创建不同实例时。

目标:以统一的接口创建不同对象,创建过程与具体创建的对象对客户端透明。

具体实现:让子类实现工厂接口,create方法返回抽象的产品。

单件模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SingleObject {
private static SingleObject instance = new SingleObject();
private SingleObject(){}
public static SingleObject getInstance(){
return instance;
}
}

// 懒加载
public class Singleton {
// volatile避免可见性问题
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

命令模式

使用场景:需要解耦行为的请求者与实现者,或者对多个行为有较强的管理需求的,可以使用命令模式将行为(请求)封装成一个对象。

具体实现:

image-20180425210304501

细节解释:

1、Invoker可以通过一个栈保存已经执行过的Commond实例,这样可以添加undo()操作以精确回退到某一个命令。

2、可以实现一个持有多个Commond的类,用来表示组合命令。

3、Commond需要持有Receiver实例,而Commond是在invoker中实例化的,如果Receiver与Invoker是一一对应的(多数情况下如此),那么Invoker就需要持有Receiver实例,而不是每次都重新与Commond一起实例化。

适配器模式、外观模式

适配器模式:转换不同的接口或者转换不同的数据。

外观模式:封装多个接口,对外界提供更简洁清楚的接口。

模板方法模式

实现:

  • 将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
  • 子类通过实现父类的抽象方法可以控制父类方法具体实现。方法调用由父类发生,子类仅提供方法执行所必须的信息;即允许低层组件将自己挂钩到高层组件上,高层组件决定如何以及何时使用这些低层组件。

迭代器模式

提供一种方法顺序访问一个集合对象中的各个元素,而又不暴露其内部结构。典型实现如JDK中的迭代器。

组合模式

组合模式将对象组合成树形结构用以表现“整体/部分”层次结构。对象A和对象组B(包含一组A),均实现同一个接口,这样对于客户端来说,无需去差分A与B。

状态模式

当一个类(Context)需要根据自身状态对同一个调用做不同响应的时候,可以考虑使用状态模式。状态模式将改变状态的操作封装到具体的状态类中,而Context类只负责提供外层调用接口和自身状态信息的维护。

代理模式

1、通常意义上的代理,转发实际服务的请求与结果,做一些数据上的转换,作为服务器与客户端通信的媒介。

2、虚拟代理,除去转发功能,也可以承担一部分服务器的功能。比如代理去请求服务器数据,但是服务器运算时间很久,此时代理会先给客户端返回一个默认的数据(行使一部分服务端的功能),等服务端数据到达后再转发。

反模式

每个程序员要注意的 9 种反模式

桥接模式

如果一个类有两个可扩展的维度,则将这两个维度抽象出来,并在抽象层定义他们之间的关系,这样使得这两个维度都易于扩展都变得易于扩展。

有这样一个需求,Interface有两个抽象方法A、B(对应于上述的两个可变化的维度),要求它的具体实现中,方法A、B各自可能相同也可能不同,普通的实现如下:

可以发现如果出现B相同A不同或者A、B都不相同的情况,会导致类继承结构非常复杂。此时就可以使用桥接模式,将A、B单独抽象为一个独立的抽象类、接口,使他们通过组合联系起来。实现如下:

生成器模式

将某个类的构造赋值过程封装在一个特定的Builder类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static class Builder {
private RxImagePreview preview;

public Builder() {
preview = new RxImagePreview();
}

public Builder item(FileItem item) {
preview.fileItem = item;
return this;
}

public Builder reviewId(long id) {
preview.reviewId = id;
return this;
}

public RxImagePreview build() {
// 这里可以添加一些参数校验或者默认值赋予的逻辑
return preview;
}
}

责任链模式

当你想要一个以上的对象有机会处理某个请求的时候,就可以考虑使用责任链模式。

从下图的具体实现中可以看出,所有的Handler形成了一个链表,事件由此逐步传递处理。

蝇量模式

如果想要让某一个类的实例能提供许多的“虚拟实例”,就可以考虑使用蝇量模式。使用该模式可以减少运行时的对象个数,减少内存,同时也可以集中式的管理“虚拟对象”的状态。

比如我们需要绘制很多棵树,每棵树唯一的不同点在于坐标不同,那么就可以只实例化一个树对象,然后用集合保存每棵树的坐标值,每次渲染的时候就给树对象赋值然后绘制。

中介者模式

可以用来集中相关对象之间复杂的沟通和控制方式。

假如多个对象之间存在复杂的相互关系,如下图:

在加入中介者之后,所有的对象都不直接与其他对象交互,而是通过中介者通信,中介者维护各个对象之间的关系。这样可以使对象之间的关系更加清晰。

备忘录模式

当你需要让对象返回之前的状态时,可以使用备忘录模式。

具体实现就是用另外一个专用的备份类,来存储需要被存储的对象的状态,下次恢复对象时传入这些状态参数。

典型的如Android Framework中的onSaveInstanceState方法的设计。

原型模式

当创建给定类的实例的过程很昂贵或很复杂时,使用原型模式。在Java中,其实就等价于使用clone()或者反序列化方式创建一个新的实例。

访问者模式

访问者适合于数据结构相对稳定的系统。它把数据结构和作用于结构之上的操作之间的耦合解脱开来,使得操作集合可以相对自由地演化。

访问者模式的目的是要把处理从数据结构分离出来。如果这样的系统有比较稳定的数据结构,又有易于变化的算法的话,使用访问者模式就是比较适合的,因为访问者模式使得算法操作的增加变得容易。

那其实访问者模式的优点就是增加新的操作很容易,因为增加新的操作就意味着增加一个新的访问者,访问者模式将有关的行为集中到一个访问者对象中。

那访问者的缺点其实也就是使得增加新的数据结构变的困难了。

结合下面的代码,Wheel、Engine就代表稳定的数据结构,而Visitor则代表具体的操作,可见Visitor是易于扩展的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
interface Visitor {
void visit(Wheel wheel);

void visit(Engine engine);

void visit(Body body);

void visit(Car car);
}

class Wheel {
private String name;

Wheel(String name) {
this.name = name;
}

String getName() {
return this.name;
}

void accept(Visitor visitor) {
visitor.visit(this);
}
}

class Engine {
void accept(Visitor visitor) {
visitor.visit(this);
}
}

class Body {
void accept(Visitor visitor) {
visitor.visit(this);
}
}

class Car {
private Engine engine = new Engine();
private Body body = new Body();
private Wheel[] wheels = { new Wheel("front left"), new Wheel("front right"), new Wheel("back left"),
new Wheel("back right") };

void accept(Visitor visitor) {
visitor.visit(this);
engine.accept(visitor);
body.accept(visitor);
for (int i = 0; i < wheels.length; ++i)
wheels[i].accept(visitor);
}
}

class PrintVisitor implements Visitor {
public void visit(Wheel wheel) {
System.out.println("Visiting " + wheel.getName() + " wheel");
}

public void visit(Engine engine) {
System.out.println("Visiting engine");
}

public void visit(Body body) {
System.out.println("Visiting body");
}

public void visit(Car car) {
System.out.println("Visiting car");
}
}

public class VisitorDemo {
static public void main(String[] args) {
Car car = new Car();
Visitor visitor = new PrintVisitor();
car.accept(visitor);
}
}

参考资料

design pattern 包教不包会

Head First 设计模式(中文版)