designPattern

Design Pattern(14) - Template

以下文章是閱讀 深入淺出Design Pattern 還有 聖經還有Source making的筆記 圖片截圖自lynda.com的Foundations of Programming: Design Patterns 要更深入的理解一定要去看這兩本書

來點咖啡因

上班日的下午 最需要的就是咖啡因來提神 那基本上咖啡因來源有二 茶或咖啡

泡茶的步驟如下: 1.把水煮開 2.沸水倒進茶葉 3.茶倒進杯子 4.加檸檬

泡咖啡的步驟如下: 1.把水煮開 2.用沸水沖咖啡 3.咖啡倒進杯子 4.加糖和牛奶

public class Tea {
   void prepareRecipe() {
    boilWater();
    steepTeaBag();
    pourInCup();
    addLemon();
  }
 
  public void boilWater() {
    System.out.println("Boiling water");
  }
 
  public void steepTeaBag() {
    System.out.println("Steeping the tea");
  }
  
  public void pourInCup() {
    System.out.println("Pouring into cup");
  }

  public void addLemon() {
    System.out.println("Adding Lemon");
  }
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 through filter");
  }
 
  public void pourInCup() {
    System.out.println("Pouring into cup");
  }
 
  public void addSugarAndMilk() {
    System.out.println("Adding Sugar and Milk");
  }
}

那在Coffee bar的人做起事來就簡單了 只要call prepaqreRecipe就搞定

public class Bartender {
 
  public static void main(String[] args) {
    Tea tea = new Tea();
    Coffee coffee = new Coffee();
    System.out.println("Making tea...");
    tea.prepareRecipe();
    System.out.println("Making coffee...");
    coffee.prepareRecipe();
  }
}

Output結果

Making tea...
Boiling water
Steeping the tea
Pouring into cup
Adding Lemon
Making coffee...
Boiling water
Dripping Coffee through filter
Pouring into cup
Adding Sugar and Milk

重複的程式碼太多 失敗 應該要把共同的部分提出來

這裡的共同部分就是步驟1和步驟3

想當然爾 步驟1跟步驟3是鐵定要放base class的 步驟2跟4就要把它命名得抽象一點 讓子class去實作細節

public abstract class CaffeineBeverage {
  final void prepareRecipe() {
    boilWater();
    brew();
    pourInCup();
    addCondiments();
  }

  abstract void brew();
  //步驟2 steepTeaBag, brewCoffeeGrinds
  
  abstract void addCondiments();
  //步驟4 addLemon addSugarAndMilk
 
  void boilWater() {//步驟1
    System.out.println("Boiling water");
  }
  
  void pourInCup() {//步驟3
    System.out.println("Pouring into cup");
  }
}

注意 CaffeineBeverage 是抽象類別 因為這個類別裡面有抽象方法

prepareRecipe是final因為我們不希望子類別改變這個template

剩下的就容易了

public class Tea extends CaffeineBeverage {
  public void brew() {
    System.out.println("Steeping the tea");
  }
  public void addCondiments() {
    System.out.println("Adding Lemon");
  }
}
public class Coffee extends CaffeineBeverage {
  public void brew() {
    System.out.println("Dripping Coffee through filter");
  }
  public void addCondiments() {
    System.out.println("Adding Sugar and Milk");
  }
}

prepareRecipe是我們的樣板方法(Template method)

為什麼呢? 因為他 1.是一個方法(method) 2.是一個樣板(algorithm template)

想必經過我精闢的解說之後大家都知道為什麼他是樣板方法

Alt text

樣板方法定義了一個演算法的步驟 並允許次類別為一個或多個步驟提供實踐

樣板方法模式

將一個演算法的骨架定義在一個方法中 而演算法本身用到的方法則定義在次類別中 樣板方法讓次類別在不改變演算法骨架的前提下 重新定義演算法中的某些步驟

樣板方法模式結構

Alt text

優缺點

1.由父類別主導演算法 而不是各個subclass各自maintain

2.程式碼再利用

3.演算法只存在一個地方

掛鉤

其實除了咖啡跟茶以外 我還蠻常喝牛奶的 可是牛奶的步驟如下

