Willow's blog

设计模式归纳

面向对象设计有几个原则(开闭、单一等),如果能够按照这些原则来开发软件,就能充分发挥面向对象的好处(低耦合,易扩展)。设计模式正式这些原则的体现,它是前人编码过程中的经验总结,针对某些问题而提出的解决方案。

最经典的设计模式出自《设计模式-可复用面向对象软件的基础》,针对不同具体问题,书中提出了23个设计模式,可大致分为三类:创建模式结构模式行为模式

一、创建模式

「单例、工厂、抽象工厂、原型、建造者」

开发中会用到很多对象、最佳的使用对象的方式,是遵循面向对象的一个原则“==针对接口编程==”,即在程序代码中出现的对象,都是接口类型,而不要出现具体的类,创建型模式大部分都是为了解决这个问题,即:对象创建交给专门的几个类去负责,创建完之后交给用户使用,用户得到的是一个接口类型,既不必知道这些类具体是什么(只要知道它们都是某个接口的实现类),也不必知道是如何创建它们的。

1. 单例模式(Singleton)

「创建唯一的一个对象」

  • 设计:
1
2
3
4
5
6
7
8
9
class A{
private static A single=null;
public static A getSingleton(){
if(null==single){
single=new A();
}
return single;
}
}
  • 客户端:
1
A a = A.getSingleton();

注意

如果是并发环境中、或者在好几个虚拟机之间创建唯一的对象,则要再用其他的方法,如并发控制、rmi等。

2. 工厂模式(Factory)

「最常见的一种模式,可在java类库中大量见到,用户只需要调用工厂方法类的相应方法即可创建对象」

  • 设计:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Product 产品接口
interface Product{}

//具体产品A
class ProductA implements Product{}

//Factory 工厂接口
interface Factory{
Product create();
}

//class FactoryA implements Factory{
@Override
public Product create(){
return new ProductA();
}
}
  • 客户端:
1
2
Factory factory = new FactoryA();
Product product = factory.create();

说明

  1. 要修改Product对象的创建方式,客户端代码并不受影响
  2. 要创建另一种Product对象(如ProductB),则创建一个Facrory的子类FactoryB,然后用这个对象来创建ProductB。虽然也修改了代码,但是仅仅限于创建Factory对象的地方,其他的业务逻辑部分并没有修改(业务逻辑指用到了Product类型的地方)
  3. 工厂方法的一种改进,叫做静态工厂方法,如果要创建另一种Product对象,把该过程写在工厂类的静态方法里,这在jdk类库里大量使用

3. 抽象工厂(Abstract Factory)

「创建一组对象」

上面的工厂方法其实并没有体现出好处,因为每个工厂只创建一种对象,但是如果需要创建一批对象,好处就体现出来了。抽象工厂方法适用于类似下列情形:比如要更换程序界面的显示风格,如窗口、按钮、滚动条等。如果不用抽象工厂,就得一个个对象都替换掉,这显然不简单。实际上高层业务逻辑也只是针对这些窗口或者按钮的接口进行编程,抽象工厂可以很容地把这些对象立刻换成另外一批。

  • 设计:
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
//窗口对象接口
interface Window{}

//某种风格A的窗口
class WindowA implements Window{}

//按钮对象接口
interface Button{}

//某种风格A的按钮
class ButtonA implements Button{}

//工厂接口
interface Factory{
//创建窗口
Window createWindow();
//创建按钮
Button createButton();
}

//生产某种风格A对象的工厂
class FactoryA implements Factory{
@Override
public Window createWindow(){
return new WindowA();
}
@Override
public Button createButton(){
return new ButtonA();
}
}
  • 客户端
1
2
3
Factory facory = new FactoryA();
Window window = factory.createWindow();
Button button = factory.createButton();

4. 原型模式(Prototype)

「对象可以自身复制出新的对象」

适用场景:创建的对象很多属性都差不多,只需要在其他对象上修改一小部分。很多编程语言支持这种方式创建对象,比如java中每个对象都有clone()方法,那么在这样的语言里,不用专门写这种设计模式、利用自带方法即可。

  • 设计
