32.2 策略模式VS状态模式

32.2 策略模式VS状态模式

在行为类设计模式中,状态模式和策略模式是亲兄弟,两者非常相似,我们先看看两者的通用类图,把两者放在一起比较一下,如图32-3所示。

image-20210930171106992

图32-3 策略模式(左)和状态模式(右)的通用类图

两个类图非常相似,都是通过Context类封装一个具体的行为,都提供了一个封装的方法,是高扩展性的设计模式。但根据两者的定义,我们发现两者的区别还是很明显的:策略模式封装的是不同的算法,算法之间没有交互,以达到算法可以自由切换的目的;而状态模式封装的是不同的状态,以达到状态切换行为随之发生改变的目的。这两种模式虽然都有变换的行为,但是两者的目标却是不同的。我们举例来说明两者的不同点。

人只要生下来就有工作可做,人在孩童时期的主要工作就是玩耍(学习只是在人类具有了精神意识行为后才产生的);成人时期的主要工作是养活自己,然后为社会做贡献;老年时期的主要工作就是享受天伦之乐。按照策略模式来分析,这三种不同的工作方式就是三个不同的具体算法,随着时光的推移工作内容随之更替,这和对一堆数组的冒泡排序、快速排序、插入排序一样,都是一系列的算法;而按照状态模式进行设计,则认为人的状态(孩童、成人、老人)产生了不同的行为结果,这里的行为都相同,都是工作,但是它们的实现方式确实不同,也就是产生的结果不同,看起来就像是类改变了。

32.2.1 策略模式实现人生

下面按照策略模式进行设计,先来看类图,如图32-4所示。

这是非常典型的策略模式,没有太多的玄机,它定义了一个工作算法,然后有三个实现类:孩童工作、成年人工作和老年人工作。我们来看代码,首先看抽象工作算法,如代码清单32-19所示。

image-20210930171228691

图32-4 策略模式实现人生的类图

代码清单32-19 抽象工作算法

1
2
3
4
public abstract class WorkAlgorithm {
//每个年龄段都必须完成的工作
public abstract void work();
}

无论如何,每个算法都必须实现work方法,完成对工作内容的定义,三个具体的工作算法如代码清单32-20、32-21、32-22所示。

代码清单32-20 孩童工作

1
2
3
4
5
6
7
public class ChildWork extends WorkAlgorithm {
//小孩的工作
@Override
public void work() {
System.out.println("儿童的工作是玩耍!");
}
}

代码清单32-21 成年人工作

1
2
3
4
5
6
7
public class AdultWork extends WorkAlgorithm {
//成年人的工作
@Override
public void work() {
System.out.println("成年人的工作就是先养活自己,然后为社会做贡献!");
}
}

代码清单32-22 老年人工作

1
2
3
4
5
6
7
public class OldWork extends WorkAlgorithm {
//老年人的工作
@Override
public void work() {
System.out.println("老年人的工作就是享受天伦之乐!");
}
}

我们再来看环境角色,如代码清单32-23所示。

代码清单32-23 环境角色

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Context {
private WorkAlgorithm workMethod;
public WorkAlgorithm getWork() {
return workMethod;
}
public void setWork(WorkAlgorithm work) {
this.workMethod = work;
}
//每个算法都有必须具有的功能
public void work(){
workMethod.work();
}
}

我们编写一个场景类来模拟该场景,如代码清单32-24所示。

代码清单32-24 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
public static void main(String[] args) {
//定义一个环境角色
Context context=new Context();
System.out.println("====儿童的主要工作=====");
context.setWork(new ChildWork());
context.work();

System.out.println("\n====成年人的主要工作=====");
context.setWork(new AdultWork());
context.work();
System.out.println("\n====老年人的主要工作=====");
context.setWork(new OldWork());
context.work();
}
}

在这里我们把每个不同的工作内容作为不同的算法,分别是孩童工作、成年人工作、老年人工作算法,然后在场景类中根据不同的年龄段匹配不同的工作内容,其运行结果如下所示:

