1. 使用场景

模版方法模式的两个使用场景:

  • 设计通用函数。功能抽象的重要内容。
  • 代码向上集中。用于在各种子类已有代码的基础上,通过重构,提取公共的行为并集中到父类中以免代码重复。

2. 求和与”传统”模板模式

求和为例,我们可能有下面这些需求。

  1. 求[a,b]之间自然数的和;
  2. 求[a,b]之间自然数的立方的和
  3. 求调和级数前n项的和,其中调和级数为H(n)= 1/1 + 1/2 + 1/3 + 1/4 + … + 1/n
  4. 求Pi,其中pi/8 = 1/(13)+1/(57)+1/(9*11)+…
  5. ……

通过分析,将求和分为以下两部分。

  1. 抽象,提取出的统一部分。统一形参部分,当参数个数不同时,设计的通用函数时需要取最多个数的参数;不同求和函数的返回值类型不同时,则取更大的数据类型如double,作为通用函数的返回值类型…
  2. 变化,需要参数化的部分。各种求和函数的关键不同、可变化部分有2处——步进和累加项。将两个可变的部分设计为抽象方法 next() 和 item()。

变化部分的抽象方法如何封装,GoF 选择“将它的一些步骤延迟到子类中”(让子类继承,然后强制实现),可以称之为传统模版方法模式
该模式的缺点在于两个抽象方法next() 和 item()的组合会造成子类数量爆炸

  1. //抽象类实现求和模板方法
  2. public abstract class Sum {
  3. public final double getSum(double a,double b){
  4. double sum=0;
  5. for(;a<=b;a=next(a)){
  6. sum+=item(a);
  7. }
  8. return sum;
  9. }
  10. public abstract double next(double a);
  11. public abstract double item(double a);
  12. }
  13. //抽象类实现求和模板方法
  14. public interface Sum1 {
  15. default double getSum(double a,double b){
  16. double sum=0;
  17. for(;a<=b;a=next(a)){
  18. sum+=item(a);
  19. }
  20. return sum;
  21. }
  22. double next(double a);
  23. double item(double a);
  24. }
  25. //调和级数
  26. //H(n)= 1/1 + 1/2 + 1/3 + 1/4 + ... + 1/n
  27. public class Harmonic extends Sum {
  28. @Override
  29. public double next(double i) {
  30. return i + 1;
  31. }
  32. @Override
  33. public double item(double x) {
  34. return 1.0 / x;
  35. }
  36. }

3. 策略模式 VS 模板模式

为避免子类爆炸,尤其是考虑到 item(double x) 的实现有无限可能,程序员通常应该将 模板方法所依赖的抽象方法从Sum中分离出去(行为参数化)。
重构 Sum 作为环境类而二次使用策略模式,而 IItem 、 INext 可以作为两个成员变量,也可作为模板方法的参数(提供4参数的模板方法),此时 Sum 返璞归真为 final 工具类

  1. public interface IItem{
  2. double item(double a);
  3. }
  4. public interface INext{
  5. double next(double a);
  6. }
  7. public final class Sum{
  8. public static double getSum(double a, double b, INext iNext, IItem iItem) {
  9. double sum = 0;
  10. for (; a <= b; a = iNext.next(a)) {
  11. sum += iItem.item(a);
  12. }
  13. return sum;
  14. }
  15. }

这一有4参数的模板方法说明,模板方法代码中的变化部分存在多种设计选项:

  • 可以延迟到子类中,由子类继承实现
  • 作为模板方法的参数

采用了多重策略的模板模式可以定义为:模板方法定义一个算法的骨架。它的一些可变部分可以延迟到子类中,或采用多重策略(行为参数化),这些策略彼此独立

4. 可变步骤如何设计

首先,要了解子类对待父类函数的5种形式/态度

  • 直接继承。如父类的final方法、不准备被子类override的其他方法。
  • 改进语意的改写。子类的改写方法中必须调用父类相同方法。
  • 替换语意的改写。完全以新的实现替换父类的实现,体现了对父类函数的代码的漠视。
  • 空方法的改写。没有代码复用的诱惑,一种特定情况下使用的设计技巧。
  • 接口继承(抽象方法)。对抽象方法的继承,无代码可以复用。

对象抽象的代码向上集中原则,本身意味着对父类代码的复用。
这5种形式中,仅前2种复用了父类代码,前3种父类都提供了具体代码,或是final类,或是。
父类在设计模板模式中的可变步骤时,通常有3种设计选择:

  • 抽象方法
  • 空方法
  • 具体方法(含接口中的default方法)

    4.1 抽象方法

    求和函数中使用的 item(double x) ,就是一种最典型的设计选择——抽象方法。可变/不稳定的部分定义为抽象方法(C++中为纯虚函数)是首选,子类必须改写它们

    4.2 空方法

    空方法指方法体为空或者返回固定值的方法。空方法可以称为可选方法/操作(optional operation),是一种特定情况下使用的设计技巧,不会强迫(具体的)子类必须改写该方法。

    4.3 具体方法

    父类提供具体方法时,一定要警惕实现继承的缺陷。
    考虑求和函数使用的 next(double x) ,在循环计算中步进为一的 i++ 最为常见,能否在设计 INext 时充分强调 i++ 这种应用呢?将 i++ 设计成一个具体的方法( default 方法),更进一步,将 i++ 设计为静态成员 ONE 也是一种设计选择。
    但是,这种方案使得INext成为普通接口,用户需要INext的其他实现时,只能够采用匿名类而不能够使用简洁的lambda表达式。
    实际上,充分重视 i++ ,完全仍然采用抽象方法,而在 Sum 工具类中,为步进为一的情况,添加一个重载的三参数的版本,即getSum(double a, double b, IItem iItem)。

4.4 改进语意的改写

父类的设计者应该明白,作为设计者,他不能够依靠其他程序员(编写子类型的人)的自律。
父类设计者可以强制子类必须采用改进语意的改写,手段是将 m3() 重构为一个小的、private的模板方法 _m3() 。

  1. public abstract class Sup{
  2. //模板方法
  3. private void _m3(){
  4. //原来的m3()的代码。避免子类忘记
  5. pln("Sup._m3(),base");
  6. //假定子类的扩展代码将放在原来的m3()之后
  7. this.m3();
  8. }
  9. public abstract void m3(); //强制子类复写,GoF所称的钩子
  10. public void m3(){}; //可选
  11. public final void templateMethod(){
  12. _m3() ;
  13. }
  14. }