1
2
3
4
class A{
//复制自己的实例
A clone(){}
}
  • 客户端
1
2
3
A a = new A();
A b = a.clone();
//下面b可修改一些属性,成为一个新的对象

5. 建造者模式(Builder)

对象的创建其实是一个非常复杂的过程,可能需要先创建一些零件,最后再把这些零件组装起来,建造者模式把组装这个行为分离出来,使得相同的零件,可以用不同的组装方法。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*创建一个个零件类*/

//假设A、B是零件的接口
class Builder{
A createA();
B createB();
}

//定义零件组装过程
class Director{
Builder builder;

Director(Builder builder){
this.builder = builder;
}

void assembly(){
A a = builder.createA();
B b = builder.createB();
//下面就是根据某种方法,将a和b组装起来
}
}
  • 客户端
1
2
3
Builder builder = new Builder();
Director director = new Director(builder);
director.assembly();

说明

这里的Builder和Director完全可以定义成接口,利用前面讲的工厂方法等来提供,也就是说设计模式之间是可以组合使用的,最终可以使得客户端中不存在new这种显示创建对象的方法。

小结

  • 创建型模式是设计模式的基础,只有把这个问题解决了,上层模块才能完全对接口进行编程,想要哪个对象,直接调用某个创建型模式对象的方法就可以了。细心看上面的例子可以发现:尽管业务对象可以通过工厂类对象等创建,没有显示出现new关键字,但是工厂类对象本身却出现了new关键字
  • 有==两种方法可以彻底消灭new==:
    1. 把创建这些工厂类的代码聚集到一起,使变化局限于局部;
    2. java中有“反射”机制,可以把创建工厂类的方式通过配置文件来提供,这样就完全避免了修改代码,完全避免了代码中出现new,这是最佳的解决方案:即工厂对象由配置文件创建,业务对象由工厂对象创建。很多框架,例如==Spring==,就是采取这种方式来提供对象,它有个时髦的名字叫做“==依赖注入==”。

二、结构模式

「适配器、桥接、组合、装饰、外观、享元、代理」

结构型模式最能体现设计的精妙,从中可以学到不少智慧。针对特定的问题,这些模式的解决思路很有借鉴启发的作用。

1. 适配器(Adapter)

「转接口」

适用于这样的情况:用户(或者高层业务逻辑)需要一种接口来调用某种功能,手头已经有能够实现这种功能的类,但是接口与用户需要的不一致,这时候就可以创建一个类似于“转接口”的类使得用户接口和已有库的接口能够对接,而不是仅仅因为接口不一致而重新开发。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//客户想要的接口
interface A{ void function(); }

//已经存在的接口
interface Old{ void func(); }

//设计一个适配器
class Adapter implements A{
Old o;

@Override
function(){
o.func();
}
}
  • 客户端
1
2
3
A a = new Adapter();
a.function();
//对于客户来说,它并不知道,也没有必要知道这期间是否进行了转接,能用就行

2. 桥接(Bridge)

「解决类多方向发展带来的结构僵化问题」

