滿(mǎn)堂花醉三千客迁沫,一劍霜寒十四州
芦瘾。在并發(fā)編程之AQS探秘中捌蚊,我們有提到AbstractQueuedSynchroizer是基于模板方法模式設(shè)計(jì)的,那么到底什么是模板方法模式近弟?運(yùn)行的原理又是什么缅糟?下面就讓我們帶著這些問(wèn)題一探究竟。本文包括以下部分:
- 前言
- 模板方法模式
2.1 為何要用
2.2 定義
2.3 一個(gè)例子-奶茶
2.4 添加鉤子
2.5 JDK中的模板方法
2.6 模板方法VS策略模式VS工廠(chǎng)方法- 總結(jié)
1. 前言
面向?qū)ο笫澜缋锏娜筇匦浴?strong>封裝藐吮、繼承溺拱、多態(tài)
盛名在外逃贝,無(wú)人不知無(wú)人不曉谣辞。設(shè)計(jì)模式往往也是圍繞著這些特性展開(kāi)的,就拿封裝
來(lái)說(shuō)沐扳,工廠(chǎng)模式封裝了對(duì)象的創(chuàng)建泥从、命令模式封裝了方法的調(diào)用、門(mén)面模式封裝了復(fù)雜接口
沪摄。接下來(lái)我們要封裝算法
躯嫉。
2. 模板方法模式
2.1 為什么要用
當(dāng)我們用一種新東西的時(shí)候,著急去使用之前杨拐,不妨花一點(diǎn)時(shí)間想一想為什么要用祈餐,當(dāng)我們想明白為什么要用后。才會(huì)讓我們?cè)诮酉聛?lái)的運(yùn)用中更加得心應(yīng)手哄陶。
假設(shè)作者本人是個(gè)大廚??
帆阳,大廚現(xiàn)在要烹飪兩個(gè)菜:紅燒肉和糖醋排骨。
其中紅燒肉的制作
過(guò)程大致如下:
- 準(zhǔn)備五花肉
- 倒入炒鍋
- 翻炒
- 添加醬油
- 裝盤(pán)
而糖醋排骨
的制作過(guò)程大致如下:
- 準(zhǔn)備排骨
- 倒入炒鍋
- 翻炒
- 添加糖
- 裝盤(pán)
- 第一版代碼如下
package com.moxieliunian.template;
//紅燒肉
public class BraisedPork {
//食物制作
void prepareFood(){
//準(zhǔn)備五花肉
preparePork();
//倒入炒鍋
putInPan();
//翻炒
fry();
//添加醬油
addSoy();
//裝盤(pán)出鍋
fill();
}
void preparePork(){
System.out.println("準(zhǔn)備紅燒肉");
}
void putInPan(){
System.out.println("倒入鍋中");
}
void fry(){
System.out.println("翻炒");
}
void addSoy(){
System.out.println("添加醬油");
}
void fill(){
System.out.println("裝盤(pán)屋吨,制作完畢");
}
}
package com.moxieliunian.template;
//糖醋排骨
public class Ribs {
//食物制作
void prepareFood(){
//準(zhǔn)備五花肉
prepareRibs();
//倒入炒鍋
putInPan();
//翻炒
fry();
//添加糖
addSugar();
//裝盤(pán)出鍋
fill();
}
void prepareRibs(){
System.out.println("準(zhǔn)備排骨");
}
void putInPan(){
System.out.println("倒入鍋中");
}
void fry(){
System.out.println("翻炒");
}
void addSugar(){
System.out.println("添加糖");
}
void fill(){
System.out.println("裝盤(pán)蜒谤,制作完畢");
}
}
我們發(fā)現(xiàn)了這兩個(gè)類(lèi)中大量的重復(fù)代碼。既然有重復(fù)的代碼至扰,這表示我們可以重新整理下設(shè)計(jì)鳍徽。那么如何做呢?很自然的想法就是把公共的代碼提取出來(lái)敢课,放到一個(gè)基類(lèi)中阶祭。
- 第二版代碼如下:
public abstract class FoodBase {
abstract void prepareFood();
void putInPan(){
System.out.println("倒入鍋中");
}
void fry(){
System.out.println("翻炒");
}
void fill(){
System.out.println("裝盤(pán),制作完畢");
}
}
//紅燒肉
public class BraisedPork extends FoodBase {
@Override
void prepareFood() {
//準(zhǔn)備五花肉
preparePork();
//倒入炒鍋
putInPan();
//翻炒
fry();
//添加醬油
addSoy();
//裝盤(pán)出鍋
fill();
}
private void preparePork() {
System.out.println("準(zhǔn)備紅燒肉");
}
private void addSoy() {
System.out.println("添加醬油");
}
}
//糖醋排骨
public class Ribs extends FoodBase {
@Override
//食物制作
void prepareFood() {
//準(zhǔn)備五花肉
prepareRibs();
//倒入炒鍋
putInPan();
//翻炒
fry();
//添加糖
addSugar();
//裝盤(pán)出鍋
fill();
}
private void prepareRibs() {
System.out.println("準(zhǔn)備排骨");
}
private void addSugar() {
System.out.println("添加糖");
}
}
此時(shí)我們發(fā)現(xiàn)濒募,算法的實(shí)現(xiàn)流程由子類(lèi)控制,那么紅燒肉和糖醋排骨還有什么共通點(diǎn)嗎切厘?
preparePork和prepareRibs萨咳,addSoy和addSugar 只是作用的對(duì)象不一樣,但是具體的作用都是相同的疫稿。
我們嘗試做第三版抽象培他。
- 第三版代碼
public abstract class FoodBase {
//規(guī)定了食物的制作流程
protected void prepareFood() {
//準(zhǔn)備原材料
prepareMaterial();
//倒入炒鍋
putInPan();
//翻炒
fry();
//添加配料
addIngredient();
//裝盤(pán)出鍋
fill();
}
//準(zhǔn)備原材料
protected abstract void prepareMaterial();
//添加配料
protected abstract void addIngredient();
private void putInPan() {
System.out.println("倒入鍋中");
}
private void fry() {
System.out.println("翻炒");
}
private void fill() {
System.out.println("裝盤(pán)鹃两,制作完畢");
}
}
//紅燒肉
public class BraisedPork extends FoodBase {
@Override
protected void prepareMaterial() {
System.out.println("準(zhǔn)備紅燒肉");
}
@Override
protected void addIngredient() {
System.out.println("添加醬油");
}
}
//糖醋排骨
public class Ribs extends FoodBase {
@Override
protected void prepareMaterial() {
System.out.println("準(zhǔn)備排骨");
}
@Override
protected void addIngredient() {
System.out.println("添加糖");
}
}
我們將preparePork和prepareRibs,addSoy和addSugar 抽象為prepareMaterial和addIngredient
舀凛,此時(shí)類(lèi)圖如下:
問(wèn):第三版代碼(模板方法)相比前兩版而言,有什么好處猛遍?
1. 算法的骨架由基類(lèi)(FoodBase)規(guī)定且保護(hù)馋记,且只存在基類(lèi)中,后期修改只需要修改一處即可懊烤,便于維護(hù)梯醒。2. 子類(lèi)只需要覆蓋其需要覆蓋的方法,可以達(dá)到最大化的代碼復(fù)用腌紧。3. 同時(shí)可以時(shí)間算法的實(shí)現(xiàn)和算法本身想分離
2.2 定義
明白了上面的例子茸习,我們就了解了模板方法的原理。模板方法在一個(gè)方法中定義了算法的骨架壁肋,而將一些步驟延遲到子類(lèi)中号胚。模板方法使得子類(lèi)可以在不改變算法結(jié)構(gòu)的情況下,重新定義算法中的某些步驟浸遗。
就拿我們上面的例子來(lái)說(shuō)猫胁,模板方法FoodBase.prepareFood()規(guī)定了食物的制作流程為:準(zhǔn)備原料、倒入炒鍋跛锌、翻炒弃秆、添加配料媒峡、出鍋抵卫。子類(lèi)只需要實(shí)現(xiàn)準(zhǔn)備原料和添加配料的方法即可。所有的子類(lèi)食物制作方法都是按照這個(gè)流程進(jìn)行事格。
2.3 一個(gè)例子-奶茶
現(xiàn)在有個(gè)奶茶店氢卡,生產(chǎn)兩種奶茶:珍珠奶茶和紅豆奶茶
,其中珍珠奶茶制作流程如下:
- 準(zhǔn)備奶和茶
- 倒入杯子
- 添加珍珠
- 加冰
- 密封
而紅豆奶茶制作如下
- 準(zhǔn)備奶和茶
- 倒入杯子
- 添加紅豆
- 加冰
- 密封
這兩個(gè)過(guò)程有著相同的算法骨架锈至,我們很容易想到用模板方法模式來(lái)實(shí)現(xiàn),如下:
//奶茶算法基類(lèi)
public abstract class TeaBase {
protected void prdouceTea(){
//準(zhǔn)備奶和茶
prepareMilkAndTea();
//倒入杯子
putInCup();
//添加配料
addIngredient();
//加冰
addIce();
//密封打包
box();
}
private void prepareMilkAndTea(){
System.out.println("準(zhǔn)備奶和茶");
}
private void putInCup(){
System.out.println("倒入杯子");
}
protected abstract void addIngredient();
private void addIce(){
System.out.println("加冰");
}
private void box(){
System.out.println("密封");
}
}
//珍珠奶茶
public class BubbleTea extends TeaBase{
@Override
protected void addIngredient() {
System.out.println("添加珍珠");
}
}
//紅豆奶茶
public class RedBeanTea extends TeaBase{
@Override
protected void addIngredient() {
System.out.println("添加紅豆");
}
}
使用我們的算法
BubbleTea bubbleTea=new BubbleTea();
RedBeanTea redBeanTea=new RedBeanTea();
System.out.println("珍珠奶茶制作開(kāi)始");
bubbleTea.prdouceTea();
System.out.println("珍珠奶茶制作結(jié)束");
System.out.println("紅豆奶茶制作開(kāi)始");
redBeanTea.prdouceTea();
System.out.println("紅豆奶茶制作結(jié)束");
珍珠奶茶制作開(kāi)始
準(zhǔn)備奶和茶
倒入杯子
添加珍珠
加冰
密封
珍珠奶茶制作結(jié)束
紅豆奶茶制作開(kāi)始
準(zhǔn)備奶和茶
倒入杯子
添加紅豆
加冰
密封
紅豆奶茶制作結(jié)束
可以看到算法按照我們規(guī)定的骨架译秦,正確執(zhí)行了
2.4 添加鉤子
上面的例子中峡捡,我們使用模板方法模式來(lái)實(shí)現(xiàn)了奶茶的制作流程。但是有一個(gè)問(wèn)題筑悴,無(wú)論是珍珠奶茶還是紅豆奶茶们拙,都默認(rèn)加冰。如果要制作一杯不加冰的奶茶該如何操作呢阁吝?我們可以添加一個(gè)鉤子,Hook
砚婆。先看怎么用。
//奶茶算法基類(lèi)
public abstract class TeaBase {
protected void prdouceTea(){
//準(zhǔn)備奶和茶
prepareMilkAndTea();
//倒入杯子
putInCup();
//添加配料
addIngredient();
//默認(rèn)加冰
if (isNeedIce()){
//加冰
addIce();
}
//密封打包
box();
}
private void prepareMilkAndTea(){
System.out.println("準(zhǔn)備奶和茶");
}
private void putInCup(){
System.out.println("倒入杯子");
}
protected abstract void addIngredient();
//是否加冰突勇,默認(rèn)是
protected boolean isNeedIce(){
return true;
}
private void addIce(){
System.out.println("加冰");
}
private void box(){
System.out.println("密封");
}
}
//由客戶(hù)自己決定是否加冰的珍珠奶茶
public class SmartBubbleTea extends TeaBase{
private boolean isNeedIce;
SmartBubbleTea(boolean isNeedIce){
super();
this.isNeedIce=isNeedIce;
}
@Override
protected void addIngredient() {
System.out.println("添加珍珠");
}
@Override
public boolean isNeedIce() {
return isNeedIce;
}
}
使用
SmartBubbleTea smartBubbleTea = new SmartBubbleTea(false);
System.out.println("不加冰珍珠奶茶制作開(kāi)始");
smartBubbleTea.prdouceTea();
System.out.println("不加冰珍珠奶茶制作結(jié)束");
不加冰的珍珠奶茶制作開(kāi)始
準(zhǔn)備奶和茶
倒入杯子
添加珍珠
密封
不加冰的珍珠奶茶制作結(jié)束
可以看到装盯,我們利用hook成功實(shí)現(xiàn)了坷虑,奶茶加不加冰的自由控制。
hook的作用有以下幾點(diǎn):
-
鉤子可以讓子類(lèi)實(shí)現(xiàn)算法的可選部分埂奈,或者在鉤子對(duì)于子類(lèi)的實(shí)現(xiàn)不重要的時(shí)候迄损,子類(lèi)可以對(duì)這個(gè)鉤子置之不理
。 -
鉤子可以讓子類(lèi)對(duì)模板中即將發(fā)生的東西做出反應(yīng)
账磺。
2.5 JDK中的模板方法
上面說(shuō)了那么多芹敌,都是我們自己在寫(xiě)demo。那么JDK中有沒(méi)有用到模板方法的地方呢垮抗?當(dāng)然是有的氏捞,比如:AbstractQueuedSynchronizer.acquire(int arg)、AbstractList.addAll(int index, Collection<? extends E> c)借宵、Arrays.sort(Object[] a)
等幌衣,以Arrays.sort為例,找出其中的模板方法
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
assert a != null && lo >= 0 && lo <= hi && hi <= a.length;
int nRemaining = hi - lo;
if (nRemaining < 2)
return; // Arrays of size 0 and 1 are always sorted
// If array is small, do a "mini-TimSort" with no merges
if (nRemaining < MIN_MERGE) {
int initRunLen = countRunAndMakeAscending(a, lo, hi);
binarySort(a, lo, hi, lo + initRunLen);
return;
}
private static void binarySort(Object[] a, int lo, int hi, int start) {
assert lo <= start && start <= hi;
if (start == lo)
start++;
for ( ; start < hi; start++) {
Comparable pivot = (Comparable) a[start];
// Set left (and right) to the index where a[start] (pivot) belongs
int left = lo;
int right = start;
assert left <= right;
/*
* Invariants:
* pivot >= all in [lo, left).
* pivot < all in [right, start).
*/
while (left < right) {
int mid = (left + right) >>> 1;
if (pivot.compareTo(a[mid]) < 0)
right = mid;
else
left = mid + 1;
}
assert left == right;
可以看到壤玫,sort方法調(diào)用的時(shí)候,依賴(lài)于傳入對(duì)象的compareTo方法哼凯,也就是說(shuō)sort方法規(guī)定了排序的執(zhí)行骨架欲间,而由具體的類(lèi)去決定排序的算法細(xì)節(jié)(重寫(xiě)compareTo)
。這里之沒(méi)有用到繼承断部,一方面是因?yàn)閿?shù)組不能被繼承猎贴,另一方面則是因?yàn)榕判蚍椒ㄏM茏饔糜谒械臄?shù)組
。
我們可以看出設(shè)計(jì)模式往往并不是一塵不變的蝴光,使用中往往伴隨著不同場(chǎng)景的適配她渴。
2.6 模板方法VS策略模式VS工廠(chǎng)方法
模板方法模式和策略模式都是跟算法有關(guān),那么這兩個(gè)設(shè)計(jì)模式之間有什么異同呢蔑祟?模板方法模式和工廠(chǎng)方法模式又有什么異同呢趁耗?
名稱(chēng) | 定義 | 區(qū)別與聯(lián)系 |
---|---|---|
策略模式 | 定義了算法族,分別封裝起來(lái)疆虚,讓它們之間可以互換苛败。 | 與模板方法一樣都是作用于算法 ,不同的是策略模式側(cè)重于算法的替換径簿,使用組合 來(lái)實(shí)現(xiàn)罢屈。 |
模板方法模式 | 在一個(gè)方法中定義了算法的骨架,將某些實(shí)現(xiàn)推遲到子類(lèi)中篇亭,使子類(lèi)可以改變實(shí)現(xiàn)細(xì)節(jié)缠捌,而不影響整體架構(gòu) | 與策略模式一樣,都是作用于算法 译蒂。不同的是模板方法側(cè)重于子類(lèi)改變算法的某個(gè)細(xì)節(jié)曼月,而不會(huì)改變模板的整個(gè)算法骨架肃叶,使用繼承 實(shí)現(xiàn)。 |
工廠(chǎng)方法模式 | 定義了一個(gè)創(chuàng)建對(duì)象的接口十嘿,但由子類(lèi)決定要實(shí)例化的是哪一個(gè)因惭,將類(lèi)的實(shí)例化推遲到子類(lèi)中 | 工廠(chǎng)方法是模板方的一個(gè)特殊版本 |
3. 總結(jié)
本篇文章中,我們探討了模板方法模式的相關(guān)內(nèi)容绩衷。并和與之相似的設(shè)計(jì)模式:策略模式蹦魔、工廠(chǎng)方法模式進(jìn)行了簡(jiǎn)單對(duì)比。
由于技術(shù)水平所限咳燕,文章難免有不足之處勿决,歡迎大家指出。我們下一篇文章見(jiàn).....