1. 模板方法的引入
变与不变
- 变化是软件设计的永恒主题,如何管理变化带来的复杂性?设计模式的艺术性和复杂度就在于如何分析,并发现系统中的变化点和稳定点,并使用特定的设计方法来应对这种变化。
- 在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却有很多改变的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
- 如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求?
- 准备一个抽象类,将部分逻辑以具体方法以及具体构造子类的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模版方法模式的用意。
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构,即可重定义该算法的某些特定步骤。
——《设计模式》GoF
2. 模板方法的结构
- 抽象模版(AbstractClass)角色:
- 定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。
- 定义并实现了一个模版方法。这个模版方法一般是一个具体方法,它给出了一个顶级逻辑的骨架(操作步骤),而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
- 具体模版(ConcreteClass)角色:
- 实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
- 每一个抽象模版角色都可以有任意多个具体模版角色与之对应,而每一个具体模版角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
模板方法模式的实现
- 模板方法 (Template Method)—实现整体操作
- 基本方法 (Primitive Method)—实现基本操作步骤
- 抽象方法(Abstract Method)
父类中声明,必须在子类中实现 - 具体方法(Concrete Method)
父类中给出实现,子类可以覆盖,也可以直接继承使用 - 钩子方法(Hook Method)
- 与“具体步骤”挂钩,子类可以覆盖,返回值为bool
- 空方法,子类覆盖,实现IOC
3. 模板方法模式的代码实现
钩子方法
……
//模板方法
public void TemplateMethod()
{
Open();
Display();
//通过钩子方法来确定某步骤是否执行
if (IsPrint())
{
Print();
}
}
//钩子方法—返回bool,可以在模板方法中起到选择的作用,子类可以重写
public bool IsPrint()
{
return true; //如果不需要执行Print(),则可以重写此方法,并返回false
}
……
抽象类典型代码:
abstract class AbstractClass
{
//模板方法
public void TemplateMethod()
{
PrimitiveOperation1();
PrimitiveOperation2();
PrimitiveOperation3();
}
//基本方法—具体方法,一般不需要子类重写
public void PrimitiveOperation1()
{
//实现代码
}
//基本方法—抽象方法—子类中必须重写
public abstract void PrimitiveOperation2();
//基本方法—钩子方法(空方法)—子类中可以重写—不重写的话则不执行该功能
public virtual void PrimitiveOperation3()
{ }
}
具体子类典型代码:
class ConcreteClass : AbstractClass
{
public override void PrimitiveOperation2()
{
//实现代码
}
public override void PrimitiveOperation3()
{
//实现代码
}
}
4. 模版方法模式使用原则
- 将行为尽量移动到结构的高端(抽象类),而将状态尽量移动到结构的低端(具体类)。
应当根据行为而不是状态定义一个类。- 也就是说,一个类的实现首先建立在行为的基础之上,而不是建立在状态的基础之上。
在实现行为时,尽量使用抽象状态而不是用具体状态。 - 如果一个行为涉及到对象的状态时,使用间接的引用而不是直接的引用,换言之,应当使用取值方法而不是直接引用属性。
给操作划分层次。 - 一个类的行为(反映该类功能/职责的核心方法)应当放到一组核心方法(Kernel Methods)里面,这些方法可以很方便地在子类中加以置换–override。
将状态属性的确认推迟到子类中。 - 不要在抽象类中过早地声明属性变量,应将它们尽量地推迟到子类中去声明。在抽象父类中,如果需要状态属性的话,可以调用抽象的取值方法,而将抽象的取值方法的实现放到具体子类中–状态变量及其存取方法都应该尽量放到子类中。
- 也就是说,一个类的实现首先建立在行为的基础之上,而不是建立在状态的基础之上。
5. 模板方法模式的总结
- Template Method模式是一种非常基础性的设计模式,在面向对象系统中有着大量的应用。它用最简洁的机制(虚函数的多态性)为很多应用程序框架提供了灵活的扩展,是代码复用方面的基本实现结构。
- “Don‘t call me, let me call you(不要调用我,让我来调用你)”的反向控制结构(IoC)是Template Method的典型应用。
- 通过将不变行为移到父类中,去除子类中的重复代码,从而达到代码复用的目的。
6.例子
namespace TemplateMethodSample{
// 模板类
abstract class Account{
//基本方法——具体方法
public bool Validate(string account, string password) {
Console.WriteLine("账号:{0}", account);
Console.WriteLine("密码:{0}", password);
//模拟登录验证
if (account.Equals("张无忌") && password.Equals("123456")) {
return true;
}
else {return false; }
}
//基本方法——抽象方法
public abstract void CalculateInterest();
//基本方法——具体方法--子类可以替代--此例中将利息计算过程与利息计算结果分离,可以不替代
public void Display() {
Console.WriteLine("显示利息!");
}
//模板方法
public void Handle(string account, string password) {
if (!Validate(account,password)) { //具体方法
Console.WriteLine("账户或密码错误!");
return;
}
CalculateInterest(); //抽象方法
Display(); //具体方法
}
}
}
namespace TemplateMethodSample{
// 活期账户类--具体子类
class CurrentAccount : Account{
//覆盖父类的抽象基本方法
public override void CalculateInterest() {
Console.WriteLine("按活期利率计算利息!");
}
}
}
namespace TemplateMethodSample{
// 定期账户类--具体子类
class SavingAccount : Account{
//覆盖父类的抽象基本方法
public override void CalculateInterest() {
Console.WriteLine("按定期利率计算利息!");
}
}
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="subClass" value="TemplateMethodSample.CurrentAccount"/>
</appSettings>
</configuration>
namespace TemplateMethodSample{
class Program{
static void Main(string[] args){
Account account;
//读取配置文件
string subClassStr = ConfigurationManager.AppSettings["subClass"];
//反射生成对象
account = (Account)Assembly.Load("TemplateMethodSample").CreateInstance(subClassStr);
account.Handle("张无忌", "123456"); //调用模板方法
}
}
}
namespace TemplateMethodSample{
// 数据图表显示类--抽象模板类
abstract class DataViewer{
//抽象方法:获取数据
public abstract void GetData();
//具体方法:转换数据--这里试图将所有不同格式,统一转换为xml格式--也可以在子类中实现格式转换
public void ConvertData() {
Console.WriteLine("将数据转换为XML格式。");
}
//抽象方法:显示数据
public abstract void DisplayData();
//钩子方法:判断是否为XML格式的数据
public virtual bool IsNotXMLData() {
return true; //默认需要进行数据格式转换
}
//模板方法
public void Process(){
GetData();
//如果不是XML格式的数据则进行数据转换--利用钩子函数改变执行流程
if (IsNotXMLData())
{
ConvertData();
}
DisplayData();
}
}
}
namespace TemplateMethodSample
{
/// <summary>
/// 其他格式--具体模板类
/// </summary>
class OtherDataViewer : DataViewer
{
//实现父类方法:获取数据
public override void GetData()
{
Console.WriteLine("从其他格式文件中获取数据。");
}
//实现父类方法:显示数据
public override void DisplayData()
{
Console.WriteLine("以饼状图显示数据。");
}
}
}
namespace TemplateMethodSample
{
/// <summary>
/// XML格式--具体模板类
/// </summary>
class XMLDataViewer : DataViewer
{
//实现父类方法:获取数据
public override void GetData()
{
Console.WriteLine("从XML文件中获取数据。");
}
//实现父类方法:显示数据
public override void DisplayData()
{
Console.WriteLine("以柱状图显示数据。");
}
//覆盖父类的钩子方法--表明不需要格式转换
public override bool IsNotXMLData()
{
return false;
}
}
}
namespace TemplateMethodSample
{
class Program
{
static void Main(string[] args)
{
DataViewer dv;
//dv = new XMLDataViewer();
dv = new OtherDataViewer();
dv.Process();
Console.Read();
}
}
}