服务编织时用模板方法模式是一种非常实用技巧,通过模板方法定义出服务基本操作、日志、异常处理等,也方便做限流、报警、流量统计等。这里的可扩展性体现在,当需要实现新添加的服务时,只需要套用模板,实现差异点就可以了。当然模板对可扩展点的定义和粒度都会影响具体的效果。
以API服务的实现为例,实现一个简单模板,有基本的日志、异常处理,代码如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// API服务实现模板
public abstract class ApiServiceTemplate<R, P> {
protected Logger logger = LoggerFactory.getLogger(getClass());
// 业务逻辑入口
protected abstract R process() throws BizException;
// 接口参数
protected abstract P getParam();
// 模板执行入口
public Response<R> execute() {
long startTime = System.currentTimeMillis();
logger.info("Enter service, params:{}", getParam().toString());
try {
R result = process();
return new Response<R>(SystemCode.BIZ_SUCCESS, result);
} catch (BizException e) {
logger.error("Api sevice execute Bizexception", e);
return new Response<R>(SystemCode.BIZ_FAILED, null);
} catch (Throwable e) {
logger.error("Api sevice execute error", e);
return new Response<R>(SystemCode.BIZ_FAILED, null);
} finally {
long totalTime = System.currentTimeMillis() - startTime;
if (totalTime > 200) {
logger.warn("This method used too long time, please check and optimize it:{}ms", totalTime);
}
}
}
}
// 其中Response、SystemCode和BizException的代码我删掉了,只突出重点
针对一些具体的业务流程,模板方法的作用更明显,业务的具体实现是应用的核心代码,这部分可复用的价值很高,但相应地,问题也很多。主要有两点:
问题一:合作开发成本高。别人实现的带有强烈主观意愿的设计思路,往往都不那么好理解。且不好的设计尤其是各种“专属设定”往往会隐藏着坑!我曾经接手一个应用,服务实现对流程和操作抽象度很高,甚至BO都是继承了N层的接口,结果来来回回看好几遍都不清楚层次结构,每层的定位是什么。
问题二:盲目复用和多层复用会导致代码后期维护非常复杂。看下面这个吃牛肉的例子:
/**
* Created by song on 2019/1/5.
*/
// 错误示范
public interface Eat {
// 吃牛肉
public void eatBeaf();
}
// 一般吃法,作为基本实现
abstract class NormalEat implements Eat{
public void eatBeaf() {
// 第一步,做熟
cookBeaf();
// 第二步,吃牛肉
eatTheBeaf();
}
protected void cookBeaf() {
System.out.println("roast beaf");
}
protected void eatTheBeaf() {
System.out.println("eat with hand");
}
}
// 西式吃法
class WestEat extends NormalEat{
@Override
protected void eatTheBeaf() {
System.out.println("eat with knife & folk");
}
}
// 东方吃法
class EestEat extends NormalEat{
@Override
protected void cookBeaf() {
System.out.println("hot pot beaf");
}
@Override
protected void eatTheBeaf() {
System.out.println("eat with chopsticks");
}
}
// 中式吃法
class ChineseEat extends EestEat{
@Override
protected void cookBeaf() {
super.cookBeaf();
// 中式吃法加很多辣椒
System.out.println(" add plenty of chilli");
}
}
// 日式吃法
class JapaneseEat extends EastEat{
@Override
protected void eatTheBeaf() {
talkBeforeEat();
super.eatTheBeaf();
}
protected void talkBeforeEat() {
// 日本人吃之前先说:"我开动了"
System.out.print("いただきます");
}
}
// 日本北海道吃法
class JapaneseBhdEat extends JapaneseEat{
@Override
protected void talkBeforeEat() {
// 北海道人吃之前不说"我开动了"(我胡诌的)
}
}
// 日本北海道札幌市吃法
class JapaneseBhdZhEat extends JapaneseBhdEat{
@Override
protected void cookBeaf() {
super.cookBeaf();
// 煮牛肉火锅加咸鱼
System.out.println("add salt fish");
}
}
例子中讲牛肉的吃法,最基础实现NormalEat中定义了基本实现分两步:做牛肉cookBeaf和吃牛肉eatTheBeaf,到“日本北海道”吃法中,由于复用了父类JapaneseEat的eatTheBeaf和祖父类EastEat的cookBeaf,想知道北海道是怎么吃的,就需要一层层查上去。这是个非常简单的例子,实际应用中的业务场景会复杂的多,层数如果很多,而中间层又有很多差异化的“共性实现”,那么将是一个灾难,这样一层层集成下去,结果就是北海道札幌市鬼子村的松尾桑在做牛肉和吃牛肉之间做了什么,没有人能说清楚,也不知道他有没有说“我开动了”!
那么怎么解决这个问题呢?
1)不能分层太多,最多不能超过3层,比如北海道札幌市鬼子村的松尾桑吃牛肉的方法,就用一个和北海道平级的实现,虽然这一层的实现变多了,但很清晰,最多找两层就清楚所有实现了。
2)一些共用实现方法从模板里拆出来,作为独立完整的小服务,调用出只关心出入口。这样既使代码结构清晰,又使得服务的实现更加稳定且便于维护扩展。
对上例做修改如下:
// 再来一顿,改进版
public interface EatAgain {
// 吃牛肉
public void eatBeaf();
}
// 服务模板抽象
abstract class AbsEatAgain implements EatAgain{
// 做牛肉方法
public abstract CookBeafHandler getCookBeafHandler();
// 吃牛肉方法
public abstract EatTheBeafHandler getEatTheBeafHandler();
public void eatBeaf() {
// 第一步,做熟
getCookBeafHandler().doIt();
// 第二步,吃牛肉
getEatTheBeafHandler().doIt();
}
}
// 做牛肉方法
interface CookBeafHandler {
// 做牛肉
public void doIt();
}
// 吃牛肉方法
interface EatTheBeafHandler {
// 吃牛肉方法
public void doIt();
}
// 一般吃法,作为基本实现
class NormalEat extends AbsEatAgain{
public CookBeafHandler getCookBeafHandler() {
return new BaseCookBeafHandler();
}
public EatTheBeafHandler getEatTheBeafHandler() {
return new BaseEatTheBeafHandler();
}
}
// 西式吃法
class WestEat extends NormalEat{
public EatTheBeafHandler getEatTheBeaf() {
return new KnifeFolkEatTheBeafHandler();
}
}
// 东方吃法
class EestEat extends NormalEat{
public CookBeafHandler getCookBeafHandler() {
return new HotPotCookBeafHandler();
}
public EatTheBeafHandler getEatTheBeafHandler() {
return new EatWithChopsticksHandler();
}
}
// 中式吃法
class ChineseEat extends NormalEat {
public CookBeafHandler getCookBeafHandler() {
return new ChilliCookBeafHandler();
}
}
// 日式吃法
class JapaneseEat extends NormalEat {
public EatTheBeafHandler getEatTheBeafHandler() {
return new TalkAndEatTheBeafHandler();
}
}
// 日本北海道吃法
class JapaneseBhdEat extends NormalEat {
// 可以直接用NormalEat,因为跟基本吃法是一样的
}
// 日本北海道札幌市吃法
class JapaneseBhdZhEat extends NormalEat {
public CookBeafHandler getCookBeafHandler() {
return new SaltFishCookBeafHandler();
}
}
所有的Handler:
// 基本做牛肉方法
class BaseCookBeafHandler implements CookBeafHandler{
public void doIt() {
System.out.println("roast beaf");
}
}
// 基本吃牛肉方法
class BaseEatTheBeafHandler implements EatTheBeafHandler{
// 吃牛肉
public void doIt() {
System.out.println("eat with hand");
}
}
// 吃牛肉方法:刀叉吃法
class KnifeFolkEatTheBeafHandler extends BaseEatTheBeafHandler{
// 吃牛肉方法
public void doIt() {
System.out.println("eat with knife & folk");
}
}
// 做法:火锅牛肉
class HotPotCookBeafHandler extends BaseCookBeafHandler {
public void doId() {
System.out.println("hot pot beaf");
}
}
// 吃牛肉方法:用筷子吃
class EatWithChopsticksHandler extends BaseEatTheBeafHandler {
public void doId() {
System.out.println("eat with chopsticks");
}
}
// 做法:加辣椒的做法
class ChilliCookBeafHandler extends BaseCookBeafHandler {
public void doId() {
super.doIt();
System.out.println("add plenty of chilli");
}
}
// 吃牛肉方法:说完再吃
class TalkAndEatTheBeafHandler extends BaseEatTheBeafHandler {
public void doId() {
System.out.print("いただきます");
super.doIt();
}
}
// 做法:加咸鱼做法
class SaltFishCookBeafHandler extends BaseCookBeafHandler {
public void doId() {
super.doIt();
System.out.println("add salt fish");
}
}
上述代码中,有两项重要调整:一是将模板中的两个重要节点“做牛肉”和“吃牛肉”方法拆出来,建立专门接口和实现,这样做的好处是这些功能点完全独立,可复用性更好,并且作为独立完整的小模块功能可以更强大和稳定;二是不再出现超多层继承,服务的每个实现都很清晰做了哪些事。相对来说,虽然比之前的实现多了很多Handler,并且没有做到“只要能复用就复用”,但层次会清晰很多,扩展和管理更方便。
现在如果北海道札幌市鬼子村的松尾桑想要吃牛肉,那么只需要添加一个SongWeiEat继承AbsEatAgain,并实现继承BaseCookBeafHandler和BaseEatTheBeafHandler的Handler就可以了。