人人都會設計模式—模版方法模式–Template-Method

tigerchain發表於2019-02-14
模版方法模式大綱

PS:轉載請註明出處
作者: TigerChain
地址: www.jianshu.com/p/6c6191a47…
本文出自 TigerChain 簡書 人人都會設計模式

教程簡介

  • 1、閱讀物件
    本篇教程適合新手閱讀,老手直接略過
  • 2、教程難度
    初級,本人水平有限,文章內容難免會出現問題,如果有問題歡迎指出,謝謝
  • 3、Demo 地址:Android Demo—github.com/githubchen0… 對應的 TemplateMethod
    Java Demo:github.com/githubchen0…

正文

一、什麼是模版方法模式

1、生活中的模版方法模式

1、燒茶、煮咖啡

身為苦逼的程式猿(媛),一定是茶葉和咖啡的忠實粉絲,多少個夜晚加班加點,累了困了喝紅牛—不對是喝茶葉、咖啡「我們無形中使用了一個設計模式–模版方法模式」。我們知道不管是燒茶、煮咖啡都基本上分為以下幾個步驟:

  • 1、燒水
  • 2、把茶葉或咖啡放入水壺中
  • 3、加熱不停的煮
  • 4、把煮好的茶葉或咖啡到入杯子中
  • 5、拿起杯子喝「不能直接喝,小心你的嘴」

我們看到除了原材料放入的不同「茶葉和咖啡」,其它的方法都一毛一樣,那麼我們把這些方法就可以制定為一個模版「相當於我們有一個既能燒茶又有煮咖啡的器具」,這就是模版定義了一個基本框架

2、高考答題

說了上面的例子,大家可能還懵懵的。那麼來說一個更實際的例子,參加過考慮的同學都知道一張考試卷子對所有的同學都是一模一樣的,這個卷子就是一個模版,以數學卷子為例吧:有選擇題、填空題、判斷題、應用題「這都是固定的」–這就是一個題的框架,是一個模版,至於每位考生如何答題那就考生的事情

2、程式中的模版方法模式

模版方法模式的定義

定義一個操作演算法的骨架「我們知道這個演算法所需要的關鍵步驟」,而將一些步驟的實現延遲到子類中去實現。通俗的說模版就是一個抽象類,方法就是策略「一些固定的步驟」。模版方法模式是一個行為型模式

模版方法模式的特點

演算法的結構不變,子類可以改變模版的某些步驟的實現方式,模版方法就是抽象的封裝,一般情況下,模版方法中有一些具體方法「部分邏輯」,抽象方法實現其它剩餘的邏輯「子類不同實現的方式就不同」,並且部分邏輯和剩餘邏輯共同組成了演算法的結構「一般是執行流程,這些流程是固定的,但是具體的實現細節不一樣,就可以使用模版方法」

封裝不變的部分,擴充套件可變的部分「可變的部分交給子類去實現」

模版方法模式的目的

模版方法模式的目的就是讓子類擴充套件或者具體實現模版中的固定演算法的中的某些演算法的步驟

模版方法簡單框架

模版方法模式的結構

角色 類別 說明
AbstractClass 抽象類 抽象模版類
ConcreateClass 具體模版 可以有多個「因為每個具體模版實現的內容可能不一樣」
HookMethod 鉤子方法 不是必須的,是一個開關,用來提供某些方法是否需要呼叫

模版方法模式簡單的 UML

模版方法模式簡單的 UML

二、模版方法模式舉例

1、把大象裝冰箱

把大象裝冰箱一共分為幾步?我們都知道三步:第一步把冰箱門開啟,第二步把大象裝進去,第三步把冰箱門蓋上。我們把裝大象的這三步運作可以看做一個演算法的步驟「這個步驟不變」,但是具體的你是使用松下冰箱裝大象,還是海爾冰箱裝大象,再進一步說使用冰箱裝所有動物,大象只是其中的一種,那麼就需要抽象出一個模版來,我們使用模版方法模式來實現這一過程

把大象裝冰箱簡單的 UML

把大象裝冰箱簡單的 UML

根據 UML 擼碼

  • 1、抽象出一個冰箱介面 IRefrige.java
/**
 * Created by TigerChain
 * 抽象冰箱
 */
public interface IRefrige {
    //取得品牌的名字
    String getRefrigeModel() ;
    //設定冰箱品牌
    void setModel(String model) ;
}
複製程式碼
  • 2、抽象一個動物類 Animail.java
/**
 * Created by TigerChain
 * 定義動物的抽象類
 */
public abstract class Animal {
    // 取得動物的名字
    abstract String getAnimailName() ;
}
複製程式碼
  • 3、定義抽象模版方法類 AbstractMothodWork.java
