JavaScript设计模式与开发实践(十二)状态模式&适配器模式

状态模式

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

第一部分的意思是将 状态封装成独立的类 ,并将 请求委托给当前的状态对象 ,当对象的内部状态改变时,会带来不同的行为变化。第二部分是从客户的角度来看,我们使用的对象, 在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化而来的,实际上这是使用了委托的效果

状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。

示例:切换灯光效果

假设客厅的灯有三种颜色,每次按下开关会切换颜色。(白 - 黄 - 蓝)

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
class Light {
button = null
state = 'off'

init() {
this.button = document.createElement("button")
this.button.innerHTML = '开关'
this.button.onclick = () => {
this.handlePressed()
}
document.body.appendChild(this.button)
}

handlePressed() {
if (this.state === 'off') {
console.log('白光');
this.state = 'white'
} else if (this.state === 'white') {
console.log('黄光');
this.state = 'yellow'
} else if (this.state === 'yellow') {
console.log('蓝光');
this.state = 'blue'
} else {
console.log('关灯');
this.state = 'off'
}
}
}

const light = new Light()
light.init()

如果再加入一种灯光需要在 handlePressed中再加一条 if else语句,逻辑复杂之后很不好管理(ps:发现好多设计模式其实都在给出一种不同场景下减少冗余 if else语句的方案),因此我们引入状态模式。

引入状态模式的好处是可以清晰管理各个状态之间的转换关系,通过调用 setState进行状态的切换。在下面的代码中,Light类是状态模式的 Context,在 Context保存每个状态的实例,以便把请求委托给状态实例。

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
class Light {
off = new OffState(this)
white = new WhiteState(this)
yellow = new YellowState(this)
blue = new BlueState(this)
curState = null
button = null
init() {
this.curState = this.off
this.button = document.createElement("button")
this.button.innerHTML = '开关'
this.button.onclick = () => {
this.curState.handlePressed()
}
document.body.appendChild(this.button)
}
setState(newState) {
this.curState = newState
}
}

class State {
light = null
constructor(light) {
this.light = light
}
handlePressed() {
throw new Error(this.constructor.name + '父类的 handlePressed 方法必须被重写')
}
}

class OffState extends State {
handlePressed() {
console.log('白光');
this.light.setState(this.light.white)
}
}

class WhiteState extends State {
handlePressed() {
console.log('黄光');
this.light.setState(this.light.yellow)
}
}

class YellowState extends State {
handlePressed() {
console.log('蓝光');
this.light.setState(this.light.blue)
}
}

class BlueState extends State {
handlePressed() {
console.log('关灯');
this.light.setState(this.light.off)
}
}

const light = new Light()
light.init()

或者可以使用更JavaScript的写法:用一个状态机对象替代状态类及状态子类。

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
const FSM = {
off: {
handlePressed(_this) {
console.log('白光');
_this.curState = FSM.white
}
},
white: {
handlePressed(_this) {
console.log('蓝光');
_this.curState = FSM.yellow
}
},
yellow: {
handlePressed(_this) {
console.log('黄光');
_this.curState = FSM.blue
}
},
blue: {
handlePressed(_this) {
console.log('关灯');
_this.curState = FSM.off
}
}
};

小结

其实状态模式跟策略模式还有点类似,但是策略模式的各个策略类之间没有必然的状态切换关系。

状态模式其实就是状态机的一种具体的实现,对于应用中具有非常多状态切换的场景就可以考虑使用状态机来帮助梳理状态跟状态之间切换的逻辑,避免代码的冗余。

适配器模式

适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。

在程序开发中有许多这样的场景:当我们试图调用模块或者对象的某个接口时,却发现这个接口的格式并不符合目前的需求。 这时候有两种解决办法。

  • 第一种是修改原来的接口实现,但如果原来的模块很复杂,或者我们拿到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。
  • 第二种办法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道

适配器模式包含三个角色:

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

对象适配器

对象适配器的原理就是通过组合来实现适配器功能。具体做法:让 Adapter 实现 Target 接口,然 后内部持有 Adaptee 实例,然后再 Target 接口规定的方法内转换 Adaptee

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
class Adaptee {
public void specialRequest() {
System.out.println("处理特殊请求");
}
}

interface Target {
void request();
}

class Adapter implements Target {

private Adaptee adaptee;

public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}

@Override
public void request() {
System.out.println("处理普通请求");
this.adaptee.specialRequest();
}
}

public class Client {
public static void main(String[] args) {
Adaptee adaptee = new Adaptee();
Target target = new Adapter(adaptee);
target.request();
}
}

类适配器

类适配器的原理就是通过继承来实现适配器功能。具体做法:让 Adapter 实现 Target 接口,并且继承 Adaptee,这样 Adapter 就具备 TargetAdaptee可以将两者进行转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Adaptee {
public void specialRequest() {
System.out.println("处理特殊请求");
}
}

interface Target {
void request();
}

class Adapter extends Adaptee implements Target {
@Override
public void request() {
System.out.println("处理普通请求");
super.specialRequest();
}
}

public class Client {
public static void main(String[] args) {
Target target = new Adapter();
target.request();
}
}

接口适配器

接口适配器的关注点与类适配器和对象适配器的关注点不太一样,类适配器和对象适配器着重于将系统存在的一个角色(Adaptee)转化成目标接口(Target)所需内容,而接口适配器的使用场景是解决接口方法过多,如果直接实现接口,那么类会多出许多空实现的方法,类显得很臃肿。

此时,使用接口适配器就能让我们只实现我们需要的接口方法,目标更清晰。

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
class AC220 {
public int outputAC220V() {
int output = 220;
System.out.println("输出电压:" + output + "V");
return output;
}
}

interface DC {

int output5V();

int output12V();

int output24V();

int output36V();

}

class PowerAdapter implements DC {

AC220 ac220;

public PowerAdapter(AC220 ac220) {
this.ac220 = ac220;
}

@Override
public int output5V() {
return 0;
}

@Override
public int output12V() {
return 0;
}

@Override
public int output24V() {
return 0;
}

@Override
public int output36V() {
return 0;
}
}

class Out12V extends PowerAdapter {
Out12V(AC220 ac220) {
super(ac220);
}

@Override
public int output5V() {
int adapterInput = ac220.outputAC220V();
int adapterOutput = adapterInput / 44;
System.out.println("输入AC" + adapterInput + "输出DC" + adapterOutput);
return adapterOutput;
}
}

public class Client {
public static void main(String[] args) {
DC adapter = new Out12V(new AC220());
adapter.output5V();
}
}

总结

优点

1、能提高类的透明性和复用,现有的类复用但不需要改变

2、目标类和适配器类解耦,提高程序的扩展性

3、在很多业务场景中符合开闭原则

缺点

1、适配器编写过程需要全面考虑,可能会增加系统的复杂性

2、增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱


JavaScript设计模式与开发实践(十二)状态模式&适配器模式
https://jing-jiu.github.io/jing-jiu/2023/01/17/notebooks/JavaScript设计模式与实践/设计模式(十二)/
作者
Jing-Jiu
发布于
2023年1月17日
许可协议