代码改变世界

设计模式初学者系列-模板方法

2007-10-09 09:04  横刀天笑  阅读(4727)  评论(15编辑  收藏  举报
本系列文章目录   本文章首发在IT168技术频道,有修改 点击这里查看

模板方法的定义
 
在微软的WebCast上,李建中老师有个设计模式的系列讲座,其中在模板方法说到:如果你只想学习一种设计模式就学习模板方法吧。那我们就来瞧瞧这个大众设计模式是什么样子的。
要说起模板方法就要从她所从属的分类开始说起。
     
模板方法属于行为型设计模式,行为型设计模式主要关注对象之间职责分配和算法的问题。类行为型模式使用继承来分配类之间的职责,模板方法就是个类行为型模式。对象行为型模式使用组合来分配职责。在我们构建软件的过程中大部分时候我们都是在思考实体之间的职责,怎样的职责分配最合理,不至于过重,又不至于过轻,而且又不越权。
      
模板方法Gof的定义是:在一个方法里定义算法的骨架,将一些步骤延迟到其子类。模板方法使得子类有机会重新定义算法的某些步骤而不改变算法的结构。模板方法实际上是利用多态这种晚绑定机制来将一些执行系列延迟到子类(运行时)。如下图:


       其中父类Application中的DoSomething()方法就是一个模板方法。模板方法顾名思义就是定义一个模板或框架,这个框架里调用了其他两个方法Operation1和Operation2,这两个方法是虚方法或者抽象方法以允许子类MyApplication有机会覆盖掉其定义。父类仅仅作一些抽象性的工作,而具体的事情由子类来完成。

模板方法的应用

    我们来作这样一个假设:教育部规定了高校新生报到的流程(这好比写好了算法的骨架),流程为 凭录取通知书教务处报到->缴费->本院系报到(获取自己的专业班级信息)->教材科发教材。

 1public class 高校
 2{
 3//这就是一个模板方法,定义了算法的骨架
 4    public void 报到()
 5{
 6//这个执行步骤要是中间还穿插些情节就更完美了,现在姑且这样吧
 7        教务处报到();
 8缴费();
 9本院系报到();
10教材科发教材();
11}

12protected abstract void 教务处报到();
13protected abstract void 缴费();
14protected abstract 专业等信息 本院系报到();
15protected abstract 教材 教材科发教材();
16}
     但是各个具体的院校因为自己的情况对这个步骤作进一步补充:清华大学(该校信息技术及其发达,处处透露出信息化气息)。清华大学的学生领取的通知书是一张磁卡,学生拿着磁卡到教务处报到只需要去刷卡即可,该卡还具有银行支付功能,学生只需要向该卡转入学费就可以完成缴费,但是必须是先到教务处报到后该卡才生效。学生缴费以后由电子系统自动到学生院系报到并通过手机短信方式将学生专业班级信息发送给学生,完成后由学校的物流配送系统将学生教材送到学生寝室。
 1public class 清华大学 : 高校
 2{
 3protected override void 教务处报到()
 4{
 5//刷卡
 6}

 7protected override void 缴费()
 8{
 9//银行卡转账
10}

11protected override 专业等信息 本院系报到()
12{
13//系统自动将学生信息转入到院系信息系统
14//系统将学生专业班级信息发送到学生手机
15    return 手机短信方式的专业等信息
16}

17protected override 教材 教材科发教材()
18{
19//学校物流配送系统送教材
20return 教材
21}

22}

23

    北京大学(该校认为作为有深厚文化底蕴的高校,应该注重人文气息,不能用冷冰冰的电子设备代替人工,所以该校报到过程全部人工化)该校通知书为普通纸片,到教务处报到需要学生签字处长签字,然后到财务处缴纳学费,然后到院系见书记一面,给了你一张有专业班级等信息的卡片,最后拿个袋子到教材科装教材回寝室了。
 1public class 北京大学
 2{
 3protected override void 教务处报到()
 4{
 5//学生签字
 6//教务处处长签字
 7//报到完毕
 8}

 9protected override void 缴费()
10{
11//到财务处缴纳现金
12}

13protected override 专业等信息 到本院系报到()
14{
15//见到院书记
16    return 纸片方式的专业等信息
17}

18protected override 教材 教材科发教材()
19{
20//自备袋子到教材科
21    return 教材
22}

23}

24

    以上是两个高校针对教育部颁发的新生报到算法作出自己具体的实现。那么全国有这么多高校又有多少种实现呢?让教育部分别给每所高校规定新生报到算法,这样是不现实的,如果教育部给所有高校规定同一个样子的报到流程就失去了灵活性,毕竟要考虑实际嘛。


    通过上面的描述是不是觉得模板方法好像是:上有政策,下有对策?对,模板方法就是上面的这个政策,各个下级有自己对政策中具体步骤的实现。模板方法就像我们的宪法一样,对其他所有法律指定了一些大的框架,要制订新的法律必须依照这个框架。

现实中的模板方法

    有人说上面这个DEMO太不符合实际。实际上在现实中中模板方法有很多应用,相信大家都有使用模板方法的经历。特别是针对哪些框架程序员,他们既不能规定死框架,又不能什么都不规定。比如.net中的WinForm:窗体的初始化、显示、消息循环都有固定的过程,但是不同的窗体上面有不同的控件,控件有自己具体的参数设置。.net框架的实现者们就规定好这些框架:一个WinForm窗体如何初始化,如何显示。我们这些.net框架的使用者(二手程序员)就可以通过覆盖掉其中的一些步骤来丰富自己的窗体了。有用C/C++写过调用Win32API作窗体的一定知道,一个啥也没有的空窗体就要百几十行的代码,实际上我们发现其中很多代码是重复的,整个代码流程也很类似,我觉得写框架程序的过程就是一个模板方法提取的过程。

下面举一个前几天在群里讨论的一个问题:

 1private void Page_Load(object sender, System.EventArgs e)
 2{
 3//取出资源文件
 4        rm = (ResourceManager)Application["RM"];
 5// 在此处放置用户代码以初始化页面
 6        int iLen = MyUICulture.SelectedItem.Text.IndexOf(" ">= 0
 7? MyUICulture.SelectedItem.Text.IndexOf(" ") : MyUICulture.SelectedItem.Text.Length;
 8String SelectedCulture = MyUICulture.SelectedItem.Text.Substring(0, (8 <= iLen ? 8 : iLen));
 9if (!SelectedCulture.StartsWith("Choose"))
10{
11// If another culture was selected, use that instead.
12            Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(SelectedCulture);
13Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
14}

15this.labCol.Text = rm.GetString("article1");
16}

17

    要实现一个国际化的页面,该页面上有一个DropDownList控件MyUICulture,用户可以用该控件选择页面使用的语言。选择后页面上所有控件的Text属性将使用资源文件中的语言字符串进行初始化,并将线程语言特性设为选择的语言。但是碰到一个问题:这个设置语言的过程所有页面流程是一样的,但是不同的页面里面的控件不一样,需要分别初始化,不想每个页面拷贝想同的代码,如何解决这个问题?聪明,就是模板方法模式。我们将这个选择语言的方法作为一个模板方法,而不同的页面里面的初始化控件Text属性放在另外一个虚方法中。将这两个方法放到一个BasePage的父类中,然后所有的页面继承这个BasePage,各个页面可以重新定义这个虚方法。来,我们来实现我们的想法:
 1public class BasePage : Page
 2………………….
 3private void Page_Load(object sender, System.EventArgs e)
 4{
 5//调用选择语言方法
 6        SelectLanguage();
 7}

 8//这是一个虚方法,子类可以覆盖该方法完成自己页面上控件Text属性的初始化
 9    private virtual void SetControlText()
10{
11}

12//选择语言方法,该方法为一个模板方法,调用SetControlText完成自己的算法。
13    private void SelectLanguage()
14{
15//取出资源文件
16        rm = (ResourceManager)Application["RM"];
17// 在此处放置用户代码以初始化页面
18        int iLen = MyUICulture.SelectedItem.Text.IndexOf(" ">= 0
19? MyUICulture.SelectedItem.Text.IndexOf(" ") : MyUICulture.SelectedItem.Text.Length;
20String SelectedCulture = MyUICulture.SelectedItem.Text.Substring(0, (8 <= iLen ? 8 : iLen));
21if (!SelectedCulture.StartsWith("Choose"))
22{
23// If another culture was selected, use that instead.
24            Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(SelectedCulture);
25Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
26}

27//调用设置自己页面控件的Text属性的方法
28        SetControlText();
29}

30

下面是某个实现了BasePage的页面:
1public class MyPage : BasePage
2.
3public override void SetControlText()
4{
5this.labCol.Text =rm.GetString("article1");
6}

模板方法和其他模式的联系

    各个模式之间都有联系,模板方法也不例外,她并不是孤立存在的。模板中的那些虚方法实际上都是使用工厂方法设计模式,将父类的执行逻辑延迟到子类。有的时候模板方法里定义算法的步骤会用到策略模式,因为有的时候这个算法不止一种,比如上面的教育部规定新生报到流程这个算法,有可能教育部规定了三四种,那么我们就可以用策略模式封装这几套算法。

感想

    学习模式好久了,总是一种模模糊糊的感觉,现在只体会到一点:在开始一个设计之前不要就想着使用那个那个模式,不要一个个的往上套,模式是用来解决问题的,不是用来设计的,在编码阶段,如果你碰到难以解决的问题,比如耦合度太高啊,很难添加新特性了,那么你就应该回过头来重构你的程序,这个时候模式就为你指明了重构的方向。如果你的设计没有出现问题,你的编码没有出现任何困难,你的程序很容易添加新特性也很好维护,那么就不要引入模式了,并不是你的程序用到了模式你的程序就是好的。

帖子发了几篇了,也得到了很多人的反馈,甚是感到。反馈有好的,有坏的,我在这里都接受,这是我学设计模式做的笔记,都是写了好久了,现在发出来,也代表我自己学习的路吧。