重构与数据验证:重构输入验证逻辑,提升健壮性

输入验证这件事,说大不大,说小不小。

我见过太多系统,上线后第一个崩溃的原因,不是核心业务逻辑写错了,而是用户输入了一个「不该出现」的字符。嗯,说白了,验证就是系统的第一道防线。这道防线如果漏了,后面再好的架构也扛不住。

为什么验证逻辑需要重构?

很多项目初期,验证代码都是随手写的。比如这样:

if (name != null && name.length() > 0) {
    // 处理业务
}

看起来没问题对吧?但项目一复杂,问题就来了:

  • 验证逻辑散落在各个业务方法里,改一个规则要翻遍整个项目
  • 同样的验证重复写了好几遍,比如「手机号格式」在注册、修改、绑定时各写一次
  • 错误提示不统一,有的返回「参数错误」,有的返回「输入有误」

我在项目中遇到过最夸张的一次,一个接口的验证代码占了整个方法 60% 的行数。业务逻辑反而被淹没了。你想想看,这代码谁愿意维护?

重构的核心思路:分层与统一

我个人习惯把验证逻辑拆成三层:

  1. 基础格式验证 —— 比如非空、长度、正则匹配
  2. 业务规则验证 —— 比如库存是否充足、用户是否存在
  3. 权限验证 —— 比如当前用户是否有操作权限

这三层职责不同,混在一起就会乱。重构的第一步,就是让它们各归其位。

核心原则:验证逻辑与业务逻辑分离。验证只负责「能不能通过」,业务只负责「通过后做什么」。

实战:从散乱到整洁

我们来看一个具体的例子。假设有一个用户注册接口,原始代码可能是这样的:

public String register(String username, String phone, String email) {
    if (username == null || username.trim().isEmpty()) {
        return "用户名不能为空";
    }
    if (username.length() < 3 || username.length() > 20) {
        return "用户名长度需在3-20之间";
    }
    if (phone == null || !phone.matches("^1[3-9]\\d{9}$")) {
        return "手机号格式不正确";
    }
    if (email != null && !email.matches("^\\w+@\\w+\\.\\w+$")) {
        return "邮箱格式不正确";
    }
    // 业务逻辑...
    return "注册成功";
}

这段代码的问题很明显:验证和业务混在一起,而且错误信息是硬编码的。如果以后要支持国际化,或者要统一错误格式,改起来就麻烦了。

重构之后,我们可以这样做:

// 1. 定义验证器接口
public interface Validator<T> {
    ValidationResult validate(T input);
}

// 2. 实现具体的验证器
public class UsernameValidator implements Validator<String> {
    @Override
    public ValidationResult validate(String username) {
        if (username == null || username.trim().isEmpty()) {
            return ValidationResult.fail("用户名不能为空");
        }
        if (username.length() < 3 || username.length() > 20) {
            return ValidationResult.fail("用户名长度需在3-20之间");
        }
        return ValidationResult.success();
    }
}

// 3. 组合使用
public class RegistrationService {
    private List<Validator<UserInput>> validators;
    
    public Result register(UserInput input) {
        for (Validator<UserInput> validator : validators) {
            ValidationResult vr = validator.validate(input);
            if (!vr.isSuccess()) {
                return Result.fail(vr.getMessage());
            }
        }
        // 业务逻辑...
        return Result.success("注册成功");
    }
}

你看,这样一拆,每个验证器只负责一件事。以后要加新的验证规则,只需要新增一个 Validator 实现类,不用改任何业务代码。

小技巧:我建议把验证器设计成可组合的。比如可以用链式调用:new ValidatorChain().add(new NotNullValidator()).add(new LengthValidator(3, 20)).validate(value)。这样写起来很灵活,也容易测试。

避坑指南:我曾经踩过的坑

我曾经犯过一个错误:把所有验证都放在前端做,后端只接收数据。结果上线第一天,有人直接调接口绕过了前端验证,往数据库里写了一大堆脏数据。嗯,从那以后我明白了——后端验证永远不能信任前端

还有一次,我在验证手机号时用了很复杂的正则,结果某个运营商的号段更新后,正则没跟上,导致一批用户注册失败。后来我改成用专门的库来验证,比如 Google 的 libphonenumber。专业的事交给专业的工具,别自己造轮子。

警告:永远不要在验证逻辑中抛出运行时异常。验证失败是预期行为,不是系统错误。用返回结果对象(如 ValidationResult)比抛异常更优雅,也更容易处理。

验证逻辑的常见模式

我整理了几种常见的验证模式,供你参考:

模式 适用场景 示例
守卫模式 方法入口处快速检查 if (input == null) return;
校验器模式 复杂业务规则验证 Validator 接口 + 实现类
注解驱动 Java Bean 验证 @NotNull, @Size, @Pattern
策略模式 不同场景不同验证规则 根据用户类型选择验证策略

我个人比较推荐「校验器模式 + 注解驱动」的组合。注解用来做基础格式验证,校验器用来做业务规则验证。这样分工明确,代码也干净。

SVG:验证重构的核心流程

下面这张图展示了验证逻辑重构前后的对比,以及重构后的核心流程:

验证逻辑重构:从散乱到分层 重构前:散乱混合 业务方法A: if(name!=null) ... 业务方法B: if(phone.matches(...)) ... 业务方法C: if(email!=null) ... 验证逻辑散落在各处,重复且混乱 重构后:分层清晰 基础验证层: NotNull, Length, Pattern 业务验证层: UserExists, StockEnough 权限验证层: HasPermission, IsOwner 各层独立,可复用,易测试 接收输入 基础格式验证 业务规则验证 返回错误提示 执行业务逻辑

这张图左边是重构前的状态,验证逻辑散落在各个业务方法里。右边是重构后的分层结构,每一层职责清晰。下方的流程展示了验证通过后,业务逻辑才能执行。

总结一下

重构输入验证逻辑,说白了就是两件事:分离关注点统一处理方式

分离关注点,让验证逻辑不再和业务逻辑纠缠在一起。统一处理方式,让所有验证都走同样的流程,返回同样的格式。做到这两点,系统的健壮性自然就上来了。

我个人的经验是,每次写新功能时,先花 10 分钟想想验证逻辑该怎么组织。这 10 分钟的投资,往往能省下后面几小时的调试时间。你想想看,是不是这个道理?


公众号:蓝海资料掘金营,微信deep3321