接手过一个用户注册接口,里面十几个校验规则全部用if-else堆在一起,后来规则越加越多,代码长到每次改动都要在编辑器里翻半天。那段代码让我彻底想清楚一件事:校验逻辑不是不能多,而是不该以“手抽筋”的方式存在。后来我把整块逻辑重构成责任链模式,用一条链把校验规则串成流水线,新增规则时只需要往链上挂一个节点,改动量从“改一大坨”变成“加一两个文件”。这篇文章就把这套思路完整拆开讲,从模式原理到代码落地,再到真实项目里会踩的坑,适合正在被校验逻辑折磨、又不想上重型规则引擎的同学参考。
1. if-else校验链,是怎么从“还行”一步步烂到“手抽筋”的
很多设计模式文章一上来就画类图、讲定义,但我觉得责任链这东西,得先从“痛”说起。你只有真的被if-else校验折磨过,才能理解流水线式的写法到底解决了什么问题。
1.1 三年前的代码,看起来还挺正常
最早给一个用户注册接口写校验时,逻辑确实不多,大概长这样:
public String createUser(UserDTO dto) { if (dto.getUsername() == null || dto.getUsername().isEmpty()) { return "用户名不能为空"; } if (dto.getPassword() == null || dto.getPassword().length() < 6) { return "密码长度至少6位"; } if (dto.getEmail() == null || !dto.getEmail().contains("@")) { return "邮箱格式不正确"; } // 业务逻辑…… return "ok"; }三个if,每个还都挺直白。阅读起来不费劲,改起来也不费劲,顶多就是return语句多点。这时候你跟任何人说“我们需要引入责任链模式”,对方八成觉得你在小题大做。
但问题是,业务不会停在这里。
1.2 规则从3条变成10条之后,代码开始变味
三个月后,同一个接口的校验规则变成了这样:用户名不能包含敏感词,手机号要校验运营商号段,密码不能和用户名相同,邮箱要校验域名白名单,邀请码必须是有效状态,注册渠道必须在枚举范围内,用户协议版本号不能太老,IP要在地理白名单里……于是我看到了这种代码:
public String createUser(UserDTO dto, RegisterContext ctx) { // 第1层:空值校验 if (dto.getUsername() == null || dto.getUsername().isEmpty()) { return "用户名不能为空"; } // 第2层:格式校验 if (dto.getPassword() == null || dto.getPassword().length() < 6) { return "密码长度至少6位"; } if (dto.getEmail() == null || !dto.getEmail().contains("@")) { return "邮箱格式不正确"; } // 第3层:业务规则校验 if (ctx.isUsernameSensitive(dto.getUsername())) { return "用户名包含敏感词"; } if (dto.getPassword().equals(dto.getUsername())) { return "密码不能与用户名相同"; } // 第4层:外部依赖校验 if (!ctx.checkMobileSegment(dto.getMobile())) { return "手机号号段不支持"; } if (ctx.checkIpWhiteList(ctx.getIp()) == false) { return "IP不在白名单内"; } // 第5层:组合逻辑校验 if (dto.getAge() != null && dto.getAge() < 18 && "ADULT".equals(dto.getChannel())) { return "成年渠道不允许未成年用户注册"; } // ……后面还有三四条 }这段代码的问题不是“长”,而是它的结构在悄悄腐烂。每层之间有肉眼可见的顺序依赖——比如密码和用户名相同的判断必须放在空值校验之后,否则dto.getPassword().equals(dto.getUsername())会直接空指针。IP白名单校验放在最后,因为它是外部调用,慢,但你没法提前知道前面哪条规则会拦住请求。每新增一条规则,你都得从头到尾读一遍这段代码,找到合适的位置插进去,然后祈祷没插错。
1.3 真正让人“手抽筋”的,是改动成本和心理负担
我见过更夸张的版本——有人把十几个校验用if-else嵌套起来,每个分支还附带不同的错误码和错误提示,最外层再用一个循环去遍历多个字段。这种代码阅读时的心理负担非常大:
- 你要判断这条新规则应该放在哪一层,放错了前面的规则就拦截不到它;
- 你改一条规则,得担心它会不会影响后面所有规则的执行顺序;
- 你没法单独测试某一个校验,因为整段逻辑是耦合在一起的,想测用户名敏感词校验,就得先构造一堆能通过前面所有校验的数据;
- 最要命的是,有一天产品说“把手机号校验从原有逻辑里拆出来,独立成一个可配置项”,这时候你面对的是一大坨动一发而牵全身的代码。
说白了,if-else不是不能写,而是它把“规则”和“规则的执行顺序”焊死在一起了。你想独立地增删规则、调整顺序、单独测试,if-else这种写法给不了你任何支持。
这时候责任链模式的价值就体现出来了:它把每条规则拆成独立的节点,把执行顺序变成一条可以自由拼装的链。规则之间不再互相“看见”,每个节点只关心自己那一件事。
2. 责任链的“流水线”思维:把校验从“判决书”变成“传送带”
2.1 一句话看懂责任链模式
责任链模式(Chain of Responsibility)是GoF二十三个经典设计模式之一,核心思想用一句话说就是:让多个对象都有机会处理同一个请求,把它们串成一条链,请求沿着链传递,直到某个对象处理它为止。
放到校验场景里翻译一下:每个校验规则是一个节点,节点之间通过next指针串联起来,请求数据像流水线上的工件一样从第一个节点流到最后一个节点。每个节点只回答一个问题——“我负责的这条规则满不满足,不满足我就返回错误,满足我就把数据传给下一个节点”。
这个模式的精髓在于:发送请求的调用方不知道也不关心链上有多少个节点、节点内部是什么逻辑,它只把请求扔给链头,然后等着结果。
2.2 为什么“流水线”这个比喻特别贴切
网上讲这个模式都喜欢用“层层审批”来比喻,比如请假条从组长到经理到总监。但我在校验场景里更偏爱“流水线”这个说法,原因是工厂产线的几个特性跟校验逻辑高度吻合:
- 每个工位只干一件事:流水线上拧螺丝的工位不会顺便去喷漆,对应到校验里,用户名非空校验的节点不会去管密码长度;
- 工件顺序经过所有工位:校验数据也是按固定顺序经过每个规则节点,顺序可以由你定义;
- 某个工位发现次品就下线:生产线上质检不合格的工件直接踢出产线,不会继续往下流,对应校验里某个规则不通过就立刻返回错误;
- 新增工位只需要在产线上加一段:流水线想加一道质检工序,不需要把整条产线拆了重装,责任链里加一个规则节点也是类似的效果。
为什么很多团队在做大型重构时倾向用责任链而不是不停加if-else?因为产线是“柔性的”——你可以调整工位顺序、可以临时旁路某个工位、可以单独调试某道工序。而if-else是“浇筑”出来的,改一道工序等于砸整块混凝土。
2.3 责任链 vs 策略模式 vs 装饰器模式:别搞混了
实践里这三个模式经常被拿来比较,我把区别用表格列一下,方便选择:
| 模式 | 核心特征 | 适合场景 | 校验场景中的对应 |
|---|---|---|---|
| if-else | 分支判断硬编码 | 规则极少、几乎不变 | 2~3条固定校验 |
| 策略模式 | 从多个算法中选一个执行 | 同一行为有多个可替换的实现 | 不同渠道用不同校验策略 |
| 装饰器模式 | 动态叠加增强功能 | 给对象一层层加职责 | 校验之外打日志、加缓存 |
| 责任链模式 | 多个处理者依次尝试处理 | 请求可能被链上任意一环拦截 | 一组校验按顺序执行、任一失败即停 |
一句话记忆:策略模式是“从一堆里挑一个”,责任链是“把一串顺序走完,谁先拦住了谁说了算”。校验这种“多个规则、依次判断、失败即返回”的场景,责任链天然契合。
3. 落地第一版:写一个能用的校验责任链
原理讲完了,直接上代码。我会用Java写,因为这是最典型的责任链落地语言,但思路本身在Python、Go、JavaScript里完全通用——无非是把“类”换成“对象”或“函数数组”而已。
3.1 先把抽象节点定义出来
校验责任链的第一步,是定义所有节点都要遵守的“接口契约”。这里有个设计决策:validate方法返回什么?我建议返回一个统一的ValidateResult对象,而不是直接返回布尔值。原因很简单——校验失败时需要告诉调用方“哪条规则、什么原因、错误码是什么”,只返回一个false是远远不够的。
// 校验结果:携带规则名、是否通过、错误信息和错误码 public class ValidateResult { private boolean success; private String ruleName; private String errorMessage; private String errorCode; // 成功结果的快捷方法 public static ValidateResult ok() { ValidateResult r = new ValidateResult(); r.success = true; return r; } // 失败结果的快捷方法 public static ValidateResult fail(String ruleName, String errorCode, String errorMessage) { ValidateResult r = new ValidateResult(); r.success = false; r.ruleName = ruleName; r.errorCode = errorCode; r.errorMessage = errorMessage; return r; } public boolean isSuccess() { return success; } // getter / setter 省略 }对应的抽象校验器接口长这样:
public abstract class AbstractValidator<T> { protected AbstractValidator<T> next; // 设置下一个节点,返回下一个节点,便于链式调用 public AbstractValidator<T> setNext(AbstractValidator<T> next) { this.next = next; return next; } // 模板方法:本节点校验通过则传给下一个节点 public ValidateResult validate(T data) { ValidateResult result = doValidate(data); if (!result.isSuccess()) { return result; } if (next != null) { return next.validate(data); } return ValidateResult.ok(); } // 子类实现自己的校验逻辑 protected abstract ValidateResult doValidate(T data); }这里validate方法本身就是一个模板方法:先执行自己的doValidate,失败就返回失败,成功就把请求传给next。这种写法把“链路调度逻辑”收敛到了抽象类里,每个具体的校验节点只需要关心自己的规则,不用操心“我下面还有谁”。
3.2 实现几个具体校验节点
有了抽象类,具体节点写起来就是“复制粘贴再改规则”的活了。比如非空校验、长度校验、手机号校验:
public class NotEmptyValidator extends AbstractValidator<UserDTO> { private final String fieldName; public NotEmptyValidator(String fieldName) { this.fieldName = fieldName; } @Override protected ValidateResult doValidate(UserDTO data) { try { Field field = UserDTO.class.getDeclaredField(fieldName); field.setAccessible(true); Object value = field.get(data); if (value == null || value.toString().isEmpty()) { return ValidateResult.fail("NotEmpty", "FIELD_EMPTY", fieldName + "不能为空"); } } catch (NoSuchFieldException | IllegalAccessException e) { return ValidateResult.fail("NotEmpty", "FIELD_ERROR", fieldName + "字段不存在"); } return ValidateResult.ok(); } }说实话,用反射去拿字段在真实项目里有点绕,我更推荐直接把Function<T, Object>传进去,让节点知道“从数据里取哪个值”。不过为了示例直观,这里先用字段名。实际项目里的节点更多是这样的写法:
public class PasswordLengthValidator extends AbstractValidator<UserDTO> { @Override protected ValidateResult doValidate(UserDTO data) { if (data.getPassword() == null || data.getPassword().length() < 6) { return ValidateResult.fail("PasswordLength", "PASSWORD_TOO_SHORT", "密码长度至少6位"); } return ValidateResult.ok(); } } public class MobileValidator extends AbstractValidator<UserDTO> { @Override protected ValidateResult doValidate(UserDTO data) { String mobile = data.getMobile(); // 手机号规则:1开头 + 11位 if (mobile == null || !mobile.matches("^1\\d{10}$")) { return ValidateResult.fail("Mobile", "MOBILE_INVALID", "手机号格式不正确"); } return ValidateResult.ok(); } }每个类只干一件事:检查自己的规则,合规就ok(),不合规就返回一个带着规则名、错误码、错误提示的失败结果。
3.3 组装流水线,真的只有一行代码
接下来是最爽的部分——把所有节点串起来。责任链模式的价值在这里体现得淋漓尽致:
AbstractValidator<UserDTO> validator = new NotEmptyValidator("username") .setNext(new PasswordLengthValidator()) .setNext(new MobileValidator()) .setNext(new EmailValidator()) .setNext(new SensitiveWordValidator());一行链式调用,把五个校验规则串成一条流水线。setNext返回的是追加进来的那个节点,所以你可以无限往下挂。调用的时候更简单:
ValidateResult result = validator.validate(userDTO); if (!result.isSuccess()) { // 统一处理错误:记录日志、返回给前端 }调用方完全感知不到链上有几个节点、每个节点内部是什么逻辑。它只知道:把数据扔给链头,拿回一个结果。这就是责任链最核心的封装价值——调用方和规则实现彻底解耦。
3.4 为什么入口要收敛:再把链包装一层
有人可能会问:直接暴露链头对象够不够?我觉得不够。真实业务中,“如何组装这条链”本身就是一种配置逻辑,应该被单独封装。所以我通常还会写一个类似UserRegisterValidatorFactory的东西,或者用Spring的@Configuration把链暴露成Bean:
@Configuration public class ValidatorConfig { @Bean("userRegisterValidator") public AbstractValidator<UserDTO> userRegisterValidator() { return new NotEmptyValidator("username") .setNext(new NotEmptyValidator("password")) .setNext(new PasswordLengthValidator()) .setNext(new MobileValidator()) .setNext(new EmailValidator()) .setNext(new SensitiveWordValidator()); } }这样业务代码里只需要注入这个Bean,规则怎么排布、要不要加新节点,都集中在配置类里改。用Spring的同学还可以把每个校验节点做成@Component并实现某个Ordered接口,然后让Spring自动注入一个List<AbstractValidator>,按order排序后自动串链。我实测下来,这种方式在规则数量超过15条、团队多人协作时特别有用——每个人只管好自己的校验类,不用动别人的代码。
第一版到这里已经能用了。但“能用”和“好用”之间还有一段距离,下面四个升级点是我在真实项目里反复用到的。
4. 从“能用”到“好用”:四个升级点
4.1 升级点一:短路 vs 全量——fail-fast 还是 fail-collect
第一版的链是“短路”的:某个节点失败,直接返回,后面所有节点不再执行。这在大多数场景下是对的——用户表单第一项就填错了,没必要继续校验后面的。
但有一种场景需要“全量校验”:比如管理后台上传一批用户数据,希望一次告诉运营人员所有字段的错误,而不是改一个错一个再改一个。这时候责任链就不能短路了,得让请求流经每个节点,收集所有错误。实现方式是在抽象类里加一个“聚合模式”开关:
public ValidateResult validate(T data, boolean collectAll) { List<ValidateResult> errors = new ArrayList<>(); AbstractValidator<T> current = this; while (current != null) { ValidateResult result = current.doValidate(data); if (!result.isSuccess()) { if (!collectAll) { return result; } errors.add(result); } current = current.next; } if (!errors.isEmpty()) { return ValidateResult.collect(errors); } return ValidateResult.ok(); }改成用while循环而不是递归去遍历链,是为了在聚合模式下好收集错误列表。注意这里把validate里的模板逻辑移到了外面,抽象类里面保留doValidate给子类实现。这样改造后,默认短路、按需聚合,两种模式用同一个链结构就能支持。
4.2 升级点二:节点排序——用注解还是用代码?
链式装配天然规定了顺序:先setNext的先执行。但规则多了以后,手动维护setNext顺序容易乱。我在一个项目里遇到过这种情况——产品要求“手机号校验必须在短信验证码校验之前”,但代码里有人把新加的短信验证码节点挂到了手机号节点前面,导致手机号为空时短信验证码还在傻乎乎地校验格式。
解决办法有两个方向:
- 给每个节点加
order属性,装配时按order排好,再串链; - 用Spring的
@Order注解,自动注入List后排序。
如果你的项目没上Spring,可以用一个简单的装配工具类:
public class ValidatorChain<T> { private final List<AbstractValidator<T>> validators; public ValidatorChain(List<AbstractValidator<T>> validators) { // 按order排序 validators.sort(Comparator.comparingInt(AbstractValidator::getOrder)); this.validators = validators; } public ValidateResult validate(T data) { for (AbstractValidator<T> validator : validators) { ValidateResult result = validator.doValidate(data); if (!result.isSuccess()) { return result; } } return ValidateResult.ok(); } }这里的排序逻辑和链式逻辑是等价的,都是顺序执行、失败即停。我倾向用ValidatorChain这种包装类替代裸链式调用,原因有二:一是排序逻辑集中在链内部,装配时不用手工调整setNext顺序;二是方便把聚合模式、日志注入等功能统一加进链里,而不是让调用方自己处理。
4.3 升级点三:让错误定位更省心——规则名和参数快照
纯if-else写法中,错误定位靠肉眼扫代码;责任链写法中,错误定位靠规则名。设计ValidateResult时我会多塞几个字段:规则名ruleName、错误码errorCode、错误详情errorMessage,还建议加一个contextSnapshot,用来记录校验失败时数据的关键快照。
比如密码长度校验失败时,快照里可以记录“当前密码长度=3,要求≥6”。日志打印出来是这个效果:
[VALIDATION_FAILED] rule=PasswordLength, code=PASSWORD_TOO_SHORT, detail=密码长度至少6位, snapshot={passwordLen=3, userId=9527, reqId=abc-123}线上排查的时候,这样的日志能帮你省下大量时间。你不需要去翻业务代码自己猜测“这条错误是哪个if抛出来的”,规则名直接告诉你。
4.4 升级点四:让调用方少写代码——结合建造者模式
链式装配虽然只有一行,但组装逻辑散落在各个业务方法里也很累。更优雅的做法是把链的构建封装成一个小型建造者:
public class ValidatorBuilder<T> { private final List<AbstractValidator<T>> validators = new ArrayList<>(); public ValidatorBuilder<T> add(AbstractValidator<T> validator) { validators.add(validator); return this; } public ValidatorChain<T> build() { return new ValidatorChain<>(validators); } }用起来是这样的:
ValidatorChain<UserDTO> chain = new ValidatorBuilder<UserDTO>() .add(new NotEmptyValidator("username")) .add(new NotEmptyValidator("password")) .add(new PasswordLengthValidator()) .add(new MobileValidator()) .add(new EmailValidator()) .build(); ValidateResult result = chain.validate(userDTO);建造者模式在这里就是粘合剂,它让“装配链”这个动作变得更声明式了——读代码的人一眼就能看出链上有哪些规则,顺序是什么,不需要去追踪setNext的返回值。
5. 真实项目里值得注意的四类坑
责任链写起来清爽,但用起来有几个坑是文档里不会写的。我踩过,帮你也踩踩。
5.1 坑一:规则顺序依赖与隐式NPE
责任链模式下,每个节点是独立的类,但执行是串行的——前一个节点不通过,后一个节点根本不会执行。这个特性带来一个隐式依赖:后置节点默认相信前置节点已经完成了某类校验。
比如前面提到过的“密码不能与用户名相同”这条规则,它隐含的假设是“用户名和密码都非空”。在if-else里你还能靠肉眼看到前面有非空判断,但拆成责任链节点后,这类依赖就藏起来了。你新写一个节点,很自然地用了data.getPassword().equals(data.getUsername()),但链上万一有人把非空校验挪到它后面,或者新节点被挂在了非空校验的前面,空指针就来了。
我的经验是给所有依赖前置规则的节点加防护:
public class PasswordNotEqualsUsernameValidator extends AbstractValidator<UserDTO> { @Override protected ValidateResult doValidate(UserDTO data) { if (data.getUsername() == null || data.getPassword() == null) { // 前置规则缺失时,避免本节点NPE,直接放行 return ValidateResult.ok(); } if (data.getUsername().equals(data.getPassword())) { return ValidateResult.fail("PasswordNotEqualsUsername", "PASSWORD_SAME_AS_USERNAME", "密码不能与用户名相同"); } return ValidateResult.ok(); } }不要觉得这是多余代码。你自己想想,一个10节点的链,你能保证以后每个人都知道“这个节点必须在非空校验后面”吗?与其靠默契,不如靠防御。
5.2 坑二:规则冗余和重复校验
责任链的节点看似解耦了,但解耦的是“代码结构”,不是“业务语义”。一个很常见的现象是:两个节点分别写了“手机号为空判断”和“手机号格式判断”,前者挂在非空校验组的公共节点里,后者挂在手机号专项节点里。功能上没毛病,但时间一长,链上会出现大量“隐性重复”——同样的规则在多个节点里各做一遍,只是错误消息和错误码不同。
这种情况我建议做两步处理:
- 把公共前置规则(非空、类型、最大值最小值)做成“组的入口节点”,专门的业务规则挂在这个入口节点后面;
- 定期review链上节点的职责清单,把重复的规则合并,而不是任其扩散。
也可以用静态检查工具扫描规则类的相似逻辑,但坦白说最有效的还是人肉review——反正责任链的每个节点都很短,review成本比if-else低得多。
5.3 坑三:链上节点出现“副作用”
设计责任链时有一个很容易越界的点:有人为了省事,把“校验通过后的字段清洗”也塞进校验节点里,比如在校验手机号格式后顺手把手机号里的空格trim掉。
这在功能上是“顺便做一下”,但严重违背了责任链的职责边界。校验链应该是无副作用的——它只回答问题“这个数据合规吗”,不负责修改数据。一旦一个节点偷偷改了数据,后面所有节点看到的输入就变了,线上出问题时会非常难排查,因为你不知道是哪一环把数据改掉的。
如果确实需要在校验通过后做些清洗,我建议单独建一条“清洗链”,或者把清洗逻辑放在校验链全部通过之后的业务处理阶段。规则的职责要单一,这样链才是可预期的。
5.4 坑四:性能损耗和日志爆炸
我不会跟你说责任链“零开销”这种话。节点多、每次调用都串行走一遍,确实比一段if-else慢那么一点点。但这个损耗在绝大多数业务场景里可以忽略——一次校验链通常不到1毫秒,而一个外部接口调用动辄几十毫秒。
真正需要注意的是日志。如果你在每个节点里都打一条日志,一个10节点的链每次校验就是10条日志,压测时直接刷屏。我做过一个优化:默认只记录“校验失败”的日志和“整链通过”的汇总日志,链路中各节点的成功日志放到debug级别。还有一个性能点是外部依赖型校验节点——比如IP白名单查询、邀请码状态查询——这类节点如果挂在主链上,建议先做本地状态缓存或短路预处理,避免每个请求都去打外部接口。
6. 边界判断:什么时候该用责任链,什么时候别硬上
写到这里肯定有人跃跃欲试,想把项目里所有if-else都改成责任链。我劝你冷静,这个模式有自己的适用边界。
6.1 适合用责任链的场景特征
结合我的实践,满足下面几条中至少两条,就值得考虑责任链:
- 校验规则多于5条,且预期会持续新增;
- 规则之间存在明确的顺序关系,且顺序可能调整;
- 希望每条规则能被独立测试、独立复用;
- 同一套校验逻辑要在多个入口复用(比如Web接口、定时任务、消息消费者都要做同样的校验)。
我这边的典型例子就是用户注册、订单提交、数据导入这类“多个入口都要做同一套规则校验”的场景。用责任链把规则集中定义,三个入口都引用同一条链,规则变更只改一处。
6.2 不适合硬上的场景
反过来,下面这些情况就别折腾了:
- 规则只有2、3条,且几乎不变——上责任链反而增加了类和配置的数量;
- 校验规则之间是强相关的“组合关系”——比如“A字段必须满足条件X,且当Y成立时A还要满足Z”,这种纠缠逻辑拆成独立节点反而让链上的隐式依赖变多变乱;
- 团队对设计模式不熟,写完没人能维护——我见过有人把简单校验硬写成责任链,后来新来的同事完全看不懂,最后还是改回了if-else。
责任链是“组织规则”的工具,不是“消除规则”的工具。它的核心收益是让新增和调整规则的成本变低,但它不能帮你解决规则本身复杂的问题。如果规则本身纠缠不清,先梳理业务再谈模式。
6.3 框架里的影子:从Servlet Filter到Spring Interceptor
其实责任链模式你早就在用,只是没意识到。Java Web开发里的Filter、Spring MVC的Interceptor、Netty的ChannelHandler,本质都是责任链的变体——请求依次经过一系列处理器,任何一个处理器都可以决定“拦截”还是“放行”。
理解了这一点,再看校验责任链就没有任何神秘感了。它就是你自己写的一层“小Filter”,只不过过滤的对象从HTTP请求变成了业务数据。你可以把Filter里学到的经验迁移过来:哪些过滤器应该放在最前面(公共前置)、哪些应该放最后(兜底校验)、如何控制过滤器的执行顺序——这些经验在校验链里完全适用。
说到底,责任链模式是“把顺序判断逻辑从业务代码中抽离出来”的通用抽象。我在实际项目中最大的体会是:引入责任链初期会多写几个类,觉得“还不如if-else直截了当”,但等到第8条、第10条规则加进来,你会庆幸当初的改造。新需求来了,新建一个校验类,在配置里挂上链,两个文件搞定,没动任何老代码——这份从容,就是责任链带来的最大回报。
最后再分享一个小技巧:给链上的节点统一起一个“动词+名词”的类名,比如MobileValidator、SensitiveWordFilter、AdultChannelGuard,链的关系一眼就能看懂,比叫Rule1Validator、Rule2Validator好用得多。命名清晰,比任何注释都管用。