设计模式 - 吕震宇
.NET设计模式系列文章
薛敬明的专栏
乐在其中设计模式(C#)
准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模版方法模式的用意。
很多人可能没有想到,模版方法模式实际上是所有模式中最为常见的几个模式之一,而且很多人可能使用过模版方法模式而没有意识到自己已经使用了这个模式。模版方法模式是基于继承的代码复用的基本技术,模版方法模式的结构和用法也是面向对象设计的核心。
模版方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。代表这些具体逻辑步骤的方法称做基本方法(primitive method);而将这些基本法方法总汇起来的方法叫做模版方法(template method),这个设计模式的名字就是从此而来。
模版方法模式的静态结构如下图所示。
这里涉及到两个角色:
定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。
定义并实现了一个模版方法。这个模版方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
每一个抽象模版角色都可以有任意多个具体模版角色与之对应,而每一个具体模版角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
使用继承作为复用的手段必须慎重,C#语言的设计师对使用继承作为复用的工具有着不同层次上的认识。
不知其一
首先,初学C#的程序员可能不知道什么是继承,或者认为"继承"是高深的工具。那时候,大部分的功能复用都是通过委派进行的。
知其一、不知其二
然后慢慢地,他们发现在C#语言里实现继承并不困难,并且初步认识到继承可以使子类一下子得到基类的行为。这时他们就会跃跃欲试了,试图使用继承作为功能复用的主要工具,并把原来应当使用委派的地方,改为使用继承,这时继承就有被滥用的危险。
知其二
很多面向对象的设计专家从1986年就开始警告继承关系被滥用的可能。有一些面向对象的编程语言,如SELF语言,甚至将类的继承关系从语言的功能中取消掉,改为完全使用委派。
其他的设计师虽然不提倡彻底取消继承,但无一例外地鼓励在设计中尽可能使甩委派关系代替继承关系。比如在【GOF95】一书中,状态模式、策略模式、装饰模式、桥梁模式以及抽象工厂模式均是将依赖于继承的实现转换为基于对象的组合和聚合的实现,这些模式的要点就是使用委派关系代替继承关系。
知其三
是不是继承就根本不该使用呢?事实上对数据的抽象化、继承、封装和多态性并称C#和其他绝大多数的面向对象语言的几项最重要的特性。继承不应当被滥用,并不意味着继承根本就不该使用。因为继承容易被滥用就彻底抛弃继承,无异于因噎废食。
继承使得类型的等级结构易于理解、维护和扩展,而类型的等级结构非常适合于抽象化的设计、实现和复用。尽管【GOF95】所给出的设计模式基本上没有太多基于继承的模式,很多模式都是用继承的办法定义、实现接口的。多数的设计模式都描写一个以抽象类作为基类,以具体类作为实现的等级结构,比如适配器模式、合成模式、桥梁模式、状态模式等。
模版方法模式则更进了一步:此模式鼓励恰当地使用继承。此模式可以用来改写一些拥有相同功能的相关的类,将可复用的一般性的行为代码移到基类里面,而把特殊化的行为代码移到子类里面。
因此,熟悉模版方法模式便成为一个重新学习继承的好地方。
下面的例子演示了数据库访问的模板方法。实际应用时,请确保C盘根目录下有nwind.mdb这个Access数据库(可以从Office的安装目录下找到。中文版用户的请注意字段名可能有所不同)。
模版方法中的方法可以分为两大类:模版方法(Template Method)和基本方法(Primitive Method)。
模版方法
一个模版方法是定义在抽象类中的,把基本操作方法组合在一起形成一个总算法或一个总行为的方法。这个模版方法一般会在抽象类中定义,并由子类不加以修改地完全继承下来。
基本方法
基本方法又可以分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。
抽象方法:一个抽象方法由抽象类声明,由具体子类实现。在C#语言里一个抽象方法以abstract关键字标示出来。
具体方法:一个具体方法由抽象类声明并实现,而子类并不实现或置换。在C#语言里面,一个具体方法没有abstract关键字。
钩子方法:一个钩子方法由抽象类声明并实现,而子类会加以扩展。通常抽象类给出的实现是一个空实现,作为方法的默认实现。(Visual FoxPro中项目向导建立的项目会使用一个AppHook类实现监视项目成员变化,调整系统结构的工作。)钩子方法的名字通常以do开始。
在对一个继承的等级结构做重构时,一个应当遵从的原则便是将行为尽量移动到结构的高端,而将状态尽量移动到结构的低端。
1995年,Auer曾在文献【AUER95】中指出:
如果能够遵从这样的原则,那么就可以在等级结构中将接口与实现分隔开来,将抽象与具体分割开来,从而保证代码可以最大限度地被复用。这个过程实际上是将设计师引导到模版方法模式上去。
参考文献:
阎宏,《Java与模式》,电子工业出版社
[美]James W. Cooper,《C#设计模式》,电子工业出版社
[美]Alan Shalloway James R. Trott,《Design Patterns Explained》,中国电力出版社
[美]Robert C. Martin,《敏捷软件开发-原则、模式与实践》,清华大学出版社
[美]Don Box, Chris Sells,《.NET本质论 第1卷:公共语言运行库》,中国电力出版社
摘要:Template Method模式是比较简单的设计模式之一,但它却是代码复用的一项基本的技术,在类库中尤其重要。
主要内容
1.概述
2.Template Method解说
3..NET中的Template Method模式
4.适用性及实现要点
概述
变化一直以来都是软件设计的永恒话题,在XP编程中提倡拥抱变化,积极应对。如何更好的去抓住变化点,应对变化?如何更好的提高代码复用?通过学习Template Method模式,您应该有一个新的认识。
意图
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。[-GOF《设计模式》]
结构图
图1 Template Method 模式结构图
生活中的例子
模板方法定义了一个操作中算法的骨架,而将一些步骤延迟到子类中。房屋建筑师在开发新项目时会使用模板方法。一个典型的规划包括一些建筑平面图,每个平面图体现了不同部分。在一个平面图中,地基、结构、上下水和走线对于每个房间都是一样的。只有在建筑的后期才开始有差别而产生了不同的房屋样式。
图2 使用建筑图为例子的Template Method模式
Template Method模式解说
李建忠老师说过一句话,如果你只想掌握一种设计模式的话,那这个模式一定是Template Method模式。对于这个问题,我想可能是仁者见仁,智者见智,但是有一点不能否认的Template Method模式是非常简单而且几乎是无处不用,很少有人没有用过它。下面我们以一个简单的数据库查询的例子来说明Template Method模式(注意:这个例子在实际数据库开发中并没有任何实际意义,这里仅仅是为了作为示例而已)。
假如我们需要简单的读取Northwind数据库中的表的记录并显示出来。对于数据库操作,我们知道不管读取的是哪张表,它一般都应该经过如下这样的几步:
1.连接数据库(Connect)
2.执行查询命令(Select)
3.显示数据(Display)
4.断开数据库连接(Disconnect)
这些步骤是固定的,但是对于每一张具体的数据表所执行的查询却是不一样的。显然这需要一个抽象角色,给出顶级行为的实现。如下图:
图3
Template Method模式的实现方法是从上到下,我们首先给出顶级框架DataAccessObject的实现逻辑:
显然在这个顶级的框架DataAccessObject中给出了固定的轮廓,方法Run()便是模版方法,Template Method模式也由此而得名。而对于Select()和Display()这两个抽象方法则留给具体的子类去实现,如下图:
图4
示意性实现代码:
再来看看客户端程序的调用,不需要再去调用每一个步骤的方法:
在上面的例子中,需要注意的是:
1.对于Connect()和Disconnect()方法实现为了virtual,而Select()和Display()方法则为abstract,这是因为如果这个方法有默认的实现,则实现为virtual,否则为abstract。
2.Run()方法作为一个模版方法,它的一个重要特征是:在基类里定义,而且不能够被派生类更改。有时候它是私有方法(private method),但实际上它经常被声明为protected。它通过调用其它的基类方法(覆写过的)来工作,但它经常是作为初始化过程的一部分被调用的,这样就没必要让客户端程序员能够直接调用它了。
3.在一开始我们提到了不管读的是哪张数据表,它们都有共同的操作步骤,即共同点。因此可以说Template Method模式的一个特征就是剥离共同点。
.NET 中的Template Method模式
.NET Framework中Template Method模式的使用可以说是无处不在,比如说我们需要自定义一个文本控件,会让它继承于RichTextBox,并重写其中部分事件,如下例所示:
其中OnSelectionChanged()和OnTextChanged()便是Template Method模式中的基本方法之一,也就是子步骤方法,它们的调用已经在RichTextBox中实现了。
实现要点
1.Template Method模式是一种非常基础性的设计模式,在面向对象系统中有着大量的应用。它用最简洁的机制(虚函数的多态性)为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本实现结构。
2.除了可以灵活应对子步骤的变化外,“不用调用我,让我来调用你”的反向控制结构是Template Method的典型应用。
3.在具体实现方面,被Template Method调用的虚方法可以具有实现,也可以没有任何实现(抽象方法,纯虚方法),但一般推荐将它们设置为protected方法。[李建忠]
适用性
1.一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
2.各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。这是Opdyke和Johnson所描述过的“重分解以一般化”的一个很好的例子。首先识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
3.控制子类扩展。模板方法只在特定点调用“Hook”操作,这样就只允许在这些点进行扩展。
总结
Template Method模式是非常简单的一种设计模式,但它却是代码复用的一项基本的技术,在类库中尤其重要。
本篇文章写的比较简单,请大家见谅。更多的设计模式文章可以访问《.NET设计模式系列文章》
参考资料
Erich Gamma等,《设计模式:可复用面向对象软件的基础》,机械工业出版社
Robert C.Martin,《敏捷软件开发:原则、模式与实践》,清华大学出版社
阎宏,《Java与模式》,电子工业出版社
Alan Shalloway James R. Trott,《Design Patterns Explained》,中国电力出版社
MSDN WebCast 《C#面向对象设计模式纵横谈(14):Template Method模版方法模式(结构型模式)》
Define:Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模版方法模式的用意。
很多人可能没有想到,模版方法模式实际上是所有模式中最为常见的几个模式之一,而且很多人可能使用过模版方法模式而没有意识到自己已经使用了这个模式。模版方法模式是基于继承的代码复用的基本技术,模版方法模式的结构和用法也是面向对象设计的核心。
模版方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。代表这些具体逻辑步骤的方法称做基本方法(primitive method);而将这些基本法方法总汇起来的方法叫做模版方法(template method),这个设计模式的名字就是从此而来。
模版方法模式的静态结构如下图所示。
这里涉及到两个角色:
定义了一个或多个抽象操作,以便让子类实现。这些抽象操作叫做基本操作,它们是一个顶级逻辑的组成步骤。
定义并实现了一个模版方法。这个模版方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
实现父类所定义的一个或多个抽象方法,它们是一个顶级逻辑的组成步骤。
每一个抽象模版角色都可以有任意多个具体模版角色与之对应,而每一个具体模版角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
使用继承作为复用的手段必须慎重,C#语言的设计师对使用继承作为复用的工具有着不同层次上的认识。
不知其一
首先,初学C#的程序员可能不知道什么是继承,或者认为"继承"是高深的工具。那时候,大部分的功能复用都是通过委派进行的。
知其一、不知其二
然后慢慢地,他们发现在C#语言里实现继承并不困难,并且初步认识到继承可以使子类一下子得到基类的行为。这时他们就会跃跃欲试了,试图使用继承作为功能复用的主要工具,并把原来应当使用委派的地方,改为使用继承,这时继承就有被滥用的危险。
知其二
很多面向对象的设计专家从1986年就开始警告继承关系被滥用的可能。有一些面向对象的编程语言,如SELF语言,甚至将类的继承关系从语言的功能中取消掉,改为完全使用委派。
其他的设计师虽然不提倡彻底取消继承,但无一例外地鼓励在设计中尽可能使甩委派关系代替继承关系。比如在【GOF95】一书中,状态模式、策略模式、装饰模式、桥梁模式以及抽象工厂模式均是将依赖于继承的实现转换为基于对象的组合和聚合的实现,这些模式的要点就是使用委派关系代替继承关系。
知其三
是不是继承就根本不该使用呢?事实上对数据的抽象化、继承、封装和多态性并称C#和其他绝大多数的面向对象语言的几项最重要的特性。继承不应当被滥用,并不意味着继承根本就不该使用。因为继承容易被滥用就彻底抛弃继承,无异于因噎废食。
继承使得类型的等级结构易于理解、维护和扩展,而类型的等级结构非常适合于抽象化的设计、实现和复用。尽管【GOF95】所给出的设计模式基本上没有太多基于继承的模式,很多模式都是用继承的办法定义、实现接口的。多数的设计模式都描写一个以抽象类作为基类,以具体类作为实现的等级结构,比如适配器模式、合成模式、桥梁模式、状态模式等。
模版方法模式则更进了一步:此模式鼓励恰当地使用继承。此模式可以用来改写一些拥有相同功能的相关的类,将可复用的一般性的行为代码移到基类里面,而把特殊化的行为代码移到子类里面。
因此,熟悉模版方法模式便成为一个重新学习继承的好地方。
下面的例子演示了数据库访问的模板方法。实际应用时,请确保C盘根目录下有nwind.mdb这个Access数据库(可以从Office的安装目录下找到。中文版用户的请注意字段名可能有所不同)。
模版方法中的方法可以分为两大类:模版方法(Template Method)和基本方法(Primitive Method)。
模版方法
一个模版方法是定义在抽象类中的,把基本操作方法组合在一起形成一个总算法或一个总行为的方法。这个模版方法一般会在抽象类中定义,并由子类不加以修改地完全继承下来。
基本方法
基本方法又可以分为三种:抽象方法(Abstract Method)、具体方法(Concrete Method)和钩子方法(Hook Method)。
抽象方法:一个抽象方法由抽象类声明,由具体子类实现。在C#语言里一个抽象方法以abstract关键字标示出来。
具体方法:一个具体方法由抽象类声明并实现,而子类并不实现或置换。在C#语言里面,一个具体方法没有abstract关键字。
钩子方法:一个钩子方法由抽象类声明并实现,而子类会加以扩展。通常抽象类给出的实现是一个空实现,作为方法的默认实现。(Visual FoxPro中项目向导建立的项目会使用一个AppHook类实现监视项目成员变化,调整系统结构的工作。)钩子方法的名字通常以do开始。
在对一个继承的等级结构做重构时,一个应当遵从的原则便是将行为尽量移动到结构的高端,而将状态尽量移动到结构的低端。
1995年,Auer曾在文献【AUER95】中指出:
如果能够遵从这样的原则,那么就可以在等级结构中将接口与实现分隔开来,将抽象与具体分割开来,从而保证代码可以最大限度地被复用。这个过程实际上是将设计师引导到模版方法模式上去。