1
2
3
4
5
6
====儿童的主要工作===== 
儿童的工作是玩耍!
====成年人的主要工作=====
成年人的工作就是先养活自己,然后为社会做贡献!
====老年人的主要工作=====
老年人的工作就是享受天伦之乐!

通过采用策略模式我们实现了“工作”这个策略的三种不同算法,算法可以自由切换,到底用哪个算法由调用者(高层模块)决定。策略模式的使用重点是算法的自由切换——老的算法退休,新的算法上台,对模块的整体功能没有非常大的改变,非常灵活。而如果想要增加一个新的算法,比如未出生婴儿的工作,只要继承WorkAlgorithm就可以了。

32.2.2 状态模式实现人生

我们再来看看使用状态模式是如何实现该需求的。随着时间的变化,人的状态变化了, 同时引起了人的工作行为改变,完全符合状态模式。我们来看类图,如图32-5所示。

image-20210930171554473

图32-5 状态模式实现人生的类图
这与策略模式非常相似,基本上就是几个类名称的修改而已,但是其中蕴藏的玄机就大了,看看代码你就会明白。我们先来看抽象状态类,如代码清单32-25所示。

代码清单32-25 人的抽象状态

1
2
3
4
5
6
7
8
9
10
public abstract class HumanState {
//指向一个具体的人
protected Human human;
//设置一个具体的人
public void setHuman(Human _human){
this.human = _human;
}
//不管人是什么状态都要工作
public abstract void work();
}

抽象状态定义了一个具体的人(human)必须进行工作(work),但是一个人在哪些状态下完成哪些工作则是由子类来实现的。我们先来看孩童状态,如代码清单32-26所示。

代码清单32-26 孩童状态

1
2
3
4
5
6
7
public class ChildState extends HumanState{
//儿童的工作就是玩耍
public void work(){
System.out.println("儿童的工作是玩耍!");
super.human.setState(Human.ADULT_STATE);
}
}

ChildState类代表孩童状态,在该状态下的工作就是玩耍。读者看着可能有点惊奇,在 work方法中为什么要设置下一个状态?因为我们的状态变化都是单方向的,从孩童到成年 人,然后到老年人,每个状态转换到其他状态只有一个方向,因此会在这里看到work有两个 职责:完成工作逻辑和定义下一状态。

我们再来看成年人状态和老年人状态,分别如代码清单32-27、32-28所示。

代码清单32-27 成年人状态

1
2
3
4
5
6
7
8
public class AdultState extends HumanState {
//成年人的工作就是先养活自己,然后为社会做贡献
@Override
public void work() {
System.out.println("成年人的工作就是先养活自己,然后为社会做贡献!");
super.human.setState(Human.OLD_STATE);
}
}

代码清单32-28 老年人状态

1
2
3
4
5
6
7
public class OldState extends HumanState {
//老年人的工作就是享受天伦之乐
@Override
public void work() {
System.out.println("老年人的工作就是享受天伦之乐!");
}
}

每一个HumanState的子类都代表了一种状态,虽然实现的方法名work都相同,但是实现的内容却不同,也就是在不同的状态下行为随之改变。我们来看环境角色是如何处理行为随状态的改变而改变的,如代码清单32-29所示。

代码清单32-29 环境角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Human {
//定义人类都具备哪些状态
public static final HumanState CHIILD_STATE = new ChildState();
public static final HumanState ADULT_STATE = new AdultState();
public static final HumanState OLD_STATE = new OldState();
//定义一个人的状态
private HumanState state;
//设置一个状态
public void setState(HumanState _state){
this.state = _state;
this.state.setHuman(this);
}
//人类的工作
public void work(){
this.state.work();
}
}

定义一个Human类代表人类,也就是状态模式中的环境角色,每个人都会经历从孩童到成年人再到老年人这样一个状态过渡(当然了,老顽童周伯通的情况我们就没有考虑进来),随着状态的改变,行为也改变。我们来看场景类,如代码清单32-30所示。

