Appearance
6.1 实战:用设计模式重构一个表单验证
欢迎来到实战环节!在本章,我们将把之前学到的理论知识应用到一个非常真实且常见的场景中:前端表单验证。
我们将从一段典型的、充满了“坏味道”的代码开始,一步步地运用策略模式和命令模式,将其重构成一个健壮、可维护、可扩展的验证系统。
一、起点:一个典型的“意大利面”验证函数
假设我们有一个注册表单,包含用户名和密码两个字段。验证需求如下:
- 用户名不能为空。
- 密码长度不能少于 8 位。
很多时候,我们可能会写出下面这样的代码:
html
<!-- index.html -->
<form id="registerForm">
<input type="text" name="username" placeholder="用户名" />
<input type="password" name="password" placeholder="密码" />
<button type="submit">注册</button>
</form>javascript
// index.js
const form = document.getElementById("registerForm");
form.addEventListener("submit", function (e) {
e.preventDefault();
const username = form.username.value;
const password = form.password.value;
// --- 验证逻辑 ---
if (username === "") {
alert("用户名不能为空!");
return;
}
if (password.length < 8) {
alert("密码长度不能少于 8 位!");
return;
}
alert("验证通过,提交成功!");
});问题分析
这段代码虽然能工作,但它是一个典型的“反面教材”:
- 违反单一职责原则:事件处理函数承担了所有职责:获取表单数据、定义验证规则、执行验证、提示错误信息。
- 违反开放/封闭原则:如果现在需要增加一个“密码不能为空”的校验,或者“用户名必须为邮箱格式”的校验,你必须深入这个函数内部进行修改,增加更多的
if-else。 - 可复用性极差:如果另一个页面也有“非空校验”的需求,你只能复制粘贴这部分代码。
二、第一次重构:策略模式登场!
我们的首要目标是解决掉这个丑陋的 if-else 结构。这正是策略模式的用武之地!我们可以将每一条验证规则都封装成一个独立的“策略”。
第 1 步:创建策略库 (strategies.js)
我们创建一个对象,用来存放所有可复用的验证策略。
javascript
// strategies.js
export const strategies = {
isNotEmpty: function (value, errorMsg) {
if (value === "") {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.length < length) {
return errorMsg;
}
},
isEmail: function (value, errorMsg) {
if (!/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(value)) {
return errorMsg;
}
},
};第 2 步:创建验证器 (validator.js)
验证器是策略模式中的“上下文”(Context)。它负责接收用户的验证请求,并委托给相应的策略去执行。
javascript
// validator.js
import { strategies } from "./strategies.js";
export class Validator {
constructor() {
this.cache = []; // 存储要校验的规则
}
// 添加规则
add(dom, rules) {
for (const rule of rules) {
const { strategy, errorMsg } = rule;
const [strategyName, ...args] = strategy.split(":");
this.cache.push(() => {
const value = dom.value;
return strategies[strategyName].apply(dom, [value, ...args, errorMsg]);
});
}
}
// 开始校验
start() {
for (const validateFn of this.cache) {
const errorMsg = validateFn();
if (errorMsg) {
return errorMsg; // 如果有错误,立即返回
}
}
}
}第 3 步:重构客户端代码 (index.js)
现在,我们的客户端代码变得非常“声明式”和清晰。
javascript
// index.js (重构后)
import { Validator } from "./validator.js";
const form = document.getElementById("registerForm");
form.addEventListener("submit", function (e) {
e.preventDefault();
const validator = new Validator();
// 添加验证规则
validator.add(form.username, [
{ strategy: "isNotEmpty", errorMsg: "用户名不能为空!" },
{ strategy: "isEmail", errorMsg: "请输入正确的邮箱格式!" }, // <--- 轻松扩展!
]);
validator.add(form.password, [
{ strategy: "minLength:8", errorMsg: "密码长度不能少于 8 位!" },
]);
// 执行校验
const errorMsg = validator.start();
if (errorMsg) {
alert(errorMsg);
return;
}
alert("验证通过,提交成功!");
});阶段性胜利! 我们通过策略模式,成功地将易变的验证规则与不变的验证逻辑分离开来。现在添加、删除、修改规则都变得非常容易,完全符合开放/封闭原则。
三、第二次重构:引入命令模式
观察上面的客户端代码,虽然已经很好了,但事件处理函数中仍然包含了“创建 validator -> 添加规则 -> 启动校验 -> 处理结果”这一系列过程。我们可以把这个“发起验证”的过程,进一步封装成一个命令。
第 1 步:创建命令 (command.js)
javascript
// command.js
import { Validator } from "./validator.js";
export class ValidationCommand {
constructor(form) {
this.form = form;
this.validator = new Validator();
// 在这里配置所有的验证规则
this.validator.add(form.username, [
{ strategy: "isNotEmpty", errorMsg: "用户名不能为空!" },
{ strategy: "isEmail", errorMsg: "请输入正确的邮箱格式!" },
]);
this.validator.add(form.password, [
{ strategy: "minLength:8", errorMsg: "密码长度不能少于 8 位!" },
]);
}
// 命令的统一执行接口
execute() {
return this.validator.start();
}
}第 2 步:最终的客户端代码 (index.js)
客户端代码现在变得极致简洁。它只负责创建和执行命令。
javascript
// index.js (最终版)
import { ValidationCommand } from "./command.js";
const form = document.getElementById("registerForm");
const command = new ValidationCommand(form); // 创建命令
form.addEventListener("submit", function (e) {
e.preventDefault();
const errorMsg = command.execute(); // 执行命令
if (errorMsg) {
alert(errorMsg);
return;
}
alert("验证通过,提交成功!");
});四、总结与反思
让我们回顾一下这次重构之旅:
- 起点:一个耦合、僵硬的
if-else函数。 - 策略模式:我们首先用策略模式,将算法的定义(验证规则)和算法的使用(验证器)分离开来,实现了核心逻辑的解耦和扩展。
- 命令模式:接着,我们用命令模式,将“发起一次完整的表单验证”这个请求本身封装成了一个对象,从而将请求的调用(按钮点击)与请求的实现(Validator 的一系列操作)分离开来,让客户端代码变得更加纯粹。
通过这次实战,我们可以深刻地体会到:
- 设计模式不是孤立的:它们经常协同工作,来解决一个复杂的问题。
- 代码是演进的:一个好的系统不是一蹴而就的,而是通过不断地重构,识别出代码中的“坏味道”,并用合适的设计模式去优化它。
- 最终目标:高内聚、低耦合、可复用、可扩展。这正是设计模式赋予我们的力量。