设计模式之模板方法

DesignPattern-Template


1. Definition

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

这个模式是用来==创建一个算法的模板==。什么是模板?模板就是一个不变的(final)方法。更具体来说,这个方法将算法定义成==一组步骤==,其中的步骤都可以是抽象的,由子类负责具体实现。这样可以确保算法的结构(骨架)保持不变,同时由子类提供部分实现。

/* 这就是用来定义模板方法的类,被声明为anstract,用来作为基类,
 * 它的子类必须实现其中的abstract方法
 */
public abstract class TemplateClass {


    /* 这就是模板方法
     * 被声明为final,防止子类重写改变这个算法的结构、骨架
     */
    final void templateMethod() {

        // 模板方法里定义了一连串的步骤,相当于定义了算法的骨架
        // 每个步骤由一个方法代表
        primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
        hook;
    }

    // 下面是两个抽象方法,子类必须去实现这两个方法
    // 不同子类对这两个方法有不同的实现(重写)
    // --> 算法的整体骨架不变,
    // 但是其中某些步骤的细节因不同子类的对抽象方法的不同实现而有所不同
    // 其实就是模板方法中变的部分
    //(变的意思是:不同的子类对某些步骤由它不同的实现方式)

    abstract void primitiveOperation1();

    abstract void primitiveOperation2();

    // 这个具体的方法被定义在抽象类中,将它声明为final,
    // 这样一来子类就无法覆盖(重写)它
    // 它可以被模板方法直接使用,或者被子类使用
    // 其实就是模板算法中不变的部分(即任何子类对这部分执行都一样,无需重写),
    // 所以直接放在基类中
    final void concreteOperation() {
        // implementation
    }

    // 这是一个Hook(钩子)方法
    // 子类可以视情况决定要不要覆盖它
    void hook() {
        //blablabla
    }
}



例子

要创建咖啡和茶

  • 咖啡
    public class Coffee {

        void prepareRecipe() {
            boilWater();
            brewCoffeeGrinds();
            pourInCup();
            addSugarAndMilk();
        }


        public void boilWater() {
            System.out.println("Boiling water");
        }

        public void brewCoffeeGrinds() {    
            System.out.println("Dripping Coffee throught filter");
        }

        public void pourInCup() {
            System.out.println("Pouring into Cup");
        }

        public void addSugarAndMilk() {
            System.out.println("Adding sugar and milk");
        }
    }
    public class Tea {

        void prepareRecipe() {
            boilWater();
            steepTeaBag();
            pourInCup();
            addLemaon();
        }


        public void boilWater() {
            System.out.println("Boiling water");
        }

        public void stepTeaBag() {    
            System.out.println("Steeping the tea");
        }

        public void pourInCup() {
            System.out.println("Pouring into Cup");
        }

        public void addLemon() {
            System.out.println("Adding Lemon");
        }

    }



不难看出两个类中boilWaterpourInCup两个方法的代码重复了,如何避免重复?

第一版改进:

  • 让Coffee和Tea继承于一个父类CaffeineBeverage

  • boilWaterpourInCup被两个子类所共享,所以把它们放在父类中

  • 由于prepareRecipe在每个子类中都不一样,所以在父类里可以定义它为抽象方法,由每个子类重写,从而实现每个子类有不同的prepareRecipe


但是抽象得还不够,可以把prepareRecipe也抽象。仔细对比Coffee和Tea的冲泡步骤,不难发现两个都采用了相同的算法骨架:

  1. 把水煮沸
  2. 用热水泡咖啡或茶
  3. 把饮料倒进杯子
  4. 在饮料内加入适当的调料
冲泡步骤CoffeeTea
1.boilWater()boilWater()
2.==brewCoffeeGrinds()====steepTeaBag()==
3.pourInCup()pourInCup()
4.==addSugarAndMilk()====addLemon()==
  • steep(浸泡)和brea(冲泡)其实差异不大 –> ==可以抽象为 brew()==

  • 加糖和加牛奶也很像,都是在饮料中加入调料 –> ==可以抽象为 addCondiments()==

–> 那么现在可以把prepareRecipe方法抽象成:

void prepareRecipe() {
    boilWater();
    brew();
    pourInCup();
    addCondiments();
}


第二版改进

  • 父类

public abstract class CaffeinieBeverage {

    /* prepareRecipe 就是我们的模板方法:
     *  1. 首先它是一个方法
     *  2. 它用作一个算法的模板,在这个例子中,它定义了制作茶和咖啡的template
     */ 
    public final void prepareRecipe() {

        // 1. 在模板内,每一个步骤都由一个方法代表
        // 2. 某些共同的方法(不变的部分,在这里是boilWater和pourInCup)
        //    由父类去实现
        // 3. 某些方法(变的部分)由不同的子类不通地实现  

        this.boilWater();
        this.brew();
        this.pourInCup();
        if (customerWantsCondiment()) {
            this.addCondiments();
        }
    }

    // 需要子类提供实现的方法,声明为abstract
    public abstract void brew();

    public abstract void addCondiments();

    private void boilWater() {
        System.out.println("Boiling water");
    }

    private void pourInCup() {
        System.out.println("pouring into cup");
    }    
}
  • Tea
public class Tea extends CaffeinieBeverage {

    @Override
    public void brew() {
        System.out.println("Steeping the tea");
    }

    @Override
    public void addCondiments() {
        System.out.println("Adding lemon");
    }
  • Coffee

public class Coffee extends CaffeinieBeverage {