桥接模式是相对比较晦涩的一个,它并不是明显针对某个问题,我觉得他的思想更重要。假设有这样的情形:一开始需要设计一个运行在PC上软件,能够显示文本和图像,则可以设计一个接口:interface Software { void show(); },以及两个子类:class TextSoftware implements Software{},class ImageSoftware implements Software{},这个时候结构并没有什么问题;下面又有了新需求,把这个功能移植到手机上,那么有人可能会这样设计:TextSoftware做两个子类:TextSoftware4Pc和TextSoftware4Phone,ImageSoftware也做两个子类:ImageSoftware4Pc,ImageSoftware4Phone,这样貌似也能解决问题,但是这会带来僵化问题:如果现在又有了新的需求,定制两个Software版本,一个为成人的,一个为儿童的,当然也要既有Pc的,也有手机的,那么在上面的基础上继续继承的话,可以这么做:TextSoftware4Pc下面派生两个子类TextSoftware4PcAdult和TextSoftware4PcChildren,TextSoftware4Phone派生两个子类TextSoftware4PhoneAdult和TextSoftware4PhoneChildren,ImageSoftware4Pc派生两个子类ImageSoftware4PcAdult和ImageSoftware4PcChildren,ImageSoftware4Phone派生两个子类ImageSoftware4PhoneAdult和ImageSoftware4PhoneChildren。从这里应该就可以隐约感觉到,这种基于继承的结构,已经开始存在危机了,如果再增加新的需求,这个类继承层次就会非常的复杂,给软件以后维护带来很大的难度。分析下需求的变化可以看到,需求同时朝着三个方向变化:1.文本显示功能和图像显示功能;2.Pc平台和手机平台;3.成人版本和儿童版本;这三个方向的高层逻辑变化各自是独立的,彼此之间并没有先后继承关系,只是在最后整合的时候才会关联。桥接模式就是针对这种一个类同时朝着几个方向发展的结构问题,它根据每个变化方向设计一个接口,分别演化,各方向之间通过组合的方式结合在一起,这样的结构就清晰多了。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//软件功能
interface Software{ void show(); }
class TextSoftwareimplements Software{
@Overrride
void show(){}
}
class ImageSoftware implements Software{
@Override
void show(){}
}

//使用群体
interface User{}
class AdultUser implements User{}
class ChildrenUser implements User{}

//平台
interface Platform{}
class PcPlatform implements Platform{
Software soft;
User user;
//下面可以自己组合这些业务元素
}

注意

我这里是以平台为基准进行组合,也可以以使用群体为基准进行组合,或者说组合的方式是灵活的,可以很容易变化。每个变化方向之间通过“组合”的方式,类似于在各个方向之间建立了一座桥梁,故这个模式叫做桥接模式。这个模式的意义在于:它说明了==继承的劣势==,==组合的优势==,实际上,每一个类都需要在其他类的基础上进行工作,有时候需要进行功能方面的继承,有时候需要进行结构方面的继承,正确的方式应该是:继承用于功能继承,组合用于结构继承,即继承是为了使用父类中的方法和字段,省的子类再重复地写,而组合是为了使用父类的结构,好像零件的组装,把一个个零件做好了之后装在一起,然后又可以拆卸,而不是一股脑把所有零件浇筑在一起。所谓组合优于继承,就是指这个。

3. 组合(Composite)

「表示具有层次关系的一类结构」

组合模式相对来说好理解一些,数据结构中的树就是典型的组合模式,树的每一个结点都可以包含另一些结点

  • 设计
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
//结点接口
interface Node{
void add(Node n);
void remove(Node n);
}

//叶子结点
class Leaf implements Node(){
@Override
public void add(Node n){}

@Override
public void remove(Node n){}
}

//非叶子结点
class Stem implements Node{
private List list;

@Override
public void add(Node n){
list.add(n);
}

@Override
public void remove(Node n){
list.remove(n);
}
}
  • 客户端
1
2
3
4
5
6
7
8
9
Node root = new Stem();
Node oneStem = new Stem();
Node leaf1 = new Leaf();
Node leaf2 = new Leaf();
Node leaf3 = new Leaf();
oneStem.add(leaf1);
oneStem.add(leaf2);
root.add(oneStem);
root.add(leaf3);

4. 装饰(Decorator)

「给子类方便地添加一些功能」

适用于这种情况:一个类要添加一点点功能,这种功能可以动态地随时添加,随时删除,这时候就不能用派生子类的方法,因为一旦派生一个子类,里面的结构就是静态的,而且本身也不鼓励使用继承。比如说有了一个窗口类,现在仅仅是要给这个窗口加个边框,或者换一个边框,但不能改变窗口类的接口,也就是说,加或者没加边框,用户看起来都是同一个接口。

  • 设计
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
//窗口
interface Window{ void display(); }
//具体的一个窗口
class AWindow inplements Window{
@Override
public void display(){}
}
//一个装饰类
class Decorator implements Window{
// 包含一个窗口
Window win;
//区别代理模式:往往被装饰的对象由构造器传入,而代理模式则完全隐藏内部对象的构造过程
public Decorator(Window win){
this.win = win;
}

@Override
public void display(){
//先加一个边框
addBorder();
//展示本来win里面的内容
win.display();
}
private void addBorder(){}
}
  • 客户端
