一、 模板方法(Template Method)模式
準(zhǔn)備一個(gè)抽象類,將部分邏輯以具體方法以及具體構(gòu)造子的形式實(shí)現(xiàn),然后聲明一些抽象方法來(lái)迫使子類實(shí)現(xiàn)剩余的邏輯。不同的子類可以以不同的方式實(shí)現(xiàn)這些抽象方法,從而對(duì)剩余的邏輯有不同的實(shí)現(xiàn)。這就是模版方法模式的用意。
很多人可能沒(méi)有想到,模版方法模式實(shí)際上是所有模式中最為常見(jiàn)的幾個(gè)模式之一,而且很多人可能使用過(guò)模版方法模式而沒(méi)有意識(shí)到自己已經(jīng)使用了這個(gè)模式。模版方法模式是基于繼承的代碼復(fù)用的基本技術(shù),模版方法模式的結(jié)構(gòu)和用法也是面向?qū)ο笤O(shè)計(jì)的核心。
模版方法模式需要開(kāi)發(fā)抽象類和具體子類的設(shè)計(jì)師之間的協(xié)作。一個(gè)設(shè)計(jì)師負(fù)責(zé)給出一個(gè)算法的輪廓和骨架,另一些設(shè)計(jì)師則負(fù)責(zé)給出這個(gè)算法的各個(gè)邏輯步
驟。代表這些具體邏輯步驟的方法稱做基本方法(primitive method);而將這些基本法方法總匯起來(lái)的方法叫做模版方法(template
method),這個(gè)設(shè)計(jì)模式的名字就是從此而來(lái)。
二、 模版方法模式的結(jié)構(gòu)
模版方法模式的靜態(tài)結(jié)構(gòu)如下圖所示。

這里涉及到兩個(gè)角色:
- 抽象模版(AbstractClass)角色有如下的責(zé)任:
定義了一個(gè)或多個(gè)抽象操作,以便讓子類實(shí)現(xiàn)。這些抽象操作叫做基本操作,它們是一個(gè)頂級(jí)邏輯的組成步驟。
定義并實(shí)現(xiàn)了一個(gè)模版方法。這個(gè)模版方法一般是一個(gè)具體方法,它給出了一個(gè)頂級(jí)邏輯的骨架,而邏輯的組成步驟在相應(yīng)的抽象操作中,推遲到子類實(shí)現(xiàn)。頂級(jí)邏輯也有可能調(diào)用一些具體方法。
- 具體模版(ConcreteClass)角色有如下的責(zé)任:
實(shí)現(xiàn)父類所定義的一個(gè)或多個(gè)抽象方法,它們是一個(gè)頂級(jí)邏輯的組成步驟。
每一個(gè)抽象模版角色都可以有任意多個(gè)具體模版角色與之對(duì)應(yīng),而每一個(gè)具體模版角色都可以給出這些抽象方法(也就是頂級(jí)邏輯的組成步驟)的不同實(shí)現(xiàn),從而使得頂級(jí)邏輯的實(shí)現(xiàn)各不相同。
三、 模板方法模式的示意性代碼
// Template Method pattern -- Structural example
using System;

// "AbstractClass"
abstract class AbstractClass
  {
// Methods
abstract public void PrimitiveOperation1();
abstract public void PrimitiveOperation2();

// The Template method
public void TemplateMethod()
 {
Console.WriteLine("In AbstractClass.TemplateMethod()");
PrimitiveOperation1();
PrimitiveOperation2();
}
}

// "ConcreteClass"
class ConcreteClass : AbstractClass
  {
// Methods
public override void PrimitiveOperation1()
 {
Console.WriteLine("Called ConcreteClass.PrimitiveOperation1()");
}

public override void PrimitiveOperation2()
 {
Console.WriteLine("Called ConcreteClass.PrimitiveOperation2()");
}
}

 /**////
