如何使用 Java8 改造模板方法模式!

我们在日常开发中,经常会遇到类似的场景:当要做一件事儿的时候,这件事儿的步骤是固定好的,但是每一个步骤的具体实现方式是不一定的。
通常,遇到这种情况,我们会把所有要做的事儿抽象到一个抽象类中,并在该类中定义一个模板方法。这就是所谓的模板方法模式。

以前的模板方法

举个例子:
当我们去银行的营业厅办理业务需要以下步骤:1.取号、2.办业务、3.评价。
三个步骤中取号和评价都是固定的流程,每个人要做的事儿都是一样的。但是办业务这个步骤根据每个人要办的事情不同所以需要有不同的实现。
我们可以将整个办业务这件事儿封装成一个抽象类:
/**

  • 模板方法设计模式的抽象类

  • @author hollis

*/

public abstract class AbstractBusinessHandler {

/**

 * 模板方法

 */

public final void execute(){

    getNumber();

    handle();

    judge();

}

/**

 * 取号

 * @return

 */

private void getNumber(){

    System.out.println("number-00" + RandomUtils.nextInt());

}

/**

 * 办理业务

 */

public abstract void handle(); //抽象的办理业务方法,由子类实现

/**

 * 评价

 */

private void judge(){

    System.out.println("give a praised");

}

}
我们在类中定义了一个execute类,这个类编排了getNumber、handle和judge三个方法。这就是一个模板方法。
其中getNumber和judge都有通用的实现,只有handle方法是个抽象的,需要子类根据实际要办的业务的内容去重写。
有了这个抽象类和模板方法,当我们想要实现一个"存钱业务"的时候,只需要继承该AbstractBusinessHandeler并且重写handle方法即可:
public class SaveMoneyHandler extends AbstractBusinessHandeler {

@Override

public void handle() {

    System.out.println("save 1000");

}

}
这样,我们在执行存钱的业务逻辑的时候,只需要调用 SaveMoneyHandler的execute方法即可:
public static void main(String []args){

SaveMoneyHandler saveMoneyHandler = new SaveMoneyHandler();

saveMoneyHandler.execute();

}
输出结果:
number-00958442164

save 1000

give a praised
以上,就是一个简单的模板方法的实现。通过使用模板方法,可以帮助我们很大程度的复用代码。
因为我们要在银行办理很多业务,所以可能需要定义很多的实现类:
//取钱业务的实现类

public class DrawMoneyHandler extends AbstractBusinessHandeler {

@Override

public void handle() {

    System.out.println("draw 1000");

}

}

//理财业务的实现类

public class MoneyManageHandler extends AbstractBusinessHandeler{

@Override

public void handle() {

    System.out.println("money manage");

}

}
一直以来,开发者们在使用模板方法的时候基本都是像上面这个例子一样:需要准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来让子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。
但是,有了Java 8以后,模板方法有了另外一种实现方式,不需要定义特别多的实现类了。

Java 8 的函数式编程

2014年,Oracle发布了 Java 8,在Java 8中最大的新特性就是提供了对函数式编程的支持。
Java 8在java.util.function下面增加增加一系列的函数接口。其中主要有Consumer、Supplier、Predicate、Function等。
本文主要想要介绍一下Supplier和Consumer这两个,使用者两个接口,可以帮我们很好的改造模板方法。这里只是简单介绍下他们的用法,并不会深入展开,如果大家想要学习更多用法,可以自行google一下。

Supplier

Supplier是一个供给型的接口,简单点说,这就是一个返回某些值的方法。
最简单的一个Supplier就是下面这段代码:
public List<String> getList() {

return new ArrayList();

}

使用Supplier表示就是:
Supplier<List<String>> listSupplier = ArrayList::new;

Consumer

Consumer 接口消费型接口,简单点说,这就是一个使用某些值(如方法参数)并对其进行操作的方法。
最简单的一个Consumer就是下面这段代码:
public void sum(String a1) {

System.out.println(a1);

}

使用Consumer表示就是:
Consumer<String> printConsumer = a1 -> System.out.println(a1);

Consumer的用法,最见的的例子就是是Stream.forEach(Consumer)这样的用法,
它接受一个Consumer,该Consumer消费正在迭代的流中的元素,并对每个元素执行一些操作,比如打印:
Consumer<String> stringConsumer = (s) -> System.out.println(s.length());

Arrays.asList("ab", "abc", "a", "abcd").stream().forEach(stringConsumer);

Java 8以后的模板方法

在介绍过了Java 8中的Consumer、Supplier之后,我们来看下怎么改造之前我们介绍过的模板方法。
首先,我们定义一个BankBusinessHandler类,并且重新定义一个execute方法,这个方法有一个入参,是Consumer类型的,然后移除handle方法,重新编排后的模板方法内容如下:
/**

  • @author Hollis

*/

public class BankBusinessHandler {

private void execute(Consumer<BigDecimal> consumer) {

    getNumber();

    consumer.accept(null);

    judge();

}

private void getNumber() {

    System.out.println("number-00" + RandomUtils.nextInt());

}

private void judge() {

    System.out.println("give a praised");

}

}