1
2
Window a = new Decorator(); //这里不考虑怎么给内部win对象的赋值,这个属于创建型模式需要解决的问题,下同
a.display();

注意

从这里可以看到,a对象根本没觉察到赋给它的那个对象是原始对象还是装饰过的,反正接口是一样的。装饰模式在java库里有很多应用,比如说io包里的各个输入输出流都是包装过的,采用了装饰模式。这里的“装饰”可以有很多应用场景,比如说j2ee里的Servlet类是用来处理客户端http请求的,那么我们实际上可以设计一个Servlet的装饰类,让它做一些前期处理,或者后期处理,这个概念类似于过滤器(但过滤器自身的前置和后置处理是采用了职责链模式[以后介绍])。

5. 外观(Facade)

「把一组功能集中起来,提供一个统一的接口」

外观模式相对比较好理解,客户需要操作一组功能,需要牵涉到好几个类或者接口,那么我们可以把这些接口集中起来,通过一个类来向用户提供。这个模式的更大意义是,这个外观类可以更好地组织那些功能,以更好的方式提供给客户。

比如说需要构建一个软件,构建是一个系统工程,需要进行预处理、编译、链接、部署等等,但对于客户来说,他可以亲自动手一步步做,也可以交给一个构建程序(比如make)来做,他只要调用基本的命令(make)就行,以后的任务交给make来做,而且make做的比他本人一步步做可能效果更好

  • 设计
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
//预处理
interface Prepare{ void doPrepare(); }
//编译
interface Compile{ void doCompile(); }
//链接
interface Link{ void doLink(); }
//清除
interface Remove{ void doRemove(); }

//外观类
class Facade{
Prepare p;
Compile c;
Link l;
Remove r;

public void build(){
p.doPrepare();
c.doCompile();
l.doLink();
}

public void remove(){
r.doRemove();
}
}
  • 客户端
1
2
3
4
5
Facade f = new Facade();
//构建软件
f.build();
//清除软件
f.remove();

注意

Facade类与各个功能类之间是组合关系,是松耦合的,很容易变化

6. 享元(Flyweight)

「有效解决使用大量重复的小对象的问题」

享元模式适用于这种情况:比如说设计一个文档编辑器,里面的基本对象是字符,简单的想法是每个字符为一个对象,存储字符的值以及放置的位置。这样似乎解决了问题,但是还是有可以优化的地方:一篇文档中存在大量相同的字符,相对应的这些字符对象仅仅是位置不同,这里似乎可以用“共享”的方式:如果遇到一个字符,先看这个字符对象是不是存在,不存在就创建,如果已经存在了,就使用这个对象,修改一下位置的值,然后画出这个字符。

具体在实现享元模式时,可以指定对象是否可以共享,如果不能共享,就每次都创建新对象。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//字符接口
interface Character{}
//两类字符对象:可以共享的;不可以共享的
class ShareCharacter implements Character{}
class UnshareCharacter implements Character{}

//创建享元对象的工厂
class CharacterFactory{
Character getChar(char c){
Character c;
//根据某些条件判断,是共享一个对象,还是生产一个新的对象
...
return c;
}
}
  • 客户端
1
Character ch = CharacterFactory.getChar('a');

注意

这里客户端只是调用工厂方法来生产字符对象,但并不知道这个字符是新创建的对象,还是已经存在的对象

7. 代理模式(Proxy)

「控制对一个对象的访问」

适用于这种情况:要访问一个对象,但又要做一些限制或者控制,或者不能直接访问到这个对象,好比网络服务中的代理服务器。那么可以在访问者和被访问者之间建立一个代理类,访问者向代理类发出请求,由代理类负责来访问。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//需要访问的对象接口
interface Subject{ void operation(); }
//实际访问的类
class RealSubject implements Subject{
@Override
public void operation(){}
}
//代理类,提供相同的接口
class Proxy implements Subject{
Subject real;

@Override
public void operation(){
//做一些连接等准备工作
...
//调用真正需要访问的类的功能
real.operation();
}
}
  • 客户端