/// Client test
/// public class Client
  {
public static void Main( string[] args )
 {
// Create instance and call template method
ConcreteClass c = new ConcreteClass();
c.TemplateMethod();
}
}
四、 繼承作為復(fù)用的工具
使用繼承作為復(fù)用的手段必須慎重,C#語(yǔ)言的設(shè)計(jì)師對(duì)使用繼承作為復(fù)用的工具有著不同層次上的認(rèn)識(shí)。
不知其一
首先,初學(xué)C#的程序員可能不知道什么是繼承,或者認(rèn)為"繼承"是高深的工具。那時(shí)候,大部分的功能復(fù)用都是通過(guò)委派進(jìn)行的。
知其一、不知其二
然后慢慢地,他們發(fā)現(xiàn)在C#語(yǔ)言里實(shí)現(xiàn)繼承并不困難,并且初步認(rèn)識(shí)到繼承可以使子類一下子得到基類的行為。這時(shí)他們就會(huì)躍躍欲試了,試圖使用繼承作為功能復(fù)用的主要工具,并把原來(lái)應(yīng)當(dāng)使用委派的地方,改為使用繼承,這時(shí)繼承就有被濫用的危險(xiǎn)。
知其二
很多面向?qū)ο蟮脑O(shè)計(jì)專家從1986年就開(kāi)始警告繼承關(guān)系被濫用的可能。有一些面向?qū)ο蟮木幊陶Z(yǔ)言,如SELF語(yǔ)言,甚至將類的繼承關(guān)系從語(yǔ)言的功能中取消掉,改為完全使用委派。
其他的設(shè)計(jì)師雖然不提倡徹底取消繼承,但無(wú)一例外地鼓勵(lì)在設(shè)計(jì)中盡可能使甩委派關(guān)系代替繼承關(guān)系。比如在【GOF95】一書(shū)中,狀態(tài)模式、策略模
式、裝飾模式、橋梁模式以及抽象工廠模式均是將依賴于繼承的實(shí)現(xiàn)轉(zhuǎn)換為基于對(duì)象的組合和聚合的實(shí)現(xiàn),這些模式的要點(diǎn)就是使用委派關(guān)系代替繼承關(guān)系。
知其三
是不是繼承就根本不該使用呢?事實(shí)上對(duì)數(shù)據(jù)的抽象化、繼承、封裝和多態(tài)性并稱C#和其他絕大多數(shù)的面向?qū)ο笳Z(yǔ)言的幾項(xiàng)最重要的特性。繼承不應(yīng)當(dāng)被濫用,并不意味著繼承根本就不該使用。因?yàn)槔^承容易被濫用就徹底拋棄繼承,無(wú)異于因噎廢食。
繼承使得類型的等級(jí)結(jié)構(gòu)易于理解、維護(hù)和擴(kuò)展,而類型的等級(jí)結(jié)構(gòu)非常適合于抽象化的設(shè)計(jì)、實(shí)現(xiàn)和復(fù)用。盡管【GOF95】所給出的設(shè)計(jì)模式基本上沒(méi)
有太多基于繼承的模式,很多模式都是用繼承的辦法定義、實(shí)現(xiàn)接口的。多數(shù)的設(shè)計(jì)模式都描寫(xiě)一個(gè)以抽象類作為基類,以具體類作為實(shí)現(xiàn)的等級(jí)結(jié)構(gòu),比如適配器
模式、合成模式、橋梁模式、狀態(tài)模式等。
模版方法模式則更進(jìn)了一步:此模式鼓勵(lì)恰當(dāng)?shù)厥褂美^承。此模式可以用來(lái)改寫(xiě)一些擁有相同功能的相關(guān)的類,將可復(fù)用的一般性的行為代碼移到基類里面,而把特殊化的行為代碼移到子類里面。
因此,熟悉模版方法模式便成為一個(gè)重新學(xué)習(xí)繼承的好地方。
五、 一個(gè)實(shí)際應(yīng)用模板方法的例子
下面的例子演示了數(shù)據(jù)庫(kù)訪問(wèn)的模板方法。實(shí)際應(yīng)用時(shí),請(qǐng)確保C盤根目錄下有nwind.mdb這個(gè)Access數(shù)據(jù)庫(kù)(可以從Office的安裝目錄下找到。中文版用戶的請(qǐng)注意字段名可能有所不同)。
// Template Method pattern -- Real World example
using System;
using System.Data;
using System.Data.OleDb;

// "AbstractClass"
abstract class DataObject
  {
// Methods
abstract public void Connect();
abstract public void Select();
abstract public void Process();
abstract public void Disconnect();

// The "Template Method"
public void Run()
 {
Connect();
Select();
Process();
Disconnect();
}
}

