应用场景
相信大家遇到过这种场景:
旧代码中已经有一堆的if-else或者switch-case了;产品却要求在这段流程里增加一个新的功能。
这种时候大家会怎么做?
我的建议是:
重构这段代码。在重构的基础上,加入新的功能。
肯定会有人说:
工期本来紧张,再对原有代码进行重构,岂不会更加捉襟见肘?
这里介绍的(也是我在实践中经常使用的)这种方式,我称之为“接口-分发器模式”。它可以在尽量减少重构工作量的同时,完成大部分重构工作。
类图
接口
这个模式首先将旧代码/功能抽取为一个接口(ServiceInterface.java)。这个接口的抽象能力,应该能够同时覆盖旧代码中的原有逻辑和新需求中的功能。换句话说就是新、旧代码都可以抽象为同一个接口。
如果这一点都无法做到,建议先回头想想这两段逻辑应不应该放到同一个抽象内。
例如我在一次重构中所做抽取的接口:
public interface RequestApprover {
void approveById(Integer id, Request requestInfo,
UserInfo approver) throws ServiceException;
}
|
这个接口是对请求数据(Request)的审批操作的抽象。
请求数据一共有三类(其中旧类型两种,新需求一种);审批操作同样也有三类(同样,旧类型两种,新需求一种)。这样,最多会有九种审批逻辑(不过实际中只有六种)。而这些审批逻辑和代码,都可以用这一个接口来描述。
分发器
分发器(ServiceDispatcher.java)是服务的入口。但它本身并不提供任何业务服务,而只负责将请求分发给实际的服务处理类。
从这一点上看,分发器其实很像一个工厂。这么说也没错,不过这个分发器的重点在于“分发”,而不是“创建”。
另外,将它隐藏在对外接口之下,是因为我将这个分发器理解为接口的一种实现;它属于抽象之中,不需要被抽象之外的调用者感知。这是我个人偏好。
对应前面的接口,我用到的分发器是这样的。
class RequestApproverAsDispatcher RequestApprover {
private RequestApprover approver4First4NotLate;
private RequestApprover approver4First4PseudoOver;
private RequestApprover approver4Final4NotLate;
private RequestApprover approver4Final4M1;
private RequestApprover approver4Final4PseudoOver;
@Override
public void approveById(Integer id, Request requestInfo,
UserInfo approver) throws ServiceException {
RequestApprover requestApprover;
switch (some_field) {
case FIRST_APPROVED:
case FIRST_REJECTED:
requestApprover = xxx;
break;
case APPROVED:
case REJECTED:
requestApprover = yyy;
break;
default:
throw new UnsupportedOperationException();
}
requestApprover.approveById(id, requestInfo, approver);
}
}
|
具体服务类
具体服务类承担实际上的业务逻辑。在类图中,它们被表示成了Service4Scene1.java ~ Service4Scene7.java。并且,我专门画了ServiceAsAdapter.java和ServiceAsSkeleton.java 来表示:这些具体服务类还可以有自己的组织方式、应用自己应用的模式。
在我上面的例子中,我通过一个RequestApproverAsSkeleton.java定义了模板。而在另一项需求中,我用了组合和中介——至少我将那几个类理解为中介模式。
小结
本质上,这个所谓“接口-分发器模式”是一种策略模式。但是它比策略模式多一点东西——分发器。另外,在实践应用中,它不可能只有策略。在“具体服务类”的组织上,几乎都会用上更多的模式。
题外话,就设计模式的应用上,有策略则必有工厂,有工厂几乎必有单例,这似乎也自成一种“模式”。
重构
那么,这个“模式”要怎样应用到重构中呢?
很简单——让旧代码和新代码都成为“具体服务类”中的成员,并且是不同的成员。
仍以上面的例子来说,我将旧代码和新代码分别安排在这两个类中。再结合前面的分发器,很简单的就完成了这次重构,并同时完成了新需求。
旧代码在这个类中:
class RequestApprover4First extends
RequestApproverAsSkeleton {
private static final Logger LOGGER = LoggerFactory
.getLogger(RequestApprover4First.class);
private RequestService service;
@Override
protected void approve(Request requestInfo,
Request request) throws InvalidDataException {
……
}
@Override
protected void reject(Request requestInfo,
Request request) {
// 不做处理
}
@Override
protected void configRequest(Request requestInfo,
Request request, UserInfo approver) {
……
}
}
|
而新的业务在这个服务中:
class RequestApprover4Check extends
RequestApproverAsSkeleton {
private static final Logger LOGGER = LoggerFactory
.getLogger(RequestApprover4Check.class);
@Override
public void approveById(Integer id, Request requestInfo,
UserInfo approver) throws ServiceException {
……
// 这个方法中有额外处理
}
@Override
protected void approve(Request requestInfo,
Request request) throws ServiceException {
……
}
@Override
protected void reject(Request requestInfo,
Request request) {
// 不做任何操作
}
@Override
protected void configRequest(Request requestInfo,
Request request, UserInfo approver) {
……
}
}
|
优点和缺点
优点应该说比较明显:新、旧逻辑和代码被隔离开了,也就完成了解耦合。并且后续如果还要加新的需求,也可以比较轻松的隔离到新的服务类中。相信接手过旧系统、旧代码的朋友们都能理解其中的意义。
另外,旧代码可以保持不动,或者简单的复制到对应的具体服务类中。因此,改造工作量比较小。
缺点呢?一是容易造成“类爆炸”。虽然不一定变得太多,但是类的数量肯定比不用模式要多。二是这种模式有时候不会(也不需要)对旧代码做任何改动。这样一来,重构目标实际上并没有实现。
最后补充
做重构之前,一定要有用于验证旧代码功能的测试,并且尽可能的覆盖流程分支。