1
2
Subject obj = new Proxy();
obj.operation();

三、行为模式

「职责链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者」

行为模式关注的是对象间的一些协作关系。

1. 职责链(Chain Of Responsibility)

「任务的转发」

适用于以下的场景:客户给出一个任务,但并不知道、也不想知道这个任务谁去执行,于是交给一个链实例,这个链实例会判断任务是否由其本身完成,如果不是,就转发给下一个实例,依次类推,直到有实例处理它或便利了所有的链实例都询问完成

网络中的数据包传递,就是这个原理,比如我要把一个消息发到某一个ip,我其实并不需要直到这个ip究竟在哪里,只要发给路由器,路由器判断这个数据包是否属于我管的这个范围,如果是就收下来,转发给下面的子网,如果不是就转发给另一个路由器;子网里面也是这个工作方式,数据包被一站一站地转发,直到最后到达目标ip的计算机。

更为贴切web开发的例子是web开发中的过滤器,FilterChain和chain.doFilter这样的特征代码就是典型的职责链模式

另一个例子是阿里的Druid数据库连接池,使用该模式对JDBC做了扩展

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//操作总接口
interface Handler{
//转发
void transfer(Handler next);

//处理消息
void operation();
}
//具体类
class HandlerA impleements Handler{
@Override
public void transfer(Handler next){}

@Override
public void operation(){
//先判断是否由自己来处理
if(isMyDuty){
doSomething();
}else{
//如果不是,则转发
transfer();
}
}
}
  • 客户端
1
2
Handler start = new HandlerA();
start.operation();

注意

可以看到,客户只要简单地把任务交给第一个类,下面就可以自动地进行转发和处理,客户无需关心具体是哪个类来处理的,或者是消息是如何发到那个类的,因为接口都是一致的。

2. 命令(Command)

「暂缺,该模式用的不多」

  • 设计
1
2


  • 客户端
1
2


注意

3. 解释器(Interpreter)

「给定一种语言,定义一个解释器来解释」

可以用在以下场景:如果要设计一个编程语言的解释器,该语言(简单点)具有两种符号:终结符和非终结符,非终结符中包含有其他的符合序列。

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 语言的上下文
class Context{}

// 解析接口
interface Expression { void interpret(Context text); }

// 终结符解释方法
class TerminalExpression implements Expression{
@Override
public void interpret(Context text){}
}

// 非终结符解释方法
class NonterminalExpression implements Expression{
// 非终结符里包含有其他符号
List<Expression> exp;

@Override
public void interpret(Context text){}
}

4. 迭代器(Iterator)

适用于以下场景:客户需要依次遍历一个数据集合中的元素,但并不清楚这个集合里元素的排列顺序,也不想知道。如果知道了这个集合中的结构,把遍历方法写死在代码里,那么反而不好。迭代器模式给出了遍历集合元素的一个接口,同时又不暴露集合内部的实现细节。

再进一步,采用直接访问集合元素的方法,即使集合结构没有变化,也会导致问题,比如说利用数组实现的线性表存储元素,一开始我们通过数组下面遍历里面的元素,后来需求变了,要求倒序访问,那么遍历的代码就要修改,这违反了开闭原则。而如果使用迭代器的话,客户代码无需变化,因为客户根本就不知道这些元素是什么顺序提供的。

  • 设计
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
// 迭代接口
interface Iterator{
Object next();
boolean hasNext();
}

// 数据元素集合
interface DataSet { Iterator getIterator(); }

// 数据元素具体的实现
class DataSetA implements{
// 具体里面的数据表示省略了
// 得到一个迭代器
@Override
Iterator getIterator(){
return new DataSetAIterator(this);
}
}

// 专门为DataSetA定义的迭代器
class DataSetAIterator implements Iterator{
DataSetA data;

DataSetAIterator(DataSetA data){
this.data=data;
}

@Override
public Object next(){
// 按某种方法取得this.data中的下一个元素
}

public boolean hasNext(){
// 根据某种方法判断有没有下一个元素
}
}
  • 客户端
