学习设计模式第二十五 - 模板方法模式
本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考。转载请标明原作者TerryLee。部分示例代码来自DoFactory。
概述
变化一直以来都是软件设计的永恒话题,在XP编程中提倡拥抱变化,积极应对。如何更好的去抓住变化点,应对变化?如何更好的提高代码复用?通过学习Template Method模式,您应该有一个新的认识。
意图
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
UML
图1 Template Method 模式结构图
参与者
这个模式涉及的类或对象:
-
AbstractClass
-
定义抽象的基元操作,具体子类定义算法每一步的实现。
-
实现一个模板方法定义算法的骨架。模板方法调用基元操作及定义于AbstractClass或那些其它对象中的操作。
-
ConcreteClass
-
实现基元操作以完成算法的具体子类步骤。
适用性
模板方法模式的意图是为算法的一系列步骤提供一个大纲。继承类保持算法的原始结构,但可以选择重定义或调整具体算法的步骤。这种模式被设计来为客户程序员提供可扩展性。模板方法常在构建一个供其它客户程序员使用的类库(如一个应用框架)时使用。.NET Framework中的例子(见下文章节)展示了这种模式可以用于何时何地。
模板方法模式与策略模式有极大的相似处。它们都是设计用于增大扩展性和可定制性,因为它们都让客户端可以改变算法或过程执行的方式。不同之处在于策略中整个算法会被改变,而模板方法中每一步可以单独被重定义。然而,它们面向对象实现完全不同:策略使用委托,模板方法基于对象继承。
如下是几种适用模板方法模式的具体场景:
-
一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
-
各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。这是Opdyke和Johnson所描述过的"重分解以一般化"的一个很好的例子。首先识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
-
控制子类扩展。模板方法只在特定点调用"Hook"操作,这样就只允许在这些点进行扩展。
DoFactory GoF代码
标准代码演示了模板方法提供一个顺序调用方法的骨架。一个或多个步骤的实现可以延迟到子类中,而在子类中实现这些步骤也不会对整体调用顺序产生影响。
// Template Method pattern // Structural example using System; namespace DoFactory.GangOfFour.Template.Structural { // MainApp test application class MainApp { static void Main() { AbstractClass aA = new ConcreteClassA(); aA.TemplateMethod(); AbstractClass aB = new ConcreteClassB(); aB.TemplateMethod(); // Wait for user Console.ReadKey(); } } // "AbstractClass" abstract class AbstractClass { public abstract void PrimitiveOperation1(); public abstract void PrimitiveOperation2(); // The "Template method" public void TemplateMethod() { PrimitiveOperation1(); PrimitiveOperation2(); Console.WriteLine(""); } } // "ConcreteClass" class ConcreteClassA : AbstractClass { public override void PrimitiveOperation1() { Console.WriteLine("ConcreteClassA.PrimitiveOperation1()"); } public override void PrimitiveOperation2() { Console.WriteLine("ConcreteClassA.PrimitiveOperation2()"); } } // "ConcreteClass" class ConcreteClassB : AbstractClass { public override void PrimitiveOperation1() { Console.WriteLine("ConcreteClassB.PrimitiveOperation1()"); } public override void PrimitiveOperation2() { Console.WriteLine("ConcreteClassB.PrimitiveOperation2()"); } } }
实际应用的代码中演示了一个名为Run()的模板方法提供一个顺序调用方法的骨架。这些步骤的实现被延迟到CustomerDataObject子类中,在这个子类中实现了Connect、Select、Process和Disconnect方法。
例子中涉及到的类与职责链模式中标准的类对应关系如下:
-
AbstractClass – DataObject
-
ConcreteClass – CustomerDataObject
// Template Method pattern // Real World example using System; using System.Data; using System.Data.OleDb; namespace DoFactory.GangOfFour.Template.RealWorld { // MainApp test application class MainApp { static void Main() { DataAccessObject daoCategories = new Categories(); daoCategories.Run(); DataAccessObject daoProducts = new Products(); daoProducts.Run(); // Wait for user Console.ReadKey(); } } // "AbstractClass" abstract class DataAccessObject { protected string connectionString; protected DataSet dataSet; public virtual void Connect() { // Make sure mdb is available to app connectionString = "provider=Microsoft.JET.OLEDB.4.0; " + "data source=..\\..\\..\\db1.mdb"; } public abstract void Select(); public abstract void Process(); public virtual void Disconnect() { connectionString = ""; } // The 'Template Method' public void Run() { Connect(); Select(); Process(); Disconnect(); } } // "ConcreteClass" class Categories : DataAccessObject { public override void Select() { string sql = "select CategoryName from Categories"; OleDbDataAdapter dataAdapter = new OleDbDataAdapter(sql, connectionString); dataSet = new DataSet(); dataAdapter.Fill(dataSet, "Categories"); } public override void Process() { Console.WriteLine("Categories ---- "); DataTable dataTable = dataSet.Tables["Categories"]; foreach (DataRow row in dataTable.Rows) { Console.WriteLine(row["CategoryName"]); } Console.WriteLine(); } } // "ConcreteClass" class Products : DataAccessObject { public override void Select() { string sql = "select ProductName from Products"; OleDbDataAdapter dataAdapter = new OleDbDataAdapter(sql, connectionString); dataSet = new DataSet(); dataAdapter.Fill(dataSet, "Products"); } public override void Process() { Console.WriteLine("Products ---- "); DataTable dataTable = dataSet.Tables["Products"]; foreach (DataRow row in dataTable.Rows) { Console.WriteLine(row["ProductName"]); } Console.WriteLine(); } } }
.NET优化版示例与上面实际应用的例子基本相同,主要是由于基本没有有意义的.NET优化。
// Template Design Pattern // .NET optimized example using System; using System.Data; using System.Data.OleDb; namespace DoFactory.GangOfFour.Template.NETOptimized { class MainApp { static void Main() { DataAccessObject daoCategories = new Categories(); daoCategories.Run(); DataAccessObject daoProducts = new Products(); daoProducts.Run(); // Wait for user Console.ReadKey(); } } // "AbstractClass" abstract class DataAccessObject { protected string connectionString; protected DataSet dataSet; public virtual void Connect() { // Make sure mdb is available to app connectionString ="provider=Microsoft.JET.OLEDB.4.0; " +"data source=..\\..\\..\\db1.mdb"; } public abstract void Select(); public abstract void Process(); virtual public void Disconnect() { connectionString = ""; } // The 'Template Method' public void Run() { Connect(); Select(); Process(); Disconnect(); } } // "ConcreteClass" class Categories : DataAccessObject { public override void Select() { string sql = "select CategoryName from Categories"; var dataAdapter = new OleDbDataAdapter(sql, connectionString); dataSet = new DataSet(); dataAdapter.Fill(dataSet, "Categories"); } public override void Process() { Console.WriteLine("Categories ---- "); var dataTable = dataSet.Tables["Categories"]; foreach (DataRow row in dataTable.Rows) { Console.WriteLine(row["CategoryName"]); } Console.WriteLine(); } } // "ConcreteClass" class Products : DataAccessObject { public override void Select() { string sql = "select ProductName from Products"; var dataAdapter = new OleDbDataAdapter(sql, connectionString); dataSet = new DataSet(); dataAdapter.Fill(dataSet, "Products"); } public override void Process() { Console.WriteLine("Products ---- "); var dataTable = dataSet.Tables["Products"]; foreach (DataRow row in dataTable.Rows) { Console.WriteLine(row["ProductName"]); } Console.WriteLine(); } } }
模板方法常见于UI编程中。近期的一个例子是WPF中的布局系统。在WPF中如果你需要一个自定义布局,你可以创建一个继承自Panel的类并重写两个方法 – MeasureOverride()和ArrangeOverride()。这是模板方法一个优雅的例子。
来自《大话设计模式》的例子
这个例子中学生们共同使用一份试卷并分别作答,通过模板方法模式将所有共同的部分提取到试卷类中,不同学生只需要分别作答即可。
using System; namespace TemplateMethodPattern { class Program { static void Main(string[] args) { Console.WriteLine("学生甲抄的试卷:"); TestPaper studentA = new TestPaperA(); studentA.TestQuestion1(); studentA.TestQuestion2(); studentA.TestQuestion3(); Console.WriteLine("学生乙抄的试卷:"); TestPaper studentB = new TestPaperB(); studentB.TestQuestion1(); studentB.TestQuestion2(); studentB.TestQuestion3(); Console.Read(); } } class TestPaper { public void TestQuestion1() { Console.WriteLine(" 杨过得到,后来给了郭靖,炼成倚天剑、屠龙刀的玄铁可能是[ ] a.球磨铸铁 b.马口铁 c.高速合金钢 d.碳素纤维 "); Console.WriteLine("答案:" + Answer1()); } public void TestQuestion2() { Console.WriteLine(" 杨过、程英、陆无双铲除了情花,造成[ ] a.使这种植物不再害人 b.使一种珍稀物种灭绝 c.破坏了那个生物圈的生态平衡 d.造成该地区沙漠化 "); Console.WriteLine("答案:" + Answer2()); } public void TestQuestion3() { Console.WriteLine(" 蓝凤凰的致使华山师徒、桃谷六仙呕吐不止,如果你是大夫,会给他们开什么药[ ] a.阿司匹林 b.牛黄解毒片 c.氟哌酸 d.让他们喝大量的生牛奶 e.以上全不对 "); Console.WriteLine("答案:" + Answer3()); } protected virtual string Answer1() { return ""; } protected virtual string Answer2() { return ""; } protected virtual string Answer3() { return ""; } } //学生甲抄的试卷 class TestPaperA : TestPaper { protected override string Answer1() { return "b"; } protected override string Answer2() { return "c"; } protected override string Answer3() { return "a"; } } //学生乙抄的试卷 class TestPaperB : TestPaper { protected override string Answer1() { return "c"; } protected override string Answer2() { return "a"; } protected override string Answer3() { return "a"; } } }
Template Method模式解说
李建忠老师说过一句话,如果你只想掌握一种设计模式的话,那这个模式一定是Template Method模式。对于这个问题,我想可能是仁者见仁,智者见智,但是有一点不能否认的Template Method模式是非常简单而且几乎是无处不用,很少有人没有用过它。下面我们以一个简单的数据库查询的例子来说明Template Method模式(注意:这个例子在实际数据库开发中并没有任何实际意义,这里仅仅是为了作为示例而已)。
假如我们需要简单的读取Northwind数据库中的表的记录并显示出来。对于数据库操作,我们知道不管读取的是哪张表,它一般都应该经过如下这样的几步:
-
连接数据库(Connect)
-
执行查询命令(Select)
-
显示数据(Display)
-
断开数据库连接(Disconnect)
这些步骤是固定的,但是对于每一张具体的数据表所执行的查询却是不一样的。显然这需要一个抽象角色,给出顶级行为的实现。如下图:
图2. 数据访问组件UML
Template Method模式的实现方法是从上到下,我们首先给出顶级框架DataAccessObject的实现逻辑:
public abstract class DataAccessObject { protected string connectionString; protected DataSet dataSet; public virtual void Connect() { connectionString = "Server=Rj-097;User Id=sa;Password=sa;Database=Northwind"; } public abstract void Select(); public abstract void Display(); public virtual void Disconnect() { connectionString = ""; } // The "Template Method" public void Run() { Connect(); Select(); Display(); Disconnect(); } }
显然在这个顶级的框架DataAccessObject中给出了固定的轮廓,方法Run()便是模版方法,Template Method模式也由此而得名。而对于Select()和Display()这两个抽象方法则留给具体的子类去实现,如下图:
图3.使用模板方法模式生成具有数据访问能力的对象
示意性实现代码:
class Categories : DataAccessObject { public override void Select() { string sql = "select CategoryName from Categories"; SqlDataAdapter dataAdapter = new SqlDataAdapter(sql, connectionString); dataSet = new DataSet(); dataAdapter.Fill(dataSet, "Categories"); } public override void Display() { Console.WriteLine("Categories ---- "); DataTable dataTable = dataSet.Tables["Categories"]; foreach (DataRow row in dataTable.Rows) { Console.WriteLine(row["CategoryName"].ToString()); } Console.WriteLine(); } } class Products : DataAccessObject { public override void Select() { string sql = "select top 10 ProductName from Products"; SqlDataAdapter dataAdapter = new SqlDataAdapter(sql, connectionString); dataSet = new DataSet(); dataAdapter.Fill(dataSet, "Products"); } public override void Display() { Console.WriteLine("Products ---- "); DataTable dataTable = dataSet.Tables["Products"]; foreach (DataRow row in dataTable.Rows) { Console.WriteLine(row["ProductName"].ToString()); } Console.WriteLine(); } }
再来看看客户端程序的调用,不需要再去调用每一个步骤的方法:
public class App { static void Main() { DataAccessObject dao; dao = new Categories(); dao.Run(); dao = new Products(); dao.Run(); // Wait for user Console.Read(); } }
在上面的例子中,需要注意的是:
-
对于Connect()和Disconnect()方法实现为了virtual,而Select()和Display()方法则为abstract,这是因为如果这个方法有默认的实现,则实现为virtual,否则为abstract。
-
Run()方法作为一个模版方法,它的一个重要特征是:在基类里定义,而且不能够被派生类更改。有时候它是私有方法(private method),但实际上它经常被声明为protected。它通过调用其它的基类方法(覆写过的)来工作,但它经常是作为初始化过程的一部分被调用的,这样就没必要让客户端程序员能够直接调用它了。
-
在一开始我们提到了不管读的是哪张数据表,它们都有共同的操作步骤,即共同点。因此可以说Template Method模式的一个特征就是剥离共同点。
.NET 中的模板方法模式
模板方法在.NET Framework中经常用到。通过这种模式.NET给其API的使用者提供了很大的扩展性。看一下ASP.NET中的自定义控件。自定义控件通过继承自一个基类(Control或WebControl)来创建。开箱即用,这些基类处理所有控件通用的功能,如初始化、加载、呈现、卸载及在恰当的时间出发控件生命周期中的事件。你的自定义控件通过重写基控件生成的算法,如Render和CreateChildControls方法,中的单独的步骤,或者包括处理如Postback这样的特定事件来扩展这些基类的功能。微软给这些方法添加了Core后缀。
System.Windows.Forms命名空间中的Control类也展示了模板方法的使用。这些模板方法通过将方法集中在一个单一的虚函数中式设计器基类具有扩展性。微软给这些方法添加了"Core"后缀。以控件类为例,其方法如:SetBoundsCore(),ScaleCore(),SetVisibleCore()等。这些模板方法的访问修饰符大部分是protected virtual。
.NET Framework中Template Method模式的使用可以说是无处不在,比如说我们需要自定义一个文本控件,会让它继承于RichTextBox,并重写其中部分事件,如下例所示:
public class MyRichTextBox : RichTextBox { private static bool m_bPaint = true; private string m_strLine = ""; private int m_nContentLength = 0; private int m_nLineLength = 0; private int m_nLineStart = 0; private int m_nLineEnd = 0; private string m_strKeywords = ""; private int m_nCurSelection = 0; protected override void OnSelectionChanged(EventArgs e) { m_nContentLength = this.TextLength; int nCurrentSelectionStart = SelectionStart; int nCurrentSelectionLength = SelectionLength; m_bPaint = false; m_nLineStart = nCurrentSelectionStart; while ((m_nLineStart > 0) && (Text[m_nLineStart - 1] != ',') && (Text[m_nLineStart - 1] != '{') && (Text[m_nLineStart - 1] != '(')) m_nLineStart--; m_nLineEnd = nCurrentSelectionStart; while ((m_nLineEnd < Text.Length) && (Text[m_nLineEnd] != ',') && (Text[m_nLineEnd] != '}') && (Text[m_nLineEnd] != ')') && (Text[m_nLineEnd] != '{')) m_nLineEnd++; m_nLineLength = m_nLineEnd - m_nLineStart; m_strLine = Text.Substring(m_nLineStart, m_nLineLength); this.SelectionStart = m_nLineStart; this.SelectionLength = m_nLineLength; m_bPaint = true; } protected override void OnTextChanged(EventArgs e) { // 重写OnTextChanged } }
其中OnSelectionChanged()和OnTextChanged()便是Template Method模式中的基本方法之一,也就是子步骤方法,它们的调用已经在RichTextBox中实现了。
Array类的Sort方法也是一类隐含的模板方法,任何自定义的数组类都可以通过重写Sort方法来定义新的排序方法。
实现要点
-
Template Method模式是一种非常基础性的设计模式,在面向对象系统中有着大量的应用。它用最简洁的机制(虚函数的多态性)为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本实现结构。
-
除了可以灵活应对子步骤的变化外,"不用调用我,让我来调用你"的反向控制结构是Template Method的典型应用。
-
在具体实现方面,被Template Method调用的虚方法可以具有实现,也可以没有任何实现(抽象方法,纯虚方法),但一般推荐将它们设置为protected方法。
-
为了防止子类改变模板方法中的算法,可以将模板声明为sealed。
与其他模式关系:
策略模式和模板方法模式都封装算法,一个用组合,一个用继承。
工厂方法是模板方法的一种特殊版本。
总结
模板方法模式通过把不变行为搬移到超类,去除子类中的重复代码来体现它的优势。Template Method模式是非常简单的一种设计模式,但它却是代码复用的一项基本的技术,在类库中尤其重要。