组合优先于继承这个原则没错,但也只是一种建议,而不是绝对的。
很多人看到两段代码差不多,第一反应就是抽个父类。它提醒的就是这件事。降低耦合、避免破坏封装、规避脆弱基类问题,这些都是组合的优势。但实际项目里有一种场景,继承反而是更自然的工程选择:
就是业务存在一条固定流程,只允许局部步骤变化。
我之前上线的钉钉OA工单审批就是这么个场景。核心操作就三个:创建OA工单、取消OA工单、钉钉回调处理。三个操作的流程是固定的。公司OA工单类型很多,补货订货审批、门店营业时间变更审批、商品售罄审批等一共6种,以后还会更多。流程固定,每种工单只有表单内容和审批后的处理不同,类型还很多,用继承是自然选择。
有人可能会说,为什么不用组合?把对接OA工单的逻辑拆成校验器、表单组装器、创建器等几个独立组件,每种工单类型组合这些组件,再自己组织调用顺序,不也能实现吗。
其实我一开始也这么想过。能实现。但流程对不对,更多得靠每个实现类自己盯着。每个实现类仍然拥有重新排列步骤的自由,有人可能忘了校验,有人可能先组装表单再组装基本信息。这些错误在代码审查时不一定能发现,上了生产就是事故。
我的做法是直接在父类把流程锁死。看一下AbstractDingTalkWorkFlowStrategy里createWorkFlow方法的核心流程:
Java
// 校验 → 组装基本信息 → 组装表单 → 创建
this.checkCanCreateWorkFlow(createWorkFlowCommand.getBizId());
CreateWorkFlowDTO dto = this.assembledBaseInfo(createWorkFlowCommand);
this.assembledFormComponents(dto);
this.doCreateWorkFlow(dto);
校验、组装基本信息、组装表单、创建,四步顺序写死在父类里。子类碰不到流程,只能实现变化点。
子类这边就简单了,只剩两件事。父类声明了两个抽象方法:
Java
// 子类必须实现的两个变化点
protected abstract void assembledFormComponents(CreateWorkFlowDTO createWorkFlowDTO);
protected abstract void approvalCompleted(DingTalkWorkFlowRecordEntity entity);
子类必须实现这两个方法,也只能实现这两个方法。补货订货的子类ReplenishmentOrderWorkFlowStrategy,在assembledFormComponents里组装补货订货的表单数据,在approvalCompleted里审批通过后发钉钉消息通知相关人员。
门店营业时间变更的子类ShopTimeChangeApplyWorkFlowStrategy,组装的是门店时间变更的表单,审批通过后回调门店系统更新状态。每个子类只关心自己那部分差异,cancel、handleCallBack这些固定逻辑在父类写一次,6个子类共享,不用复制。
还有一个细节。checkCanCreateWorkFlow这个方法在父类里是空的,补货订货的子类重写了它,检查是否重复申请、是否超过次数限制。不需要校验的子类什么都不用做。需要的人重写,不需要的人不管。扩展点固定在流程里,不会因为忘记调用而出问题。
至于组合。你要组织的是一组独立能力,各管各的,组合更合适。支付渠道,支付宝、微信、银联,每个都是独立能力,可以组合进任何业务。消息通知,钉钉、短信、邮件,也是独立能力。组合让每个能力独立变化、自由拼装,不需要绑死在一条流程里。
不过,继承让我坚持这么用的,还不是这些。
流程一旦变化,所有子类应该一起变化。比如审批流程以后统一加一步权限校验,在父类的createWorkFlow里加一行checkPermission(),6个审批类型自动生效,不用逐个改。你想想,换成组合的话,每个实现类都得自己加这一步,漏掉一个就是线上漏洞。继承共享的不只是今天的代码,更是未来的修改。值得继承的,是未来应该一起演进的流程。
组合优先于继承没有错。它批评的是把继承当作默认复用机制,为了少写几行代码就建继承关系。问题从来不在继承,在于你为什么建立这层继承。
判断什么时候用组合、什么时候用继承,我一般就看两件事:你面对的是几个独立能力,还是一条稳定流程里的几个变化点?这段逻辑未来是各自独立变化,还是应该随着所有实现一起演进?组合负责组织彼此独立的能力,继承负责约束需要一起演进的流程。如果一段逻辑未来应该随着所有实现一起变化,继承往往比组合更自然。面试遇到这个问题,能从业务场景和未来演进的角度给判断.
学过设计模式的话应该看出来了,上面的写法就是模板方法模式。设计模式只是后来的人给这种思想起了名字。业务怎么变,代码就该怎么设计。