深入浅出设计模式之模板模式

模板模式


一、定义

模板方法模式(Template Method Pattern):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

通俗地说,模板方法模式就是一个关于继承的设计模式。

二、以前的模板的模式

当我们去银行的营业厅办理业务需要以下步骤: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);

详情可参考之前总结的文章:https://blog.csdn.net/weixin_45278293/article/details/117471375?spm=1001.2014.3001.5501

/**
 * @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对模板方法进行改造之后,可以进一步的减少代码量,至少可少创建很多实现类,大大的减少重复代码,提升可维护性。

当然,这种做法也不是十全十美的,有一个小小的缺点,那就是理解成本稍微高一点,对于那些对函数式编程不太熟悉的开发者来说, 上手成本稍微高了一些。。。

参考文章地址:
https://blog.csdn.net/hollis_chuang/article/details/119863054?utm_medium=distribute.pc_feed.none-task-blog-yuanlijihua_tag_v1-2.nonecase&depth_1-utm_source=distribute.pc_feed.none-task-blog-yuanlijihua_tag_v1-2.nonecase
https://leetcode-cn.com/leetbook/read/design-patterns/9ena36/


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值