Skip to content

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("验证通过,提交成功!");
});

问题分析

这段代码虽然能工作,但它是一个典型的“反面教材”:

  1. 违反单一职责原则:事件处理函数承担了所有职责:获取表单数据、定义验证规则、执行验证、提示错误信息。
  2. 违反开放/封闭原则:如果现在需要增加一个“密码不能为空”的校验,或者“用户名必须为邮箱格式”的校验,你必须深入这个函数内部进行修改,增加更多的 if-else
  3. 可复用性极差:如果另一个页面也有“非空校验”的需求,你只能复制粘贴这部分代码。

二、第一次重构:策略模式登场!

我们的首要目标是解决掉这个丑陋的 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 的一系列操作)分离开来,让客户端代码变得更加纯粹。

通过这次实战,我们可以深刻地体会到:

  1. 设计模式不是孤立的:它们经常协同工作,来解决一个复杂的问题。
  2. 代码是演进的:一个好的系统不是一蹴而就的,而是通过不断地重构,识别出代码中的“坏味道”,并用合适的设计模式去优化它。
  3. 最终目标:高内聚、低耦合、可复用、可扩展。这正是设计模式赋予我们的力量。