1.把水煮開 2.沸水倒進奶粉 3.牛奶倒進杯子

他不需要加配料 怎麼辦 我們的template method有4步 可是牛奶跟他非常相似但只有3步 好痛苦啊 我不想要有重複的code 該怎麼辦呢 Alt text

簡單 多給父類別一個變數 來作為演算法的if else判斷

既然都說是演算法了 裡面有個if else也是合情合理

public abstract class CaffeineBeverageWithHook {

  final void prepareRecipe() {
    boilWater();
    brew();
    pourInCup();
    if (wantsCondiments()) {
      addCondiments();
    }
  }

  abstract void brew();

  abstract void addCondiments();

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

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

  boolean wantsCondiments() {
    return true;
  }
}

在這裡 wantsCondiments就是我們的掛勾方法(Hook) 他有著一個default的implementation 每個subclass自行決定要不要override它 然後牛奶裡面

public class MilkWithHook extends CaffeineBeverageWithHook {

  public void brew() {
    System.out.println("泡牛奶");
  }

  public void addCondiments() {

  }

  public boolean wantsCondiments() {
    return false;
  }

}

牛奶override父類別的wantsCondiments函式讓它return false就搞定

Q: 奇怪你這不是多此一舉 你的牛奶直接implement addCondiments讓他不做任何事不就好了嗎

public void addCondiments() {}

A: 那是剛好現在這個例子裡 addCondiments函數是抽象函數(父類別希望你實作的函數) 如果今天去if block裡面的是boilWater或是pourInCup 那就不能像你那樣搞

Q: 在你牛奶的case 你又override抽象函數 又override掛鉤 那我身為抽象父類別 到底該什麼時候用抽象函數 什麼時候用掛勾呢

A: 當你希望子類別一定要實作的函數 就用抽象函數 可以選擇要不要實作的函數 就用掛鉤

融會貫通

又到了開心的融會貫通時間 以下的內容建議你對於Strategy pattern非常了解再往下讀

建議你再把Strategy pattern讀過一遍再往下看 否則走火入魔後果自行負責

Alt text

定義演算法家族 並把每個演算法封裝起來 演算法之間彼此可以互換

今天我只要performFly我就會call flyBehavior.fly()

public void performFly() {
        flyBehavior.fly();
}

如果我的flyBehavior是FlyWithWing的instance我就可以飛 如果我的flyBehavior是FlyNoWay的instance我就可以飛 如果我的flyBehavior是FlyRocketPowered的instance我就飛得像火箭一樣

複習結束 有沒有覺得Strategy長得跟Template method很像

Alt text

我看我的subclass決定我要怎麼做事情 如果我的flyBehavior這個member variable是FlyWithWing我就可以飛 就如同我的CaffeineBeverage如果是Coffee 我的addCondiments就是加糖和奶精

差別差在flyBehavior是Duck的一個member variable 所以是合成 但Coffee是CaffeineBeverage的子class 所以是繼承

所以Strategy讓我們可以在run time再決定要怎麼飛 但Template method會在compile time就決定怎麼addCondiments

請努力讀懂上面這一part 這關係到五分鐘後你能不能悟道

讀懂了 恭喜你 你再度進入了見山不是山 見水不是水的境界

Alt text

上面那個論述是錯的 我舉的addCondiment例子讓人覺得好像Compile time就決定行為 但其實你要讓template method在run time決定行為也可以

  public boolean customerWantsCondiments() {

    String answer = getUserInput();

    if (answer.toLowerCase().startsWith("y")) {
      return true;
    } else {
      return false;
    }
  }

這樣的話他就會run-time等使用者的答案來決定要不要加調味

那到底真正的差別是什麼呢

Strategy讓你能夠在run time對於同一個function選擇完全不同的策略(演算法) 不同的策略之間彼此獨立不相關

Template method預先定義好了演算法的每個步驟 某些步驟是固定的 某些步驟是彈性的 彈性的步驟交給subclass去實作 Alt text

總結

演算法中固定不變的部分設計為樣板方法和父類具體方法 可改變的細節讓子類實現