今天不學習,明天變辣雞
定義
模板方法模式定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
結構圖及含義
類圖符號意義
+:表示public-
-:表示privat
AbstractClass(抽象類):其實也就是一抽象模板,定義並實現了一個模板方法( templateMethod() )。這個模板方法一般是一個具體方法,它給出了一個頂級邏輯的骨架,而邏輯的組成步驟在相應的抽象操作中,推遲到子類實現。頂級邏輯也有可能調用一些具體方法。
ConcreteClass(子類),實現父類所定義的一個或多個抽象方法。每一個AbstractClass都可以有任意多個ConcreteClass與之對應,而每一個ConcreteClass都可以給出這些抽象方法(也就是頂級邏輯的組成步驟)的不同實現,從而使得頂級邏輯的實現各不相同。
特點:模板方法模式是通過把不變行爲搬移到超類,去除子類中的重複代碼來體現它的優勢。模板方法模式就是提供了一個很好的代碼複用平臺,當不變的和可變的行爲在方法的子類實現中混合在一起的時候,不變的行爲就會在子類中重複出現。我們通過模板方法模式把這些行爲搬移到單一的地方,這樣就幫助子類擺脫重複的不變行爲的糾纏。
先舉一個簡單的例子
創建一個抽象模板結構(AblstractClass)
/**
*@description 模板方法
*@author Saiuna
*@date 2020/7/6 1:25
*/
public abstract class AbstractClass {
//模板方法用來控制子類的順序 , 聲明final不讓子類覆蓋這個方法,防止改變執行順序
public final void human(){
sport();
learn();
life();
}
void sport(){
System.out.println("我要跑步來鍛鍊身體");
}
void learn(){
System.out.println("九年義務教育");
}
//生活就像海洋
abstract void life();
}
具體模板(ConcreteClass)
/**
*@description
*@author Saiuna
*@date 2020/7/6 1:27
*/
public class ConcreteClass extends AbstractClass {
@Override
public void sport() {
System.out.println("我通過打籃球來鍛鍊身體");
}
//生活就像海洋
@Override
public void life() {
System.out.println("生活就像海洋,只有意志堅強的人才能到達彼岸");
}
public static void main(String[] args) {
new ConcreteClass().human();
}
}
輸出
我通過打籃球來鍛鍊身體
九年義務教育
生活就像海洋,只有意志堅強的人才能到達彼岸
以上是模板方法模式的一個最基本應用,下面再深入一點,介紹一下“鉤子”
鉤子
從代碼層面展示抽象類的定義
abstract class AbstractClass {
// 這就是模板方法。它被聲明爲final,以免子類改變這個算法的順序
final void templateMethod() {
// 模板方法定義了一連串的步驟,每個步驟由一個方法代表
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
abstract void primitiveOperation1();
abstract void primitiveOperation2();
final void concreteOperation() {
// 這裏是實現
}
// 這是一個具體的方法,但他什麼都不做。我們叫它爲hook(鉤子),馬上就來揭曉它如何使用
void hook();
}
鉤子是一種被聲明在抽象類中的方法,但只有空的或者默認的實現。鉤子的存在,可以讓子類有能力對算法的不同點進行掛鉤。要不要掛鉤,由子類決定。
鉤子有很多用途,我們先看其中一個:
咖啡
- 把水煮沸
- 用沸水沖泡咖啡
- 把咖啡倒進杯子
- 加糖和牛奶
茶
- 把水煮沸
- 用沸水浸泡茶葉
- 把茶倒進杯子
- 加檸檬
第1和第3步其實是一樣的,第2和第4步的操作一樣只是內容不同。據此可以抽取出一個共同點,設計出一個抽象類:
public abstract class CaffeineBeverageWithHook {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
// 我們加上一個小小的條件語句,該條件是否成立,是由一個具體方法決定
if (customerWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("Boiling water");
}
void pourInCup() {
System.out.println("Pouring into cup");
}
// 這就是一個鉤子,子類可以覆蓋這個方法,但不一定需要使用
boolean customerWantsCondiments() {
return true;
}
}
爲了使用鉤子,我們在子類中覆蓋它。在這裏,鉤子控制了咖啡因飲料是否執行某部分算法;比如,飲料中是否需要加進調料
咖啡
public class CoffeeWithHook extends CaffeineBeverageWithHook {
public void brew() {
System.out.println("Dripping Coffee through filter");
}
public void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
public boolean customerWantsCondiments() {
// 詢問用戶,是否要加調料
String answer = getUserInput();
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
String answer = null;
System.out.print("Would you like milk and sugar with your coffee (y/n)? ");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException ioe) {
System.err.println("IO error trying to read your answer");
}
if (answer == null) {
return "no";
}
return answer;
}
}
茶類
/**
*
*@author Saiuna
*@date 2020/7/6 8:47
*/
public class TeaWithHook extends CaffeineBeverageWithHook {
@Override
public void brew() {
System.out.println("Steeping the tea");
}
@Override
public void addCondiments() {
System.out.println("Adding Lemon");
}
@Override
public boolean customerWantsCondiments() {
String answer = getUserInput();
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
// get the user's response
String answer = null;
System.out.print("Would you like lemon with your tea (y/n)? ");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException ioe) {
System.err.println("IO error trying to read your answer");
}
if (answer == null) {
return "no";
}
return answer;
}
}
上面就是一個鉤子,詢問顧客是否想要調料?如果需要,就執行內容,否則就不執行。測試代碼如下:
public class BeverageTestDrive {
public static void main(String[] args) {
TeaWithHook teaHook = new TeaWithHook();
CoffeeWithHook coffeeHook = new CoffeeWithHook();
System.out.println("\nMaking tea...");
teaHook.prepareRecipe();
System.out.println("\nMaking coffee...");
coffeeHook.prepareRecipe();
}
}
執行結果如下:茶要加調料就給你加上了;咖啡不需要加調料,就沒給你加
Making tea...
Boiling water
Steeping the tea
Pouring into cup
Would you like lemon with your tea (y/n)? y
Adding Lemon
Making coffee...
Boiling water
Dripping Coffee through filter
Pouring into cup
Would you like milk and sugar with your coffee (y/n)? n
Process finished with exit code 0
那麼,我們使用鉤子的真正目的是什麼呢?
鉤子有幾種用法。如我們之前所說的,鉤子可以讓子類實現算法中可選的部分,或者在鉤子對於子類的實現並不重要的時候,子類可以對此鉤子置之不理。鉤子的另一個用法,是讓子類能夠 有機會對模板方法中某些即將發生的(或剛剛發生的)步驟做出反應;就像上述的飲料是否要加調料一樣。
好萊塢原則:“不要給我們打電話,我們會給你打電話(don‘t call us, we‘ll call you)”
好萊塢原則可以給我們一種防止“依賴腐敗”的方法。當高層組件依賴低層組件,而低層組件又依賴高層組件,而高層組件又依賴邊側組件,而邊側組件又依賴低層組件時,依賴腐敗就發生了。在這種情況下,沒有人可以輕易地搞懂系統是如何設計的。
在好萊塢原則下,我們允許低層組件將自己掛鉤到系統上,但是高層組件會決定什麼時候和怎樣使用這些低層組件。換句話說,高層組件對待低層組件的方式是“別調用我們,我們會調用你”。
好萊塢原則和模板方法之間的連接其實還算明顯:當我們設計模板方法時,我們告訴子類“不要調用我們,我們會調用你”。怎樣才能辦到呢?讓我們再看一次咖啡因飲料的設計:
我們之前還知道一個原則叫依賴倒置原則,好萊塢原則也是有點這個味道的對吧。他們之間的關係是如何的呢?
依賴倒置原則教我們儘量避免使用具體類,而多實用抽象。而好萊塢原則是用在創建框架或組件上的一種技巧,好讓低層組件能夠被掛鉤進計算中,而且又不會讓高層組件依賴低層組件。兩者的目標都是在於解耦,但是以來倒置原則更加註重如何在設計中避免依賴。
好萊塢原則教我們一個技巧,創建一個有彈性的設計,允許低層結構能夠互相操作,而又防止其他類太過於依賴它們。
注: 部分段落源於其他文章,原出處找不到了~