设计模式之模板方法模式

“在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。”

         模板方法模式主要解决的是写了大量重复代码的情况。两个方法完成的功能类似,代码逻辑也相似,只是内部有些具体实现细节有差异。这种情况就适合用模板方法模式来解决。下面以《Head First 设计模式》中的具体例子来讲解该模式的使用。

        假如说我现在有一个新需求,煮咖啡和泡茶。相关步骤如上图。可以看到,分别有四步操作。最简单的做法就是写两个方法,分别完成这四步就行了。这种方式实现起来最easy,但是代码质量最糟糕。写了很多重复代码不提,假如说我现在第一步不用水煮沸了,改用别的饮料煮沸,那么这两个方法都得进行修改,耦合度高。

        稍微好一点的做法是定义一个基类,提出共同的部分放进基类里。由上可以看到煮咖啡和泡茶的第一步和第三步是完全一样的,都是把水煮沸和倒进杯子里。所以把这两个方法放进基类里,子类去实现第二步和第四步。类图如下:

        这里的prepareRecipe方法暂且可以忽略。它只是一个总控方法,由它来去调用一二三四步骤,相当于main方法。

        我们已经优化过了,这种方式也是在实际项目开发中使用最多的方式,但是这种方式是否足够好?是否已经优化到底了?答案肯定是否定的,这种方式并没有考虑到不同部分的相同性。即使第二步和第四步不同,它们之间也是有逻辑相似的点。接下来我们进一步优化,抽象出来一个咖啡因饮料的类,如下:

        可以看到第二步和第四步也可以进行抽象,模板方法模式的具体代码如下:

        首先定义一个抽象基类CaffeineBeverage。prepareRecipe方法即为模板方法,即骨架。一般定义成final类型,防止子类去覆写该方法。煮咖啡和泡茶的第一步和第三步是完全一样的,所以在抽象类里直接实现,而不同的第二步和第四步做成抽象方法,交给子类具体去实现。这也就是定义中说的延迟到子类来实现的概念。下面来看一下具体的咖啡和茶类的实现代码:

        可以看到子类只要实现父类的抽象方法就可以了,只要把变化、不同的部分实现了就行,不需要再进行别的额外编写,算法逻辑已经在抽象基类中实现了。使用了模板方法模式的好处如下,读者可自行体会。 

        接下来要讲的是一个在模板方法模式中使用的小技巧,对模板方法模式进行挂钩。
“钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。”

        假如说现在我有了一个新需求,制作一种新咖啡,前三步都跟之前的煮咖啡一样,只是最后一步,这种咖啡不需要添加任何调料。那么我们就可以用钩子来实现这种需求。直接给出代码实现:

        红色部分即为改动部分,customerWantsCondiments方法即为钩子。在基类中是return true,如果return true才执行添加调料的动作。

        在子类中覆写了该方法,变成了return false。所以在制作这种咖啡的时候,在进行第四步操作时,就不会执行addCondiments添加调料的方法了。
        但是通过笔者的理解,这种方式仅适合减少步骤的需求。假如我现在想在一种新式咖啡中添加第五步操作:加冰块。那么使用钩子则会很尴尬(真正的尴尬点在于使用了模板方法模式)。尴尬点在于这第五步操作只是适合这种咖啡的,其他的饮料不会有这步操作。
<1>一种解决方法是在这种新式咖啡本身这个子类中去实现第五步操作。但这样的话就违背了使用模板方法模式的大前提:通过调用抽象基类的模板方法(骨架),就可以完成所有的操作逻辑。这样也违背了针对接口编程而不针对实现编程的原则。
<2>另一种解决办法是第五步方法仍然做成抽象方法,这就意味着每种实现了该抽象类的子类都得实现该抽象方法,哪怕它没有第五步操作。
<3>还一种解决办法是第五步操作就做成一个普通方法,让新式咖啡去实现该方法。这样的话其他的子类不需要去实现该方法。
        后两种方案各有利弊,前者确保了子类一定要去实现抽象类中变化的方法这一前提,但却使子类做了不该这个子类做的功能;后者则正好相反,如果新式咖啡没有去实现第五步操作,不会报编译时异常,但在运行时则会生成错误的结果。笔者倾向于后一种解决方案,在实际项目中也是这么做的。即宁愿牺牲子类一定要去实现抽象类中变化的方法这一前提,也要保证每个类的高内聚性。解决方案本身就是不断取舍的过程,需要我们考虑到各种情况,选择一种较好的方式解决。
        在一些框架的设计中,也可以看到模板方法模式的存在。例如在spring mvc的View设计中。下面取自《深入分析Java Web技术内幕(修订版)》中的原话:

“View只定义了接口方法,AbstractView类实现了在View中定义的所有方法,并留有一个抽象方法renderMergedOutputModel给子类去实现。而AbstractJasperReportsView和AbstractTemplateView抽象类又进一步实现了AbstractView的抽象方法renderMergedOutputModel,并分别进一步细化出renderReport抽象方法和renderMergedTemplateModel给子类去进一步实现。越往下面的子类需要实现的功能越少,整个模板已经建立,所以模板模式能加快整个程序的开发进度。”