生活中处处存在模版,模版定义了大的框架,具体内容由使用者填充即可,这给很多人的生活、工作带来了很大的遍历。比如:
- PPT模版:好的PPT模版提供了更全面的叙述框架,更优美的UI画面&图标,提升用户的PPT制作水平
- 技术方案模版:通过使用技术方案模版,确定需求应用的各个方面,如需求背景、上下游业务方、存储层设计、接口设计、接口性能评估等。通过完善的模版,技术方案制作时能够考虑的更加全面。
- …
你也可以想象生活中处处可见的其他“模版”,这是一个标准化的过程。在开发技术栈中,spring、mybatic等框架其实也是模版的体现,通过标准化模版式的约定简化开发成本,提升开发效率。因此,我们该怎么通过面向对象语言来体现这种标准化的过程(模版)呢?答案就是模版方法模式。
一、模版方法模式
先思考下实现这种标准化过程的模板类应该具有哪些职责?① 明确指定了标准化过程都有哪些子元素?② 这些子元素在模版中如何组织 。这两点也是我们去评价是否使用这套模版的核心问题,如Python项目不会去考虑使用含有"JVM内存分析"的技术方案模版、产品在写文档时也不会去考虑开发技术方案等。
再考虑下,模板类中“子元素”应该有模板类指定吗?这种具体问题具体分析,有些模版会给个默认示例,有的模版会置之为空,完全由用户来实现。当然,即使是前者,模版肯定也是允许用户重写或覆盖的。所以,这么一说,在代码实现上是否非常适合抽象类呢。
总结下抽象模版类的设计:
- 明确了一组标准化过程(或算法框架)-模版方法(templateMethod)
- 给出默认或要求用户实现的过程子元素(或步骤)-基本方法([action1(), action2(),…])
抽象模板类结构确定之后,那其具体实现类,即其子类又该注意哪些问题。对于模板类的基本方法而言,实现类必须实现其所有的抽象方法(子类必须实现),非抽象方法视业务要求而定是否需要重写。那问题在于子类(具体实现类)是否应该重写抽象模板类已经确定的模版方法(标准化过程)?我觉得可以通过下面两个方面分析:
- 模版含义:从模版的含义上来看,其实际是一套标准化过程,具体实现应该尽可能遵守这套流程,才能达到模版效果及预期。同时,结合实际情况看,也是允许具体实现时删除或更改流程顺序,毕竟任何东西都不能是死的要灵活应用。但一定要注意,这里的灵活也仅是小范围的改动,否则,就不用使用这套模版了。
- 设计原则:如果子类重写了模版方法,更改了标准化流程,子类重写方法与模板类的模版方法业务含义不一致了,因此不满足里氏替换原则。
结合上面两点来看,子类不应该重写模版方法(模版方法通常使用final关键字),但是如何满足具体实现类可控范围内的灵活性呢?子类不允许修改模版方法的逻辑,但是模板类可以向子类提供修改模版方法的能力,即钩子方法。模板类可以通过由子类实现或重写的钩子方法来改变标准化过程。
总结下具体子类的设计:
- 必须实现模板类所有的抽象基本方法
- 不允许重写模板类中的模版方法,但可通过模板类的钩子方法来控制模版方法的标准化流程。
到目前我们已经可以给出模板类的大致类图如下:
二、应用实践
模版方法的应用场景非常广泛,只要你能够对一件事情总结出一套标准化的流程,那就可以使用模版方法来实现之。我这里就给出一个开发过程中比较贴近的场景:随着业务的增长需要,服务常会根据职责被拆分为各个单服务,这产生了上下游依赖关系。上下游可通过RPC调用来进行数据交换,这里RPC调用的就是我们通常说的接口。我们试着给出接口对rpc请求大概处理流程:
步骤1:验证接口权限
步骤2:处理请求线程上下文信息
步骤3:解析接口请求参数
步骤4:校验接口请求参数
步骤5:执行具体的接口业务处理操作
步骤6:返回接口响应结果
步骤7:清空上下文信息
注:这里仅是示例,实际上不同业务的接口处理逻辑大多不同,标准化流程也存在差异,即模版之间是会存在差异的。
定义了这样一套标准化流程后,该服务的任何接口均可通过这套流程处理RPC请求。模版及其具体实现代码如下:
/**
* 处理RPC请求模版类
*/
public abstract class AbstractRPCHandler {
// 验证接口权限
protected abstract boolean checkPermission();
// 处理请求上下文
protected abstract void processContext();
// 解析接口请求参数
protected abstract RequestData parseRequest();
// 校验接口请求参数
protected abstract boolean validateRequest(RequestData requestData);
// 执行具体的接口处理操作
protected abstract ResponseData process(RequestData requestData);
// 返回接口响应结果
protected abstract void sendResponse(ResponseData responseData);
// 是否清空上下文
protected boolean toClearContext() {
return false;
}
// 清空上下文
protected abstract void clearContext();
// 模板方法,定义rpc接口处理的流程
public final void processInterface() {
if (!checkPermission()) { // 1
System.out.println("无权限访问接口");
return;
}
processContext(); // 2
RequestData requestData = parseRequest(); // 3
if (!validateRequest(requestData)) { // 4
System.out.println("请求参数校验失败");
return;
}
ResponseData responseData = process(requestData); // 5
sendResponse(responseData); // 6
if(toClearContext()) {
clearContext(); // 7
}
}
}
/**
* 处理RPC具体实现类
*/
public class ConcreteRPCHandler extends AbstractRPCHandler{
@Override
protected boolean checkPermission() {
// 验证权限的具体实现...
return false;
}
@Override
protected void processContext() {
// 处理上下文的具体实现
}
@Override
protected RequestData parseRequest() {
// 解析接口请求参数的具体实现
return null;
}
@Override
protected boolean validateRequest(RequestData requestData) {
// 校验接口请求参数的具体实现
return false;
}
@Override
protected ResponseData process(RequestData requestData) {
// 执行具体的接口处理操作的具体实现
return null;
}
@Override
protected void sendResponse(ResponseData responseData) {
// 返回接口响应结果的具体实现
}
@Override
protected void clearContext() {
// 清空上下文的具体实现
}
}
在如上示例中,抽象模版类的clearContext()方法实际上就是钩子方法,用于控制标准化流程processInterface()方法中是否执行清空上下文操作。关于模版方法模式有两个主要的问题:
模版方法模式是否符合开闭原则?从类的扩展角度看,新增具体子类不会影响原有业务逻辑,因此是符合开闭原则的。但是,在一些参考资料中认为模版类中增加基础方法,就需要所有子类跟随修改,所以不符合开闭原则。我在前面的开闭原则讲解中说过,有些改动不能算违背开闭原则,需要看改动背后的业务需要是否本身就对原有业务产生变动了,即需求变动了,代码改动是正常的【那我们得要求产品不改动需求】。因此,我们得先知道“模板类增加基础方法”的背后的业务改动的属性,若属于业务改动,那如前所述,代码改动是符合预期的【除非你跟产品说我不做】。相反,若属于业务扩展,那么扩展的方法似乎又不该放入标准化流程中,因为标准化流程是模板类所负责的职责,职责都变了,那怎么说是业务扩展呢。所以,“模板类增加基础方法”这件事情本身就属于业务改动,就是会影响原有业务代码逻辑-所有子类均需要适配修改。
模版方法模式是否符合里氏替换原则?很多人认为由于模版方法中子类的行为影响了父类的行为,所以不符合开闭原则。这种说法我认为还是没有理解历史替换原则的本质,被很多书籍、定义、条件弄混以至于无法真正理解。我在前面的相关设计原则文章说过这个原则的本质就是子类方法的业务含义必须与基类保持一致或兼容。我们首先看子类实现抽象方法算不算破坏里氏替换原则?如果算的话,那你告诉我该原则还怎么和依赖倒置原则兼容。如果不算,那子类实现的业务逻辑父类可是并没有呀,所以问题不能停留在表面,抽象方法指的是业务含义的定义,约束子类的实现【注:子类实现抽象方法也必须符合抽象定义,否则就是强行违背里氏替换原则】。我们再看模版方法,模版方法是具体业务逻辑还是业务含义?实际上就是业务逻辑,但是这个业务逻辑(标准化流程)是所有子类和父类所共有。这就要求所有子类的标准化流程必须和父类定义的一样,不允许修改,因此模版方法符合里氏替换原则有两个条件:① 模版方法需使用final关键字修饰 ② 不允许有钩子方法。满足这两个条件即满足里氏替换原则,否则就是不满足了。【钩子方法这个条件其实也好理解,子类要是重写了父类方法,那不就是违背了里氏替换原则么,所以确实不能有】
模版方法模式优点:
- 将标准化流程封装起来供实现类使用,本身就是开闭原则的体现
模版方法模式缺点:
- 子类的可定制性受限,必须符合标准化流程
- 采用钩子方法的模版方法模式不符合里氏替换原则,使用多态时会造成不必要的误解,降低代码可读性。