1
2
3
DataSet a = new DataSetA();
Iterator ite = a.getIterator();
Object obj = ite.next();

迭代器模式的实现相对是比较复杂的,所以还是最好参考UML类图为好,不过使用起来是很简单的,java语言内置了迭代器,比如说Collection框架中的所有数据结构类,都实现了Iterator接口,都可以直接使用jdk提供的迭代器。这23中设计模式里有一些是java语言内置提供的,从这个意义上说,这些模式应该是比较重要和常用的。

5. 中介者(Mediator)

「简化一系列对象间的通信交互」

面向对象中有一个单一职责原则,按照这个原则来编程,对象负责什么一目了然,但往往会形成很多个对象,对象间的通信会变得很复杂,因为每个对象都需要知道和它通信的是哪些对象,如果采取中介者,则对象与另一个对象的交互问题,可以交给这个中介者,对象之间可以不必知道对方是谁,在哪里。

  • 设计
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
// 中介者接口
interface Mediator{
void sendTo(String message, String name);
}

// 联合国是一个中介者
class MediatorUN implements Mediator{
//联合国需要知道每一个国家
List<Country> country;
public MediatorUn(List<Country> country){
this.country = country;
}

@Override
public sendTo(String message, String name){
//根据name选择一个Country,然后发送消息
}
}

// 国家抽象类
abstract class Country{
Mediator med;

// 把消息message发送给名字叫name的国家
void send(String message, String name);
}

// 中国
class China extends Country{
@Override
void send(String message, String name){
// 把消息和转发对象发给中介者,自己并不需要知道另一
// 国家的具体情况
this.med.sentTo(message, name);
}
}

// 美国
class USA extends Country{

@Override
void send(String message, String name){
this.med.sentTo(message, name);
}
}

有了中介者模式,对象间的交互就变得非常简单,简单地把消息交给中介者就行了。

6. 备忘录(Memento)

游戏都能存档读档,但你想过这个是如何实现的吗?备忘录模式就是来做这个的

  • 设计
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
// 备忘录,或者是存档
class Memento {
// 对象的状态,简单的用String表示
String state;

void setState(String state){
this.state=state;
}

String getState(){
return this.state;
}
}

// 存档管理者
class MemManage{
// 创建一个存档对象
Memento createMemento(){
return new Memento();
}

// 保存
void saveMem(Memento mem){}

// 获取某一个存档
void getMem(/*参数省略了,取决于实现方法*/){}
}

// 游戏程序,可以存档
class Game{
String state;

// 存档
void save(Memento mem, String state){
mem.setState(state);
}

// 读档
void load(Memento mem){
this.state=men.getState();
}
}
  • 客户端
1
2
3
4
5
6
7
Game game=new Game();
MemManager manager=new MemManager();
//存档
Memento m=manager.createMemento();
game.save(m, “存档1”);
manager.savaMen(m);
// 读档同上

7. 观察者(Observer)

监听

这个模式比较好理解,Swing里面的控件监听机制就是采用这个实现的。一个对象A需要监听另一个对象B的状态,而对象C也在监听B的状态,怎么采取一种好的方法,来实现B的状态变化之后,会通知到A和C,让它们做相应的动作,这便是观察者模式。A和C是观察者,B是被观察者。

  • 设计
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
// 观察接口,所以观察者都要实现该接口
interface Observer { void operation(); }

//观察者A
class ObserverA implements Observer{
@Override
public void operation(){
// 收到通知后做相应的事
}
}
//观察者B
class ObserverB implements Observer{
@Override
public void operation(){
// 收到通知后做相应的事
}
}

// 被观察者接口
interface Subject {
// 加入一个观察者
void add(Observer o);

// 删除一个观察者
void del(Observer o);

// 通知观察者
void notify();
}

// 被观察对象C
class SubjectC implements Subject{
// 比如用一个线性表存储观察者们
List<Observer> obs;

@Override
public void add(Observer o){
obs.add(o);
}

@Override
public void del(Observer o){
obs.remove(o);
}

@Override
public void notify(){
int len=this.obs.length;
for(int i=0; i<len; i++){
Observer tmp=this.obs.get(i);
tmp.operation();
}
}
}
  • 客户端