/**
 * Created by TigerChain
 * 抽象的模版類
 */
public abstract class AbstractMothodWork {
    //開啟冰箱
    abstract void open(IRefrige iRefrige) ;
    //把動物裝進去
    abstract void putin(Animail animail) ;
    //把冰箱門蓋上
    abstract void close() ;

    // 模版方法 定義演算法骨架 為了防止子類篡改模版方法步驟,加一個 final
    public final void handle(IRefrige iRefrige,Animail animal){
        this.open(iRefrige); //第一步
        this.putin(animail); //第二步
        this.close();        //第三步
    }
}
複製程式碼

我們看到冰箱裝動物的步驟是固定的,但是具體步驟內部實現交給子類去處理吧,這就是模版模式的使用場景

  • 4、來一個具體的模版 ConcreateMethodWork.java
/**
 * Created by TigerChain
 * 具體的模版類
 */
public class ConcreateMethodWork extends AbstractMothodWork {

    private IRefrige iRefrige ;
    private Animail animal ;

    @Override
    void open(IRefrige iRefrige) {
        this.iRefrige = iRefrige ;
        System.out.println("第 1 步把 "+iRefrige.getRefrigeModel()+" 門開啟");
    }

    @Override
    void putin(Animail animail) {
        this.animail = animal ;
        System.out.println("第 2 步把 "+animail.getAnimailName()+" 裝進去");
    }

    @Override
    void close() {
        System.out.println("第 3 步把冰箱門蓋上");
    }
}
複製程式碼
  • 5、來一個松下冰箱「當然可以是任意品牌的冰箱」 PanasonnicRefrige.java
/**
 * Created by TigerChain
 * 定義一臺松下冰箱
 */
public class PanasonnicRefrige implements IRefrige {

    private String model ;
    @Override
    public String getRefrigeModel() {
        return this.model!=null?this.model:"";
    }

    @Override
    public void setModel(String model) {
        this.model = model ;
    }
}
複製程式碼
  • 6、被裝的物件大象「當然還可以任何動物」 Elephant.java
/**
 * Created by TigerChain
 * 建立一個動物--大象
 */
public class Elephant extends Animal {

    @Override
    String getAnimailName() {
        return "大象";
    }
}
複製程式碼
  • 7、測試一下 Test.java
/**
 * Created by TigerChain
 * 測試類
 */
public class Test {
    public static void main(String args[]){
        // 要有冰箱
        IRefrige panasonnicRefrige = new PanasonnicRefrige() ;
        panasonnicRefrige.setModel("松下冰箱");

        // 要有動物,這裡是裝大象
        Animail elephant = new Elephant() ;

        //來個模版
        AbstractMothodWork  work = new ConcreateMethodWork() ;
		// 執行步驟
        work.handle(panasonnicRefrige,elephant);
    }
}
複製程式碼
  • 8、執行檢視結果
把大裝潢冰箱的結果

到此為止,我們就把大象裝到冰箱裡面了,當然你也可以把老虎、狼、貓裝進冰箱「擴充套件模版即可」,其實我們使用回撥也可以實現同樣的功能「本質上是模版方法的一種變異—是什麼?還是模版方法模式」,我們使用回撥方式修改上面程式碼「不破壞原來的結構,我們直接新加類」

  • 9、我們把想要擴充套件的方法全部抽象成介面,定義 ITemplate.java
抽象介面
  • 10、定義具體的模版 ConCreateTemplate.java 由於我們把想要擴充套件的模版方法都抽象出來了,所以我們新建模版的時候就不用抽象了「即定義具體的模版就可以了」
定義具體的模版
  • 11、修改測試類 Test.java 只貼出呼叫程式碼
修改測試類

執行結果和 8 中的結果是一樣的,這裡就不貼圖了,具體的程式碼可以看:github.com/githubchen0…

2、資料庫增、刪、改、查封裝

操作過資料庫的朋友對資料的增、刪、改、查再熟悉不過了,資料庫無非就是幹這個的。那麼我們可以使用模版方法模式把資料庫的增、刪、改、查封裝,至於查什麼,改什麼,交給具體的模版吧,典型的模版方法模式

資料庫增、刪、改、查 簡單的 UML

資料庫增、刪、改、查 簡單的 UML

根據 UML 擼碼

  • 1、定義抽象的模版–資料庫增、刪、改查抽象類 AbstractDAO.java
/**
 * Created by TigerChain
 * 定義抽象的資料庫增、刪、改、查的模版
 */