我们实现的模板方法execute中,编排了getNumber、judge以及consumer.accept,这里面consumer.accept就是具体的业务逻辑,可能是存钱、取钱、理财等。需要由其他方法调用execute的时候传入。
这时候,我们想要实现"存钱"业务的时候,需要BankBusinessHandler类中增加以下方法:
/**

  • @author Hollis

*/

public class BankBusinessHandler {

public void save(BigDecimal amount) {

    execute(a -> System.out.println("save " + amount));

}

}

在save方法中,调用execute方法,并且在入参处传入一个实现了"存钱"的业务逻辑的Comsumer。
这样,我们在执行存钱的业务逻辑的时候,只需要调用 BankBusinessHandler的save方法即可:
public static void main(String[] args) throws {

BankBusinessHandler businessHandler = new BankBusinessHandler();

businessHandler.save(new BigDecimal("1000"));

}

输出结果:
number-001736151440

save1000

give a praised

如上,当我们想要实现取钱、理财等业务逻辑的时候,和存钱类似:
/**

  • @author Hollis

*/

public class BankBusinessHandler {

public void save(BigDecimal amount) {

    execute(a -> System.out.println("save " + amount));

}

public void draw(BigDecimal amount) {

    execute(a -> System.out.println("draw " + amount));

}

public void moneyManage(BigDecimal amount) {

    execute(a -> System.out.println("draw " + amount));

}

}

可以看到,通过使用Java 8中的Comsumer,我们把模板方法改造了,改造之后不再需要抽象类、抽象方法,也不再需要为每一个业务都创建一个实现类了。我们可以把所有的业务逻辑内聚在同一个业务类中。这样非常方便这段代码的后期运维。
前面介绍如何使用Consumer进行改造模板方法,那么Supplier有什么用呢?
我们的例子中,在取号、办业务、评价这三个步骤中,办业务是需要根据业务情况进行定制的,所以,我们在模板方法中,把办业务这个作为扩展点开放给外部。
有这样一种情况,那就是现在我们办业务的时候,取号的方式也不一样,可能是到银行网点取号、在网上取号或者银行客户经理预约的无需取号等。
无论取号的方式如何,最终结果都是取一个号;而取到的号的种类不同,可能接收到的具体服务也不同,比如vip号会到VIP柜台办理业务等。
想要实现这样的业务逻辑,就需要使用到Supplier,Supplier是一个"供给者",他可以用来定制"取号逻辑"。
首先,我们需要改造下模板方法:
/**

  • 模板方法

*/

protected void execute(Supplier<String> supplier, Consumer<BigDecimal> consumer) {

String number = supplier.get();

System.out.println(number);


if (number.startsWith("vip")) {

    //Vip号分配到VIP柜台

    System.out.println("Assign To Vip Counter");

}

else if (number.startsWith("reservation")) {

    //预约号分配到专属客户经理

    System.out.println("Assign To Exclusive Customer Manager");

}else{

    //默认分配到普通柜台

    System.out.println("Assign To Usual Manager");

}

consumer.accept(null);

judge();

}

经过改造,execute的入参增加了一个supplier,这个supplier可以提供一个号码。至于如何取号的,交给调用execute的方法来执行。
之后,我们可以定义多个存钱方法,分别是Vip存钱、预约存钱和普通存钱:
public class BankBusinessHandler extends AbstractBusinessHandler {

public void saveVip(BigDecimal amount) {

    execute(() -> "vipNumber-00" + RandomUtils.nextInt(), a -> System.out.println("save " + amount));

}

public void save(BigDecimal amount) {

    execute(() -> "number-00" + RandomUtils.nextInt(), a -> System.out.println("save " + amount));

}

public void saveReservation(BigDecimal amount) {

    execute(() -> "reservationNumber-00" + RandomUtils.nextInt(), a -> System.out.println("save " + amount));

}

}

在多个不同的存钱方法中,实现不同的取号逻辑,把取号逻辑封装在supplier中,然后传入execute方法即可。
测试代码如下:
BankBusinessHandler businessHandler = new BankBusinessHandler();

businessHandler.saveVip(new BigDecimal("1000"));

输出结果:
vipNumber-001638110566

Assign To Vip Counter

save 1000

give a praised

以上,我们就是用Comsumer和Supplier改造了模板方法模式。
使用Java 8对模板方法进行改造之后,可以进一步的减少代码量,至少可少创建很多实现类,大大的减少重复代码,提升可维护性。
当然,这种做法也不是十全十美的,有一个小小的缺点,那就是理解成本稍微高一点,对于那些对函数式编程不太熟悉的开发者来说, 上手成本稍微高了一些。。。

总结

以上,我们介绍了什么是模板方法模式,以及如何使用Comsumer和Supplier改造模板方法模式。
这样的做法是我们日常开发中经常会用到的,其实,我觉得本文中的例子并不是完完全全能表达出来我想表达的意思,但是我们的真实业务中的逻辑讲起来又比较复杂。
所以,这就需要大家能够多多理解并且实践一下。如果你代码中用到过模板方法模式,那一定是可以通过本文中的方法进行改造的。
如果你还没用过模板方法模式,那说明你的应用中一定有很多重复代码,那就赶紧用起来。
作为一个开发工程师,我们要尽最大努力的消灭应用中的重复代码,功在当代,利在千秋!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容