1
2
3
4
5
6
7
8
9
10
11
12
// 建立被观察对象
Suject sub=new SubjectC();

// 建立两个观察者
Observer a=new ObserverA();
Observer b=new ObserverB();
// 向被观察对象注册
sub.add(a);
sub.add(b);

// 发生了一些事件,被观察者通知各观察者进行各自相应的动作
sub.notify();

观察者模式是一个重要的接口,java swing里面的事件监听机制就是使用这个模式的典型代表,swing里面把“观察”叫做“监听”,比如说一个按钮按下之后,一个Label需要显示一些文本,另一个Label需要显示图像,那么按钮就是一个事件源,两个Label就是监听器。可以在按钮里这么写button.addActionListener(new ActionListener()); 这里的addActionListener()方法就相当于这里例子里的add,ActionListener就是这里的Observer接口。Java里面也自带了观察者模式的接口和类,可以直接使用,但我感觉并不实用,一来这些类的接口定死了,不适用于具体情况,二来这个模式很好写,没必要用系统带的。

8. 状态(State)

考虑这样一个应用:设计一个汽车前灯的开关程序,它有三个状态变化:关->近光灯->远光灯->关,也就说每按一下,就会变成下一个状态,你会如何设计?如果不考虑面向对象的设计,一般人都会这么设计:用if语句来判断,如果当前是“关”,则按下之后,变为近光,如果是近光,按下之后变远光。。。,这种做法有两个缺点:1、会有很多if语句,2、难以适应新的变化,比如中间又加了两个新状态(朝左照射和朝右照射),那么众多if语句就要做不少修改。

如果又加了一个新需求,要让用户能够自定义转换的顺序,比如说从关到远光,那么原来的if语句的代码,基本就要废弃重写,而状态模式可以轻松解决上述问题。

  • 设计
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
// 定义状态接口
interface State {
void operation(Switch s);
}

// 定义三种状态
class Off implements State{
@Override
public void operation(Switch s){
// 执行关灯的操作
s.doOff();
// 改变一下状态
s.state=new Near();
}

}

class Near implements State{
@Override
public void operation(Switch s){
// 执行近光灯的操作
s.doNear();
// 改变一下状态
s.state=new Far();
}
}

class Far implements State{
@Override
public void operation(Switch s){
// 执行远光灯的操作
s.doFar();
// 改变一下状态
s.state=new Off();
}
}

// 开关类
class Switch {
State st;

Switch(State st){
this.st=st;
}

// 定义按下开关的方法
void press(){
st.operation(this);
}

// 定义三个操作
void doOff(){}
void doNear(){}
void doFar(){}
}
  • 客户端
1
2
3
4
5
6
//初始状态为关
Switch swi=new Switch(new Off());
// 按下三次开关
swi.press();
swi.press();
swi.press();

状态模式把各个状态变成一个个局部实体,并把将来可能的变化都限制在局部范围里,把各个状态实体与客户端的代码解耦,使得状态类的修改,并不会引起客户端的变化。

注意:下面这种设计

  • 设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Switch{
void press(){
if(状态关){
关灯
改状态为近光灯
}
else if(状态近光灯){
开近光灯
改状态为远光灯
}
else if(状态远光灯){
开远光灯
改状态为关
}
}
}
  • 客户端
1
2
3
4
5
Switch swi=new Switch();
// 按下三次开关
swi.press();
swi.press();
swi.press();

看起来如果修改了Switch类,客户端的代码并没有动,对吗?这只是从代码书写层面看没动,但是实际上修改了Switch类,Switch类当然会被重新编译,但是客户端的代码也要因此重新链接,所以实际上客户端是被影响的,这一点注意。

反过来看上面的状态模式的例子,由于客户端的是针对接口编程的,因此并不需要重新链接,Switch里的press()方法中的operation()方法是“动态绑定”或者叫“延迟绑定”的(也就是多态),当运行时才会去寻找究竟是哪个对象。所以即使修改了State对象,客户端的代码并没有受影响。这才是真正的松耦合,也是为什么多态是面向对象的核心机制。