public abstract class AbstractDAO<T> {
    // 增加資料
    abstract void add(T t) ;
    // 根據 id 刪除資料
    abstract void delete(int id) ;
    // 更新資料
    abstract void update(T t) ;
    // 根據 id 查詢資料
    abstract T findById(int id);
    // 查詢所有資料
    abstract List<T> findall() ;
}
複製程式碼

我們這裡以泛型去接收實體類,至於查那個交給子類去實現–這樣就把共同點抽象出來了

  • 2、我們操作使用者表吧,定義一個 Person.java
/**
 * Created by TigerChain
 * 定義一個 JavaBean 對應資料庫中的表
 */
public class Person {
    private int id ;         // id
    private String name ;    // 姓名
    private int age ;        // 年齡
    private String address ; // 地址
    // 省略 setter 和 getter 方法
    ...   
}
複製程式碼
  • 3、定義一個操作使用者表的 DAO PersonConCreateDAO.java
**
 * Created by TigerChain
 * 一個具體的模版對使用者表的增、刪、改、查
 */
public class PersonConCreateDAO extends AbstractDAO<Person> {
    // 庫中的使用者列表
    private List<Person> persons = new ArrayList<>() ;

    @Override
    void add(Person person) {
        // 實際上應該做插入資料庫操作,為了簡單我們直接輸出語句
        persons.add(person) ;
        System.out.println("新增了 person "+person.toString());
    }

    @Override
    void delete(int id) {
        System.out.println("刪除了 id 為 "+id+" person "+persons.get(id-1));
        persons.remove(id-1) ;
    }

    @Override
    void update(Person person) {
        person.setId(1);
        person.setName("TigerChain");
        person.setAge(30);
        person.setAddress("中國陝西西安");

        System.out.println("更新了 person "+person.toString());
    }

    @Override
    Person findById(int id) {
        // 實際這裡應該從資料庫中查出資料,為了簡單模擬一個資料
        Person person = new Person() ;
        if(id ==1){
            person.setId(1);
            person.setName("TigerChain");
            person.setAge(28);
            person.setAddress("中國陝西");
        }
        System.out.println("查詢id 為 "+id+" 的 person "+person.toString());
        return person;
    }

    @Override
    List<Person> findall() {
        System.out.println("查詢所有的 person "+ persons.toString());
        return persons;
    }
}
複製程式碼
  • 4、測試一下 Test.java
public class Test {
    public static void main(String args[]){
        // 模擬兩個使用者資料
        Person person1 = new Person() ;
        person1.setId(1);
        person1.setName("TigerChain");
        person1.setAge(28);
        person1.setAddress("中國陝西");

        Person person2 = new Person() ;
        person2.setId(2);
        person2.setName("小陳");
        person2.setAge(30);
        person2.setAddress("中國陝西西安");

        PersonConCreateDAO personConCreateDAO = new PersonConCreateDAO() ;

        // 給庫中新增使用者
        personConCreateDAO.add(person1);
        personConCreateDAO.add(person2);

        // 更新使用者 1 的資料
        personConCreateDAO.update(person1);
        personConCreateDAO.findById(1);
        personConCreateDAO.findall() ;

        // 刪除一條資料
        personConCreateDAO.delete(1);
        // 查詢所有庫中的資料
        personConCreateDAO.findall() ;

    }
}
複製程式碼
  • 5、執行檢視結果
執行結果

至此,我們就使用模版方法模式實現了資料庫的增、刪、改、查、功能,至於你想操作別的表那直接寫一個具體的模版繼承抽象模版即可,大家動手寫一下,好好的體驗一下模版方法模式

3、考試答題

考試卷對每個考生來說都是一樣的「考試卷就是一個模版」,至於每個人如何答題那是每個考生的事情,針對考試答題我們可以使用模版方法模式來模擬這一過程,程式碼就不貼了「我上傳到了 github 上」,具體看這裡:github.com/githubchen0…

**PS:**模版方法模式除了抽象模版、具體模版之外,還可能會有一個鉤子方法「Hook」,也就是說抽象模版中把規定好了演算法的步驟 1 2 3 4 ,如果我只想使用 1 2 3 ,不想使用 4 呢?Hook 方法就派上用場了,以下是抽象模版帶 Hook 的虛擬碼

public abstract class AbstractTemplateMethod{

	abstract void step1() ;
	abstract void step2() ;
	abstract void step3() ;
	abstract void step4() ;
    void step5() ;
	// 模版方法
    public final void execute(){
		
	 this.step1() ;
     this.step2() ;	
     this.step3() ;
	 if(isUseStep4()){
       this.step4() ;
     }
     this.step5() ;
	}
    // 鉤子方法
    protected boolean isUseStep4(){
		return false ;
    }
}
複製程式碼

