设计模式是编程范式的一种,而编程范式其实和编程语言没有太大关系,因此无论前端还是后端,设计模式总会在代码重构中被频繁提到。实际上在前端开发中,真正应用到设计模式的还是少数,原因之一是前端业务逻辑复杂度不够,确实用不到设计模式。技术应当是服务于业务的,我始终认为不能够为了秀技术而去硬上技术,下面就介绍一下设计模式是怎么实际应用到我们项目中去的。
业务背景
这个项目是一个问卷系统,大家使用过问卷就会知道,每个问题都可以设置一些规则上的校验,比如
这些规则可以通过我们的后台管理系统配置好,然后在前端请求问卷信息时全部拿到。也就是说这些校验规则会在前端进行校验,如果校验未通过,对应的问题就会出现错误提示。
以上就是大致的项目和需求背景,拿到这个需求脑海中第一个方案大概率就是
针对所有的问题,循环校验每一个规则
很容易能想象到伪代码大概是代码片段v1.0
这样:
代码片段v1.01 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'){ } else if (questions[i].rules[j].type === 'maxcount') { } else if (questions[i].rules[j].type === 'expression') { } } }
|
这种方式最暴力也最省事,但是对于业务未来的扩展与变更而言,就会有点力不从心了。
- 首先,每增加一个校验规则,我们就需要多写一个
else if
,而增加校验规则这种业务变更是明显可预知的;
- 同时,如果某个校验规则的逻辑发生变化,那么就需要改动到其中的某一个
if
块。
随着业务发展,最终的结果必然是 if else
越来越多,嵌套层级越来越深,接手的人越来越看不懂,最后演变为屎山。
现状
我们问卷系统采用的技术栈是 react
+ typescript
,使用了面向对象编程的风格,上述校验场景的代码进行简化后,实现细节如代码片段v2.0
所示:
代码片段v2.01 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') { } else if (this.type === 'maxcount') { } else if (this.type === 'expression') { } else { return true } } }
|
上述代码很好理解,其实就是将代码片段v1.0
的双层 for
循环进行了简单的抽象,在可读性上比代码片段v1.0
要好,但是在扩展性上依然和双层 for
循环一样,只要有需求变更,必然会改动到 Rule
类的 validate
方法,这也显然违背了开闭原则,即应当
面向扩展开放,面向修改封闭。
重构
重构之前首先要明确这个场景的特点,根据特点来选择重构的手段。根据上述业务场景的描述,特点如下:
- 规则有多条,且会逐渐增加
- 每条规则根据不同的类型来进行区分
- 每条规则下有不同的校验逻辑
看到这种结构相同但内部实现不同的模型,脑海中第一个浮现出来的应该就是类的继承。通过类的继承来实现这种业务上的多态性,可以有效的降低耦合,便于扩展。而各个子类(这里即规则)可以通过一个标识(这里即规则类型)来进行区分,那么工厂模式就是最好的选择,通过工厂来构造出每一个子类。重构后的代码如下代码片段v3.0
:
代码片段v3.01 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 { } }
export class MaxCountRule extends Rule { constructor(rule) { super(rule) } validate(): boolean { } }
export class ExpressionRule extends Rule { constructor(rule) { super(rule) } validate(): boolean { } }
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
逻辑被抽离成了三个不同的子类,每个子类的职责也是单一的,只负责对自己这一个校验规则进行校验,短小且职责单一的类是《代码整洁之道》中大力推崇的编程范式,这样也非常便于后续的扩展与维护。
至此,我们的重构就结束了,大家可以再从头捋一遍整个重构的思路,相信下次再碰到类似的场景,脑海中就能立马想到如何优化了。