Java 设计模式之模板方法模式

本文深入介绍了模板方法设计模式,探讨了其基本概念、结构及应用场景。通过茶与咖啡冲泡实例,展示了如何利用模板方法模式封装不变部分,扩展可变部分,实现代码复用和维护。同时,引入钩子的概念,使子类能够在不改变算法结构的前提下灵活定制行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、了解模板方法模式

1.1 什么是模板方法模式

模板方法模式 Template Method Parrern)在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义方法中的某些步骤。

模板就是一个方法,更具体的说,这个方法将算法定义为一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。

1.2 模板方法模式组成结构

  • 抽象类(AbstractClass):包含模板方法,定义了算法的骨架。专注于算法本身。
  • 具体类(ConcreteClass):实现抽象类中的抽象方法,完成算法的实现。

1.3 模板方法 UML 图解
这里写图片描述

1.4 模板方法的适用场景

  • 在某些类的算法中,用了相同的方法,造成代码的重复。
  • 控制子类扩展,子类必须遵守算法规则。
二、模板方法模式具体应用

2.1 问题描述

茶与咖啡:有的人喜欢喝茶,而有的人 喜欢喝咖啡。茶与咖啡 之间有什么共同的成分吗?答案是咖啡因 (怎么样,没想到吧)。不止是这样,茶与咖啡的冲泡方式也十分类似。如下

茶冲泡法:

  1. 把水煮沸
  2. 用沸水冲泡咖啡
  3. 把咖啡倒进杯子
  4. 加糖和牛奶

咖啡冲泡法:

  1. 把水煮沸
  2. 用沸水浸泡茶叶
  3. 把茶倒进杯子
  4. 加柠檬

现在我们来设计代码,完成茶与咖啡的冲泡。

2.2 简单设计实现
这里写图片描述

因为代码比较简单,所以这里只提供代码实现的设计图。从上面的设计图中,我们应该能发现一个问题:存在着一些重复的代码 (boilWater() 与 pourInCup())。所以我们要重新考虑一下设计,来避免这个问题。

PS:有一个重要的设计原则:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

于是我们可能想到使用继承,把不变的代码方法超类中,把变化的代码放在子类中去实现。

2.3 使用继承
这里写图片描述

这种通过继承的设计感觉还不错,可以很好的解决问题。但是我们仔细想想,冲泡茶与咖啡是不是还存在一些共同点呢?其实 brewCoffeeGrinds() 与 steepTeaBag() 的动作很类似,因此我们可以使用 brew() 来统一这两个方法;addSugarAndMilk() 与 addLemon() 也很类似,我们用 addCondiments() 来统一。

2.4 模板方法模式登场

(1) 模板方法 UML 设计 (可参考模板方法 UML 图解)
这里写图片描述

(2) 模板方法代码实现

抽象 CaffineBeverage 类

package com.jas.templatemethod;

public abstract class CaffineBeverage {
    /**
     * 模板方法,用作一个算法的模板,用于制作茶与咖啡
     * 并定义为 final 的,防止子类覆盖
     */
    final void prepareRecipe(){
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    void boilWater(){
        System.out.println("煮沸水!");
    }

    abstract void brew();

    void pourInCup(){
        System.out.println("倒进杯子里!");
    }

    abstract void addCondiments();
}

具体 Tea 类

package com.jas.templatemethod;

public class Tea extends CaffineBeverage {
    @Override
    void brew() {
        System.out.println("用沸水浸泡茶叶!");
    }

    @Override
    void addCondiments() {
        System.out.println("加入柠檬!");
    }
}

具体 Coffee 类

package com.jas.templatemethod;

public class Coffee extends CaffineBeverage {
    @Override
    void brew() {
        System.out.println("用沸水浸泡咖啡!");
    }

    @Override
    void addCondiments() {
        System.out.println("加入糖与牛奶!");
    }
}

测试类

package com.jas.templatemethod;

public class Test {
    public static void main(String[] args) {

        CaffineBeverage coffee = new Coffee();
        CaffineBeverage tea = new Tea();

        coffee.prepareRecipe();
        System.out.println();
        tea.prepareRecipe();
    }
}