子類重寫 isUseStep4() 的方法返回 true 或 fals 決定是否使用 step4 步驟,這就是鉤子方法,大家自行感受一下,其實就是一個開關而已

三、Android 原始碼中的模版方法模式

1、View 中的 draw(Canvas canvas) 方法

我們自定義 View 的時候有時呼叫 ondraw(Canvas canvas) 方法,這裡就用到了模版方法模式,我們來看一下 ondraw 在什麼情況下呼叫「在 View 的 draw() 方法中呼叫了」,看看 draw() 方法的核心程式碼

draw 核心程式碼

這裡只不過把抽象方法改成了 protected 的一個空方法而已「本質上是一樣的」,具體代理就不貼了,大家動手扒扒這部分原始碼,其實模版方法模式我們經常用「只不過沒有意識到而已」

2、最熟悉的 Activity

Activity 就是一個模版,其中生命週期的方法就是”不固定”的方法,如果要改變子類重寫即可

  public class Activity extends ApplicationContext {
      protected void onCreate(Bundle savedInstanceState);
 
      protected void onStart();
 
      protected void onRestart();
 
      protected void onResume();
 
      protected void onPause();
 
      protected void onStop();
 
      protected void onDestroy();
}
複製程式碼

這裡定義成 protected 方法,那麼這個方法既可以是固定的也可以是不固定的「子類實現就不固定,如果不實現就是固定的,很靈活」,Activity 就是一個模版方法模式「你天天使用 Activity 知道它是模版模式嗎?」

3、封裝 BaseActivity

做過 Android 的朋友肯定都封裝過 BaseActivity ,把一些共公的部分抽象出來,然後封裝變化,比如我們的 app 應用介面都有共公的頭、下面是內容區域,如下圖

封裝 BaseActivity

然後不同的介面寫不同的子類繼承即可,我們使用虛擬碼來模擬一下

public abstract class TemplateMethodActivity extends AppCompatActivity {
    private Button titlebar_btn_left,titlebar_btn_right ;// 左右按鈕 
    private TextView titlebar_tv_center ; // 中間文字
    private RelativeLayout content ;      // 內容佈局

    private View titleView ;
	 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
		// 載入模版 xml 檔案
        setContentView(R.layout.templatemethod_activity);
        initView() ;
		// 設定子類佈局
        setContentLayout(getLayoutResID());
        getLayoutResID() ;
        
        init() ;
    }
	// 初始化操作,比如修改按鈕樣式等
    protected abstract void init();
    // 取得子佈局的 xml 檔案
    protected abstract int getLayoutResID();
	// 設定到 content 佈局上
    private void setContentLayout(int ResId) {
        LayoutInflater.from(this).inflate(ResId, content);
    }
    // 省略若干方法
}
複製程式碼

然後子類繼承這個 Activity 重寫抽象方法即可實現自己的介面內容,我們寫一個登入介面繼承 TemplateMethodActivity ,程式碼不貼了,直接上地址:github.com/githubchen0… 檢視 TemplateMethod 相關程式碼即可

最終執行效果如下:

模版方法模式實現 BaseActivity 結果

怎麼樣,是不是一直在使用模版方法模式「只是不知道而已」

四、模版方法模式的優缺點

優點

  • 1、封裝不變的部分,擴充套件可變的部分「交給子類去實現」,這是設計模式一慣的原則「開、閉原則」
  • 2、實現了程式碼複用

缺點

  • 繼承關係本身的缺點,如果父類新增一個新的抽象方法,所有的子類都要改一遍–痛苦

五、模版方法模式 VS 策略模式

以前介紹過策略模式,是對演算法的封裝,而模版方法模式也是對演算法執行,但是它們之間有明顯的區別

  • **策略模式:**目的是使不同的演算法可以被相互替換,不影響客戶端的使用

  • **模版方法模式:**針對定義一個演算法的流程,而將一些不太一樣的“具體實現步驟”交給子類去實現「不改變演算法的流程」

六、總結

  • 1、抽象類就是一個模版,而介面是一個標準,按這樣的規則可以確定該使用介面還是抽象類
  • 2、模版方法模式就是把不固定的步驟實現方式延遲到子類實現的一種方式,它是一種行為模式
  • 3、模版方法模式基本步驟是固定的「實際開發中會有很多變種,比如回撥替換,沒有固定的步驟全是不固定的等」
  • 4、 一般情況下為了防止子類去更新演算法的實現步驟,在抽象的模版方法上加一個 final 關鍵字

到上為止,模版方法模式就介紹完了,還是那句話,一定要動手試試哦,關注博主,更多精彩內容等著你,手把手教你學會知識點

公眾號:TigerChain 歡迎大家關注–更多精彩內容等著你

TigerChain

相關文章