设计模式在前端中的实际应用

设计模式是编程范式的一种,而编程范式其实和编程语言没有太大关系,因此无论前端还是后端,设计模式总会在代码重构中被频繁提到。实际上在前端开发中,真正应用到设计模式的还是少数,原因之一是前端业务逻辑复杂度不够,确实用不到设计模式。技术应当是服务于业务的,我始终认为不能够为了秀技术而去硬上技术,下面就介绍一下设计模式是怎么实际应用到我们项目中去的。

业务背景

这个项目是一个问卷系统,大家使用过问卷就会知道,每个问题都可以设置一些规则上的校验,比如

  • 必填
  • 最多选择 n 项
  • 满足某个表达式

这些规则可以通过我们的后台管理系统配置好,然后在前端请求问卷信息时全部拿到。也就是说这些校验规则会在前端进行校验,如果校验未通过,对应的问题就会出现错误提示。

以上就是大致的项目和需求背景,拿到这个需求脑海中第一个方案大概率就是

针对所有的问题,循环校验每一个规则

很容易能想象到伪代码大概是代码片段v1.0 这样:

代码片段v1.0
1
2
3
4
5
6
7
8
9
10
11
for (let i = 0; i < questions.length; i++) {
for (let j = 0; j < questions[i].rules.length; j++) {
if (questions[i].rules[j].type === 'require'){
// do require validate
} else if (questions[i].rules[j].type === 'maxcount') {
// do maxcount validate
} else if (questions[i].rules[j].type === 'expression') {
// do expression validate
}
}
}

这种方式最暴力也最省事,但是对于业务未来的扩展与变更而言,就会有点力不从心了。

  • 首先,每增加一个校验规则,我们就需要多写一个 else if,而增加校验规则这种业务变更是明显可预知的;
  • 同时,如果某个校验规则的逻辑发生变化,那么就需要改动到其中的某一个 if 块。

随着业务发展,最终的结果必然是 if else 越来越多,嵌套层级越来越深,接手的人越来越看不懂,最后演变为屎山。

现状

我们问卷系统采用的技术栈是 react + typescript,使用了面向对象编程的风格,上述校验场景的代码进行简化后,实现细节如代码片段v2.0 所示:

代码片段v2.0
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
// 问卷类,一个问卷包含多个问题
export class Survey {
questions: Question[]

constructor(survey) {
// 实例化所有问题
this.questions = survey.questions.map((item) => new Question(item))
}

validate(): boolean {
return this.questions.every((item) => item.validate())
}
}

// 问题类,一个问题包含多条规则
export class Question {
rules: Rule[]

constructor(question) {
// 实例化所有规则
this.rules = question.rules.map((item) => new Rule(item))
}

validate(): boolean {
return this.rules.every((item) => item.validate())
}
}

// 规则类,一个规则有多种类型
export class Rule {
type: string

constructor(rule) {
this.type = rule.type
}

// 根据规则的类型进行校验
validate(): boolean {
if(this.type === 'require') {
// do require validate
} else if (this.type === 'maxcount') {
// do maxcount validate
} else if (this.type === 'expression') {
// do expression validate
} else {
return true
}
}
}

上述代码很好理解,其实就是将代码片段v1.0的双层 for 循环进行了简单的抽象,在可读性上比代码片段v1.0要好,但是在扩展性上依然和双层 for 循环一样,只要有需求变更,必然会改动到 Rule 类的 validate 方法,这也显然违背了开闭原则,即应当

面向扩展开放,面向修改封闭。

重构

重构之前首先要明确这个场景的特点,根据特点来选择重构的手段。根据上述业务场景的描述,特点如下:

  • 规则有多条,且会逐渐增加
  • 每条规则根据不同的类型来进行区分
  • 每条规则下有不同的校验逻辑

看到这种结构相同但内部实现不同的模型,脑海中第一个浮现出来的应该就是类的继承。通过类的继承来实现这种业务上的多态性,可以有效的降低耦合便于扩展。而各个子类(这里即规则)可以通过一个标识(这里即规则类型)来进行区分,那么工厂模式就是最好的选择,通过工厂来构造出每一个子类。重构后的代码如下代码片段v3.0

代码片段v3.0
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
79
80
81
82
83
84
85
export class Survey {
questions: Question[]

constructor(survey) {
this.questions = survey.questions.map((item) => new Question(item))
}

validate(): boolean {
return this.questions.every((item) => item.validate())
}
}

export class Question {
rules: Rule[]

constructor(question) {
// 这里实例化规则需要去调用工厂方法
this.rules = question.rules.map((item) => new RuleFactory.create(item))
}

validate(): boolean {
return this.rules.every((item) => item.validate())
}
}

// 规则基类,不做逻辑处理
export class Rule {
type: RuleType

constructor(rule) {
this.type = rule.type
}

validate(): boolean {
return true
}
}

// 规则子类,只做必填规则的校验
export class RequireRule extends Rule {
constructor(rule) {
super(rule)
}

validate(): boolean {
// do require validate
}
}

// 规则子类,只做最大可选项的校验
export class MaxCountRule extends Rule {
constructor(rule) {
super(rule)
}

validate(): boolean {
// do maxcount validate
}
}

// 规则子类,只做表达式的校验
export class ExpressionRule extends Rule {
constructor(rule) {
super(rule)
}

validate(): boolean {
// do expression validate
}
}

// 规则工厂类,只做规则类的实例化
export class RuleFactory {
static create(rule) {
if(rule.type === 'require') {
return new RequireRule(rule)
} else if (rule.type === 'maxcount') {
return new MaxCountRule(rule)
} else if (rule.type === 'expression') {
return new ExpressionRule(rule)
} else {
return new Rule(rule)
}
}
}

重构完成后可以看到,原来的 if else 逻辑被抽离成了三个不同的子类,每个子类的职责也是单一的,只负责对自己这一个校验规则进行校验,短小且职责单一的类是《代码整洁之道》中大力推崇的编程范式,这样也非常便于后续的扩展与维护。

至此,我们的重构就结束了,大家可以再从头捋一遍整个重构的思路,相信下次再碰到类似的场景,脑海中就能立马想到如何优化了。

设计模式在前端中的实际应用

http://cherrow.top/posts/17149ec0

作者

cherrow

发布于

2022-03-03

更新于

2022-03-09

许可协议

评论