设计模式是通用的、可复用的代码设计方案,也可以说是针对某类问题的解决方案,因此,掌握好设计模式,可以帮助我们编写更健壮的代码。
wiki中将设计模式分为四类,分别是:
- 创建模式(creational patterns)
- 结构模式(structural patterns)
- 行为模式(behavioral patterns)
- 并发模式(concurrency patterns)
策略模式和状态模式属于其中的行为模式,行为模式——从名称上就可以看出——与动作、操作有关。
这两种模式我接触下来,感觉存在一定的相似性。状态模式中通常会存在一个内部状态,状态改变时行为也会发生改变,而策略模式是针对不同条件下的行为进行封装。总的来说,两者都是在不同条件下有不同的行为。接下来我们分别来看一下。
策略模式
首先看策略模式,根据针对它的概述,貌似就是一系列算法的封装。
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
当然策略模式不止关于算法的定义,还有对算法的调用。关于这一点我们在策略模式对应的wiki:Strategy pattern页面也能看到相应的描述。
Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
这句话的大概意思是:代码在运行时接收指令,决定使用一系列算法中的哪一种。在这里一种算法就是对应一个策略。
这个描述很容易让人联想到代码中常见的条件语句if-elseif-else
,在条件分支根据不同的指令执行不同的操作。但是很显然,既然是一系列的算法,那就说明可能会有很多、甚至是大量的条件,那么可想而知,如果我们直接使用if-else
语句来编写执行代码的话,这部分代码会非常长,并且这会破坏软件设计原则中的单一功能原则,这段代码除了判断条件,还要根据不同的条件执行不同的细节操作。
策略模式中的“策略”,其实指的就是算法。然后条件判断作为一个入口,去调用对应的“策略”。所以我们看到wiki中有下面这段描述:
Typically, the strategy pattern stores a reference to some code in a data structure and retrieves it. This can be achieved by mechanisms such as the native function pointer, the first-class function, classes or class instances in object-oriented programming languages, or accessing the language implementation's internal storage of code via reflection.
这段话的意思是:策略模式会在数据结构中存储对某些代码的引用,并对其进行检索。这可以通过本地函数指针、一级函数、面向对象编程语言中的类或类实例,或通过反射 访问 语言实现的代码内部存储等机制来实现。
简单来理解,就是把一系列相关操作封装成函数,一个函数就对应一个算法的实现。
策略模式与开闭原则
我们知道在软件设计原则中有一条是:对扩展开放,对修改封闭。策略模式与开闭原则是一致的。
According to the strategy pattern, the behaviors of a class should not be inherited. Instead, they should be encapsulated using interfaces.
根据策略模式,类的行为不应被继承,它们应使用接口进行封装。也就是说,我们最好不要对父类本身做修改,而是使用接口对子类的行为进行扩展。在wiki中使用了Java来举例子,在JavaScript中也可以做类似的处理,比如不把对应的策略函数加在对象自身,而是统一放在一个地方进行调用,也就是上面所说的在数据结构中存储对某些代码的引用
。比如下面这个例子:
某商场中的商品在不同阶段的价格满足固定的逻辑,做了以下封装:
const priceProcessor = {
pre(originPrice) {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.9;
},
onSale(originPrice) {
if(originPrice >= 100) {
return originPrice - 30;
}
return originPrice * 0.8;
},
back(originPrice) {
if(originPrice >= 200) {
return originPrice - 50;
}
return originPrice;
},
fresh(originPrice) {
return originPrice * 0.5;
},
};
pre、onSale、back、fresh分别代表了在预热、大促、返场、尝鲜四种阶段下的价格处理。
在上述代码中,我们在priceProcessor
这个数据结构中存储了针对不同阶段下对价格的处理逻辑,也就是各种封装的函数。我们可以调用这些引用,从而实现在不同条件下执行不同的算法。
这样,当我们策划新的促销活动时,只需要在priceProcessor
这个结构中增加新的处理逻辑,而不需要影响商品对象和其他的处理逻辑,并且这样子处理后,测试流程中就只需要测试新的处理逻辑,而不需要回归测试整体功能。
每个处理逻辑有单独的函数实现,这也方便不同条件下的算法替换,比如在某次商场大促,想要使用返场的价格,就可以直接调用priceProcessor.back
方法,而不需要编写重复冗余的代码。
状态模式
策略模式的核心很简单,就是单一功能的函数封装。接下来我们继续看状态模式。
The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes.
状态模式也很简单,就是允许对象在内部状态发生变化时改变其行为。
状态模式的“状态”,就是指对象内部的状态,也就是说,这个模式针对的是存在内部状态的对象。
The state pattern can be interpreted as a strategy pattern, which is able to switch a strategy through invocations of methods defined in the pattern's interface.
我们可以看到wiki这部分也有说,状态模式可以解释为一种策略模式。其实上也就是说,根据不同的状态切换策略;在策略模式下,是根据不同的条件切换不同策略,这个是广泛意义下的条件,而状态模式中,不同条件就特定为不同的内部状态。
这样处理后,就不需要使用条件语句了,可以直接通过不同状态映射不同的行为。
在状态模式的wiki页面中,也列举了它所解决的主要问题:
The state pattern is set to solve two main problems:[4]
- An object should change its behavior when its internal state changes.
- State-specific behavior should be defined independently. That is, adding new states should not affect the behavior of existing states.
在某类场景中,第一,对象应根据其内部状态的改变来改变其行为。
第二,特定于状态的行为应独立定义。也就是说,添加新状态不应影响现有状态的行为。
这里看第二点,其实和策略模式的场景很类似。
对应这两个问题,状态模式描述了以下解决方案:
In this, the pattern describes two solutions:
- Define separate (state) objects that encapsulate state-specific behavior for each state. That is, define an interface (state) for performing state-specific behavior, and define classes that implement the interface for each state.
- A class delegates state-specific behavior to its current state object instead of implementing state-specific behavior directly.
第一,是定义独立的状态对象,为每个状态封装特定于状态的行为。
第二,类将特定于状态的行为委托给其当前的状态对象,而不是直接实现特定于状态的行为。
因此,状态模式中的关键就在于对状态对象的实现。比如下面这个例子:
一个养生壶有不同的功能,当切换不同的功能时我们可以认为它处于不同的工作状态。
class HealthPot {
constructor() {
this.state = new State();
}
changeState(status) {
this.state.status = status;
// 若状态不存在,则返回
if(!this.state.statusToProcessor[status]) {
return;
}
this.state.statusToProcessor[status]();
}
}
class State {
constructor() {
this.status = '';
}
statusToProcessor = {
water() {
console.log('煮开水');
},
flowersTea() {
console.log('煮花草茶');
},
fruitsTea() {
console.log('煮水果茶');
},
keepWarm() {
console.log('保温');
}
}
}
const hp = new HealthPot();
hp.changeState('flowersTea');
在上述代码中,如何实现特定状态的行为与养生壶本身无关,只与状态对象有关。养生壶的行为只是改变状态,并调用对应方法,这样如果后续有新的状态增加,也不用去修改养生壶这个具体的对象,相当于一种拆分行为。
这其实就有点类似于vue中的状态管理工具vuex。
总结
策略模式和状态模式两者存在一定的相似性,但是策略模式封装的函数其独立性会更高,而状态模式中封装的函数依赖于主体的状态,具体操作代码也可能依赖主体的其他属性,比如养生壶例子中,执行各种功能时,需要保证壶中有水,并且要判断是否通电中等等。
简单来说就是一种拆分、封装的行为,满足软件设计原则中的单一职责和开闭原则。
一般在应用开发初期,由于功能简单,开发者可能不会特别在意拆分,并且通常而言不太提倡提前优化,所以会在之后的维护和迭代中,应用这些模式来优化和重构代码;但是有时设计良好的代码,会更便于代码的维护。