    @Override
    public void brew() {
        System.out.println("Dripping coffee through filter");
    }

    @Override
    public void addCondiments() {
        System.out.println("Adding sugar and milk");
    }   
}



对比使用Template和不使用Template模式

不使用使用
Coffee和Tea主导了一切,它们控制了算法由CaffeinieBeverage类主导一切,它拥有并保护这个算法
Coffee和Tea之间有重复代码CaffeinieBeverage的存在使代码复用最大化
因为prepareRecipe的实现在子类中,所以对于算法所做的代码改变,需要打开许多子类来修改许多地方==算法只存在于一个地方,所以容易修改==
由于类的组织不具有弹性,所以加入新种类的咖啡因饮料需要做许多工作template模式提供了一个框架,可以让其他的咖啡因饮料加进来,只需要继承父类并实现其中的抽象方法即可




使用Hook

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

例:

在上面的第二版改进中假如钩子:询问顾客是否需要加调料,如果是true才添加

public abstract class CaffeinieBeverage {

    public final void prepareRecipe() {
        this.boilWater();
        this.brew();
        this.pourInCup();
        if (customerWantsCondiment()) {
            this.addCondiments();
        }
    }

    public abstract void brew();

    public abstract void addCondiments();

    private void boilWater() {
        System.out.println("Boiling water");
    }

    private void pourInCup() {
        System.out.println("pouring into cup");
    }

    // hook method
    // 通常是空的缺省实现
    // 在这里这个方法只返回true,不干别的事
    // 子类可以覆盖这个方法(因为没有声明为final),但不是一定要这样做
    public boolean customerWantsCondiment() {
        return true;
    }
}

为了使用钩子,在Coffee这个子类中覆盖它。

public class Coffee extends CaffeinieBeverage {

    @Override
    public void brew() {
        System.out.println("Dripping coffee through filter");
    }

    @Override
    public void addCondiments() {
        System.out.println("Adding sugar and milk");
    }


    // Coffee子类覆盖(重写)了这个钩子方法,提供了自己的功能 
    @Override
    public boolean customerWantsCondiment() {

        String answer = getUserInput();

        if (answer.equalsIgnoreCase("yes")) {
            return true;
        }

        return false;
    }

    private String getUserInput() {

        String answer = null;

        System.out.println("Would you like some milk an sugar 
            with your coffee (y/n) ? ");

        BufferedReader reader = new BufferedReader(new 
            InputStreamReader(System.in));

        try {
            answer = reader.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (answer == null) {
            return "no";
        }
        return answer;
    }
}




注意

1. 何时使用抽象方法,何时使用钩子?

  • 当子类必须提供算法中某个方法或步骤的实现时,使用==抽象方法==

  • 如果算法的这个部分是可选的,就使用==钩子==(子类可以选择实现这个钩子,但并不强制这样做)

2. 使用钩子的真正目的?

  • 可以让子类实现算法中可选的部分,或者在钩子对于子类的实现并不重要的时候,子类可以对这个钩子置之不理。

  • 让子类能有机会对模板方法中某些即将发生或者刚刚发生的步骤做出反应

3. 保持抽象方法的数目越少越好?

原则来说是这样的。在使用Template模式的时候要随时记得这一点。

要做到这一点,可以让算法内的步骤不要切割得太细;但是步骤太少的话,会比较没有弹性。==所以要看情况折中。==

也千万不要忘记钩子!!!当某些步骤是可选的,可以把这些步骤实现成钩子,而不是设计成抽象方法,这样可以减轻子类的负荷。




好莱坞原则

别调用我们,我们会调用你(Don’t call us, we call you!!!)

好莱坞原则可以给我们一种防止“依赖腐败“的方法。所为依赖腐败,是指:高层组件依赖底层组件,而底层组件又依赖高层组件,而高层组件又依赖边侧组件,边侧组件又依赖底层组件(依赖循环…)。

在好莱坞原则之下,==我们允许底层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些底层组件。== 决策权放在高层模块中,以便高层模块决定如何以及何时调用低层模块。

换句话说,高层组件对待底层组件的方式是:”==别调用我们,我们会调用你!==


好莱坞原则和模板方法

  • CaffeineBeverage使我们的高层组件,能够控制制作饮料的算法,只有在需要子类实现某个方法时才调用子类。

  • 客户代码只依赖CaffeineBeverage,而不依赖具体的Coffee或Tee,这样可以减少整个系统的依赖

  • Tee和Coffee两个子类仅仅用来提供一些实现细节;如果没有先被调用,绝对不会直接调用抽象类(底层组件不直接调用高层组件)


好莱坞原则和依赖倒置原则(DIP)

  • DIP:==尽量避免使用具体类,而多用抽象==

  • 好莱坞原则是用在创建框架或组件上的一种技巧,好让==底层组件能被挂钩进计算中,而且又不会让高层组件依赖底层组件。==

两者的目标都在于 ==解耦==, 但是依赖导致原则更加注重如何在设计中避免依赖。




模板方法与策略模式

模板方法(Template)策略模式(Strategy)
在父类中定义一个==算法的大纲==,由子类定义其中某些步骤的内容。这样依一来,算法的个别步骤可以有不同的实现细节,但是算法的结构依然保持不变定义一个算法家族,并让这些算法可以互换。正因为每一个算法都被封装起来,所以客户可以轻易的使用不同的算法
==代码复用率高==:会重复使用的代码,都被放进了父类,好让所有子类共享使用==对象组合==,更有弹性。客户可以在运行时改变算法,需要做的只是改用不同的策略对象而已
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值