重构与数据验证:重构输入验证逻辑,提升健壮性
输入验证这件事,说大不大,说小不小。
我见过太多系统,上线后第一个崩溃的原因,不是核心业务逻辑写错了,而是用户输入了一个「不该出现」的字符。嗯,说白了,验证就是系统的第一道防线。这道防线如果漏了,后面再好的架构也扛不住。
为什么验证逻辑需要重构?
很多项目初期,验证代码都是随手写的。比如这样:
if (name != null && name.length() > 0) {
// 处理业务
}
看起来没问题对吧?但项目一复杂,问题就来了:
- 验证逻辑散落在各个业务方法里,改一个规则要翻遍整个项目
- 同样的验证重复写了好几遍,比如「手机号格式」在注册、修改、绑定时各写一次
- 错误提示不统一,有的返回「参数错误」,有的返回「输入有误」
我在项目中遇到过最夸张的一次,一个接口的验证代码占了整个方法 60% 的行数。业务逻辑反而被淹没了。你想想看,这代码谁愿意维护?
重构的核心思路:分层与统一
我个人习惯把验证逻辑拆成三层:
- 基础格式验证 —— 比如非空、长度、正则匹配
- 业务规则验证 —— 比如库存是否充足、用户是否存在
- 权限验证 —— 比如当前用户是否有操作权限
这三层职责不同,混在一起就会乱。重构的第一步,就是让它们各归其位。
核心原则:验证逻辑与业务逻辑分离。验证只负责「能不能通过」,业务只负责「通过后做什么」。
实战:从散乱到整洁
我们来看一个具体的例子。假设有一个用户注册接口,原始代码可能是这样的:
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:验证重构的核心流程
下面这张图展示了验证逻辑重构前后的对比,以及重构后的核心流程:
这张图左边是重构前的状态,验证逻辑散落在各个业务方法里。右边是重构后的分层结构,每一层职责清晰。下方的流程展示了验证通过后,业务逻辑才能执行。
总结一下
重构输入验证逻辑,说白了就是两件事:分离关注点和统一处理方式。
分离关注点,让验证逻辑不再和业务逻辑纠缠在一起。统一处理方式,让所有验证都走同样的流程,返回同样的格式。做到这两点,系统的健壮性自然就上来了。
我个人的经验是,每次写新功能时,先花 10 分钟想想验证逻辑该怎么组织。这 10 分钟的投资,往往能省下后面几小时的调试时间。你想想看,是不是这个道理?