GitHub 21.3k Star 的Java工程师成神之路,不来了解一下吗!

GitHub 21.3k Star 的Java工程师成神之路,真的不来了解一下吗!

我们在日常开发中,经常会遇到类似的场景:当要做一件事儿的时候,这件事儿的步骤是固定好的,但是每一个步骤的具体实现方式是不一定的。

通常,遇到这种情况,我们会把所有要做的事儿抽象到一个抽象类中,并在该类中定义一个模板方法。这就是所谓的模板方法模式。

以前的模板方法

在我之前的一篇《设计模式——模板方法设计模式》文章中举过一个例子:

当我们去银行的营业厅办理业务需要以下步骤: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改造模板方法模式。

这样的做法是我们日常开发中经常会用到的,其实,我觉得本文中的例子并不是完完全全能表达出来我想表达的意思,但是我们的真实业务中的逻辑讲起来又比较复杂。

所以,这就需要大家能够多多理解并且实践一下。如果你代码中用到过模板方法模式,那一定是可以通过本文中的方法进行改造的。

如果你还没用过模板方法模式,那说明你的应用中一定有很多重复代码,那就赶紧用起来。

作为一个开发工程师,我们要尽最大努力的消灭应用中的重复代码,功在当代,利在千秋!

使用Java8改造出来的模板方法真的是yyds的更多相关文章

  1. ArrayList哪种循环效率更好你真的清楚吗

    ArrayList简介 声明:以下内容都是基于jdk1.8的 ArrayList 是一个数组队列,相当于 动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractList,实现了 ...

  2. ArrayList哪种遍历效率最好,你真的弄明白了吗?

    ArrayList简介 声明:以下内容都是基于jdk1.8的 ArrayList 是一个数组队列,相当于 动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractList,实现了 ...

  3. jQuery中的编程范式

    浏览器前端编程的面貌自2005年以来已经发生了深刻的变化,这并不简单的意味着出现了大量功能丰富的基础库,使得我们可以更加方便的编写业务代码,更重要的是我们看待前端技术的观念发生了重大转变,明确意识到了 ...

  4. 从jar包还原出java源码(项目文件)

    原文转载至:https://blog.csdn.net/mxmxz/article/details/73043156 上周接到个新任务,一个遗留的接口工程需要改造,然而根据前任开发留下的文档看,这个工 ...

  5. jQuery 中的编程范式

    浏览器前端编程的面貌自2005年以来已经发生了深刻的变化,这并不简单的意味着出现了大量功能丰富的基础库,使得我们可以更加方便的编写业务代码,更重要的是我们看待前端技术的观念发生了重大转变,明确意识到了 ...

  6. IPv6系列-初学者的10个常见困扰

    本文是<IPv6系列>文章的第二篇<常见困扰>,紧接<入门指南>,用于解答IPv6的10个常见困扰. 小慢哥的原创文章,欢迎转载 目录 ▪ 本文缘由 ▪ 困扰1. ...

  7. 九. Go并发编程--context.Context

    一. 序言 前几篇中提到 等待多个 goroutine 协作的方式可以使用WaitGroup. 但是有一种场景我们无论是使用Mutex, sync/Once,都无法满足. 场景如下 现在有一个 Ser ...

  8. java8实战一------解决冗杂,java8真的很便利(抛砖)

    你的代码很容易因为需求而变化,对自己代码改来改去的你一定会觉得烦的.在我看来,java8很容易的解决了这个问题. 先来看看例子!在一堆苹果里,筛选绿色的苹果.当然,Apple类是这样子. class ...

  9. 有了Java8的“+”真的可以不要StringBuilder了吗

    最近在头条上看到一篇帖子,说Java8开始,字符串拼接时,"+"会被编译成StringBuilder,所以,字符串的连接操作不用再考虑效率问题了,事实真的是这样吗?要搞明白,还是要 ...

随机推荐

  1. Docker进阶:容器卷、DockerFile、Docker网络原理

    1.Docker镜像 1.1镜像是什么 镜像是一种轻量级.可执行的独立软件包,用来打包软件运行环境和机遇运行环境开发的软件. 包含一个软件的所有内容.蒋所有的应用和环境,直接打包为docker镜像,直 ...

  2. npm run start失败&Node.js 查询指定端口运行情况及终止占用端口办法

    缘由: node.js项目中运行npm run start命令脚本报错,No such file or directory 最开始以为是命令脚本找不到所谓的执行路径,但后面发现不是,是package. ...

  3. IP地址详解

    讲之前了解一些网络设备的作用: 交换机:组建局域网 路由器:连接内外网 网关:一个网络的出口(Gate Way = GW)一般网关在路由器上 局域网(也称内网) 一个简单的局域网的基本组成设备:交换机 ...

  4. SpringMVC架构(一)

    SpringMVC架构 1.1Spring web mvc介绍 Spring web mvc和Struts2都属于表现层的框架,它是Spring框架的一部分,我们可以从Spring的整体结构中看得出来 ...

  5. Java多线程系列-基本概念

    Java的线程基本用法 创建线程 创建线程的方法: 实现Runnable接口 首先我们查看Runnable接口的定义: package java.lang; @FunctionalInterface ...

  6. 使用vue-preview报错Cannot read property 'open' of undefined

    最近在做一个vue项目中时,需要使用vue-preview插件制作缩略图,首先在终端使用npm i vue-preview -S指令安装了vue-preview插件,然后在main.js中,导入并引用 ...

  7. 使用mvn命令将pom和jar上传至nexus私服

    要将自定义的jar或者pom上传至nexus私服,需要配置maven的settings文件! 上传至nexus私服配置 1. settings配置 <!-- maven设置私服对应的信息:id. ...

  8. Linux虚拟机扩展磁盘

    Linux虚拟机扩展磁盘 1.虚拟机关机,Vmware中扩展磁盘 2.虚拟机开机,查看磁盘大小 [root@hadoop6 ~]# df -h 文件系统 容量 已用 可用 已用% 挂载点 devtmp ...

  9. synchronized锁代码块(七)

    synchronized同步代码块 用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法执行一个较长时间的任务,那么B线程必须等待比较长的时间.这种情况下可以尝试使用 ...

  10. 2个Double字符串进行

    public static int compare(double d1, double d2) { if (d1 < d2) return -1; // Neither val is NaN, ...