    /**
     * 输出
     * 煮沸水!
     * 用沸水浸泡咖啡!
     * 倒进杯子里!
     * 加入糖与牛奶!
     *
     * 煮沸水!
     * 用沸水浸泡茶叶!
     * 倒进杯子里!
     * 加入柠檬!
     */

(3) 问题总结

CaffineBeverage 类主导一切,它拥有算法,并保护这个算法 (模板方法定义为 final 的)。对于子类来说,因为 CaffineBeverage 类的存在,代码能够最大程度的复用。并且算法存在于模板方法中,更易于修改,并且扩展性更强。CaffineBeverage 类本身专注与算法本身,由子类提供完整实现。

2.5 对模板方法进行挂钩

(1) 什么是钩子

钩子 (Hook) 是一种被声明抽象类中的方法,但是只有空的或默认的实现。
钩子的存在,可以让子类有能力对算法的不同点进行挂钩。到底要不要挂钩,由子类自行决定。

(2) 将钩子应用到代码中去

改造 CaffineBeverage

package com.jas.templatemethod;

public abstract class CaffineBeverageWithHook {

    final void prepareRecipe(){
        boilWater();
        brew();
        pourInCup();
        /** 这里加上钩子的判断,只有钩子返回值为 true 时,表示顾客想要加调料,才添加调料*/
        if(customerWantsCondiments()){
            addCondiments();
        }else {
            System.out.println("不添加任何调料");
        }
    }

    void boilWater(){
        System.out.println("煮沸水!");
    }

    abstract void brew();

    void pourInCup(){
        System.out.println("倒进杯子里!");
    }

    abstract void addCondiments();

    /**
     * 这是一个钩子,子类可以自行选择覆盖,但是也可以不这么做。
     * @return
     */
    boolean customerWantsCondiments(){
        return true;
    }
}

改造 Coffee

package com.jas.templatemethod;

import java.util.Scanner;

public class CoffeeWithHook  extends CaffineBeverageWithHook {
    @Override
    void brew() {
        System.out.println("用沸水浸泡咖啡!");
    }

    @Override
    void addCondiments() {
        System.out.println("加入糖与牛奶!");
    }

    @Override
    boolean customerWantsCondiments() {
        /** 只有当用户输入的是 'y' 时,返回 true,才添加调料 */
        if("y".equals(getUserInput())){
            return true;
        }else {
            return false;
        }
    }

    /**
     * 获得用户在控制台输入的数据
     * @return
     */
    private String getUserInput(){
        String answer = null;
        System.out.println("你是否想要添加调料,请输入 (y/n)");

        Scanner read = new Scanner(System.in);
        answer = read.nextLine();

        return answer;
    }
}

测试

package com.jas.templatemethod;

public class Test {
    public static void main(String[] args) {

       CaffineBeverageWithHook coffeeWithHook = new CoffeeWithHook();
       coffeeWithHook.prepareRecipe();
    }
    /**
     * 输出
     * 煮沸水!
     * 用沸水浸泡咖啡!
     * 倒进杯子里!
     * 你是否想要添加调料,请输入 (y/n)
     * y
     * 加入糖与牛奶!
     */
}

(3) 钩子总结

在实际工作中,如果算法的这个部分是可选的,那么你可以选择使用钩子。
钩子可以让子类实现算法中可选的部分,或者在钩子对子类的实现并不重要时,子类可以对此钩子置之不理。
钩子可以使子类能够有机会对模板方法中即将发生的步骤做出反应。

三、 模板方法模式总结

3.1 模板方法优缺点总结

优点

  • 封装不变部分,扩展可变部分。
  • 提取公共代码,便于维护。
  • 行为由父类控制,子类实现。

缺点

每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。

3.2 模板方法模式知识点总结

  • 模板方法”定义了算法的实现步骤,并把一些步骤的实现推迟到子类中。
  • 模板方法模式为我们提供了一种代码复用的技巧。
  • 模板方法的抽象类可以定义具体方法、抽象方法和钩子。抽象方法由其子类实现。
  • 钩子是一种方法,它在抽象类中不做事,或者只做一些默认的事,子类可以选择要不要覆盖它。
  • 为了防止子类覆盖模板方法,可以将模板方法定义为 final 的。
  • 可以把工厂方法理解为模板方法的一种特殊版本。

PS:点击了解更多设计模式 http://blog.youkuaiyun.com/codejas/article/details/79236013

参考文献

《Head First 设计模式》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值