代码清单32-30 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client {
public static void main(String[] args) {
//定义一个普通的人
Human human = new Human();
//设置一个人的初始状态
human.setState(new ChildState());
System.out.println("====儿童的主要工作=====");
human.work();
System.out.println("\n====成年人的主要工作=====");
human.work();
System.out.println("\n====老年人的主要工作=====");
human.work();
}
}

运行结果如下所示:

1
2
3
4
5
6
====儿童的主要工作===== 
儿童的工作是玩耍!
====成年人的主要工作=====
成年人的工作就是先养活自己,然后为社会做贡献!
====老年人的主要工作=====
老年人的工作就是享受天伦之乐!

运行结果与策略模式相同,但是两者的分析角度是大相径庭的。策略模式的实现是通过分析每个人的工作方式的不同而得出三个不同的算法逻辑,状态模式则是从人的生长规律来分析,每个状态对应了不同的行为,状态改变后行为也随之改变。从以上示例中我们也可以看出,对于相同的业务需求,有很多种实现方法,问题的重点是业务关注的是什么,是人的生长规律还是工作逻辑?找准了业务的焦点,才能选择一个好的设计模式。

32.2.3 小结

从例子中我们可以看出策略模式和状态模式确实非常相似,称之为亲兄弟亦不为过,但是这两者还是存在着非常大的差别,而且也是很容易区分的。

  • 环境角色的职责不同

两者都有一个叫做Context环境角色的类,但是两者的区别很大,策略模式的环境角色只是一个委托作用,负责算法的替换;而状态模式的环境角色不仅仅是委托行为,它还具有登记状态变化的功能,与具体的状态类协作,共同完成状态切换行为随之切换的任务。

  • 解决问题的重点不同

策略模式旨在解决内部算法如何改变的问题,也就是将内部算法的改变对外界的影响降低到最小,它保证的是算法可以自由地切换;而状态模式旨在解决内在状态的改变而引起行为改变的问题,它的出发点是事物的状态,封装状态而暴露行为,一个对象的状态改变,从外界来看就好像是行为改变。

  • 解决问题的方法不同

策略模式只是确保算法可以自由切换,但是什么时候用什么算法它决定不了;而状态模式对外暴露的是行为,状态的变化一般是由环境角色和具体状态共同完成的,也就是说状态模式封装了状态的变化而暴露了不同的行为或行为结果。

  • 应用场景不同

两者都能实现前面例子中的场景,但并不表示两者的应用场景相同,这只是为了更好地展示出两者的不同而设计的一个场景。我们来想一下策略模式和状态模式的使用场景有什么不同,策略模式只是一个算法的封装,可以是一个有意义的对象,也可以是一个无意义的逻辑片段,比如MD5加密算法,它是一个有意义的对象吗?不是,它只是我们数学上的一个公式的相关实现,它是一个算法,同时DES算法、RSA算法等都是具体的算法,也就是说它们都是一个抽象算法的具体实现类,从这点来看策略模式是一系列平行的、可相互替换的算法封装后的结果,这就限定了它的应用场景:算法必须是平行的,否则策略模式就封装了一堆垃圾,产生了“坏味道”。状态模式则要求有一系列状态发生变化的场景,它要求的是有状态且有行为的场景,也就是一个对象必须具有二维(状态和行为)描述才能采用状态模式,如果只有状态而没有行为,则状态的变化就失去了意义。

  • 复杂度不同

通常策略模式比较简单,这里的简单指的是结构简单,扩展比较容易,而且代码也容易阅读。当然,一个具体的算法也可以写得很复杂,只有具备很高深的数学、物理等知识的人才可以看懂,这也是允许的,我们只是说从设计模式的角度来分析,它是很容易被看懂的。 而状态模式则通常比较复杂,因为它要从两个角色看到一个对象状态和行为的改变,也就是说它封装的是变化,要知道变化是无穷尽的,因此相对来说状态模式通常都比较复杂,涉及面很多,虽然也很容易扩展,但是一般不会进行大规模的扩张和修正。