// "ConcreteClass"
class CustomerDataObject : DataObject
  {
private string connectionString =
"provider=Microsoft.JET.OLEDB.4.0; "
+ "data source=c:\\nwind.mdb";
private string commandString;
private DataSet dataSet;
// Methods
public override void Connect( )
 {
// Nothing to do
}

public override void Select( )
 {
commandString = "select CompanyName from Customers";
OleDbDataAdapter dataAdapter = new OleDbDataAdapter(
commandString, connectionString );
dataSet = new DataSet();
dataAdapter.Fill( dataSet, "Customers" );
}

public override void Process()
 {
DataTable dataTable = dataSet.Tables["Customers"];
foreach( DataRow dataRow in dataTable.Rows )
Console.WriteLine( dataRow[ "CompanyName" ] );
}

public override void Disconnect()
 {
// Nothing to do
}
}

 /**////
/// TemplateMethodApp test
/// public class TemplateMethodApp
  {
public static void Main( string[] args )
 {
CustomerDataObject c = new CustomerDataObject( );
c.Run();
}
}
六、 模版方法模式中的方法
模版方法中的方法可以分為兩大類:模版方法(Template Method)和基本方法(Primitive Method)。
模版方法
一個(gè)模版方法是定義在抽象類中的,把基本操作方法組合在一起形成一個(gè)總算法或一個(gè)總行為的方法。這個(gè)模版方法一般會(huì)在抽象類中定義,并由子類不加以修改地完全繼承下來(lái)。
基本方法
基本方法又可以分為三種:抽象方法(Abstract Method)、具體方法(Concrete Method)和鉤子方法(Hook Method)。
抽象方法:一個(gè)抽象方法由抽象類聲明,由具體子類實(shí)現(xiàn)。在C#語(yǔ)言里一個(gè)抽象方法以abstract關(guān)鍵字標(biāo)示出來(lái)。
具體方法:一個(gè)具體方法由抽象類聲明并實(shí)現(xiàn),而子類并不實(shí)現(xiàn)或置換。在C#語(yǔ)言里面,一個(gè)具體方法沒(méi)有abstract關(guān)鍵字。
鉤子方法:一個(gè)鉤子方法由抽象類聲明并實(shí)現(xiàn),而子類會(huì)加以擴(kuò)展。通常抽象類給出的實(shí)現(xiàn)是一個(gè)空實(shí)現(xiàn),作為方法的默認(rèn)實(shí)現(xiàn)。(Visual
FoxPro中項(xiàng)目向?qū)Ы⒌捻?xiàng)目會(huì)使用一個(gè)AppHook類實(shí)現(xiàn)監(jiān)視項(xiàng)目成員變化,調(diào)整系統(tǒng)結(jié)構(gòu)的工作。)鉤子方法的名字通常以do開(kāi)始。
七、 重構(gòu)的原則
在對(duì)一個(gè)繼承的等級(jí)結(jié)構(gòu)做重構(gòu)時(shí),一個(gè)應(yīng)當(dāng)遵從的原則便是將行為盡量移動(dòng)到結(jié)構(gòu)的高端,而將狀態(tài)盡量移動(dòng)到結(jié)構(gòu)的低端。
1995年,Auer曾在文獻(xiàn)【AUER95】中指出:
- 應(yīng)當(dāng)根據(jù)行為而不是狀態(tài)定義一個(gè)類。也就是說(shuō),一個(gè)類的實(shí)現(xiàn)首先建立在行為的基礎(chǔ)之上,而不是建立在狀態(tài)的基礎(chǔ)之上。
- 在實(shí)現(xiàn)行為時(shí),是用抽象狀態(tài)而不是用具體狀態(tài)。如果一個(gè)行為涉及到對(duì)象的狀態(tài)時(shí),使用間接的引用而不是直接的引用。換言之,應(yīng)當(dāng)使用取值方法而不是直接引用屬性。
- 給操作劃分層次。一個(gè)類的行為應(yīng)當(dāng)放到一個(gè)小組核心方法(Kernel Methods)里面,這些方法可以很方便地在子類中加以置換。
- 將狀態(tài)屬性的確認(rèn)推遲到子類中。不要在抽象類中過(guò)早地聲明屬性變量,應(yīng)將它們盡量地推遲到子類中去聲明。在抽象超類中,如果需要狀態(tài)屬性的話,可以調(diào)用抽象的取值方法,而將抽象的取值方法的實(shí)現(xiàn)放到具體子類中。
如果能夠遵從這樣的原則,那么就可以在等級(jí)結(jié)構(gòu)中將接口與實(shí)現(xiàn)分隔開(kāi)來(lái),將抽象與具體分割開(kāi)來(lái),從而保證代碼可以最大限度地被復(fù)用。這個(gè)過(guò)程實(shí)際上是將設(shè)計(jì)師引導(dǎo)到模版方法模式上去。
|