9. 策略(Strategy)

「把算法部分独立出来」

这是个很有价值的模式,考虑这样一个软件的设计:要编写一个商场收银软件,商场的商品定价机制是很灵活的,经常会根据节假日或者什么原因临时将商品打折,打折的幅度和计算方法也会经常变,那么就给代码编写带来了难度,按照以往常规的方法,会采用很多if语句来写,这显然是一种不好的方法,一个商场有成千上万种商品,每一种商品会有很多折扣方法,难道要写成千上万个if语句吗?面向对象设计的一个原则就是:把可能变化的部分独立成接口,进行灵活的扩展,策略模式正是体现了这一点。

  • 设计
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
// 算法接口
interface Strategy {
double getPrice();
}

// 正常收费
class Normal implements Strategy{
@Override
public double getPrice(){ }
}

// 打折价格
class Discount implements Strategy{
@Override
public double getPrice() { }
}

// 收费程序
class Casher{
Strategy strategy;

void setStrategy(Strategy strategy){
this.strategy=strategy;
}

// 得到商品总价
double getMoney(int n){
return n*this.strategy.getPrice();
}
}
  • 客户端
1
2
3
4
5
6
7
Casher cash=new Casher();
//正常收费
cash.setStrategy(new Noral());
cash.getMoney(2);
// 换成打折收费
cash.setStrategy(new Discount());
cash.getMoney();

策略模式的结构非常简单,所以往往用来作为设计模式入门的例子,当然这里的setStrategy方法还是比较原始的,如果结合工厂方法等,就更加好了。

10. 模板方法(Template Method)

适用于这样的情况:有个复杂算法,由许多小算法组成,大致的框架已经可以确定,但是这些小算法需要到运行时才能确定调用哪个,或者说需要临时更换成另一个小算法。简而言之就是框架已经定了,细节还没定。模板方法就是用来解决这个问题,这同时也是很多类库和框架的设计思路,在传统的面向过程程序设计中,由于是上层函数调用下层函数,因此实际上,上层框架逻辑函数是依赖于底层实现的,底层函数没写好,上层就不能完成编译,底层函数修改了,上层函数也要被重新链接,这明显是不合理的。面向对象由于采取了多态机制,因此上层框架可以独立出来,不受底层的影响,要替换掉底层,也不会影响上层框架。

考虑一个做菜的程序,比如要做鱼汤,大致步骤是确定的:先放油,再煎鱼,再放水,再放调料,但是具体放多少油,放多少水,用什么鱼,这个具体再说,或者也可以先把鱼定下来,其他部分以后再说,这个看具体设计。

  • 设计
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
// 抽象类,没定下来的操作作为抽象方法
abstract DoFish{
void operation(){
// 放油
putOil();

// 放鱼
putFish();

// 放水
putWater();

// 放调料
putFlavour();
}

abstract void putOil();
void putFish(){
// ...
}
abstract void putWater();

abstract void putFlavour();
}

// 一种具体的做法
class OneDoFish extends DoFish(){
@Override
void putOil(){}

@Override
void putWater(){}

@Override
void putFlavour(){}
}
  • 客户端
1
2
DoFish o=new OneDoFish();
o.operation();

11. 访问者(Visitor)

这应该是23中模式中最复杂的一种了,但是和桥接模式一样,可以从中受到很大的启发。

总结

现有的这23种模式已经介绍完了, 我们应该注重体会各个设计模式的使用场景以及设计初衷,然后在适当的地方使用它们,做到物尽其用。

虽然这里介绍的是代码的设计模式,但其实我们应该站在宏观的角度看设计模式这个词。宏观来看,我们在生活和工作中反复实践总结出的经验都可以是设计模式,这里强调了反复实践,在实际应用场景中反复实践求证,这样出来的经验总结才能称作是设计模式,指导我们做好我们要做的事情。

(EOF)
杨威
发布日期 :2017-01-19
自由转载-非商用-非衍生-保持署名(知识共享3.0许可证)
杨威 wechat
微信订阅号
写点什么 心里不慌