前言
模板方法,属于类行为型模式,是一种提高代码复用性和扩展性的技术。通过将某些类的公共行为封装到一个抽象类中,并由其子类选择性的实现其中的某些行为以改变这个类的行为方式。它的迷人之处在于它是一种反向的控制结构,即父类调用一个子类的操作。
在《设计模式 - 可复用的面向对象软件》一书中将之描述为“ 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤 ”。
结构
- AbstractClass(抽象类):用来定义原语操作和钩子操作并实现模板方法以及封装类的公共行为。在模板方法中执行了封装在抽象类中的公共行为、自身定义的原语操作、钩子操作以及其它对象中的操作;
- ConcreteClass(具体类):继承抽象类,实现抽象类中的原语操作以及选择性重写钩子操作;
注:原语操作(PrimitiveOperation)指的是那些子类必须重写的操作,即抽象函数;
钩子操作(HookOperation)指的是那些子类可以选择性重写的操作(如.NET中的虚函数),通常只提供空操作;
示例
我们尝试着用代码来描述一杯咖啡的制作流程。以意式、美式和拿铁为例,如下。
public class Espresso { public void MakeCoffee() { Console.WriteLine("开始制作:[意式浓缩咖啡]"); Console.WriteLine("加入一份意式浓缩咖啡"); Console.WriteLine("装入30ml的杯子"); Console.WriteLine("制作完毕"); } } public class Americano { public void MakeCoffee() { Console.WriteLine("开始制作:[美式咖啡]"); Console.WriteLine("加入一份意式浓缩咖啡"); Console.WriteLine("加入烧开的纯净水"); Console.WriteLine("装入500ml的杯子"); Console.WriteLine("制作完毕"); } } public class Latte { public void MakeCoffee() { Console.WriteLine("开始制作:[拿铁咖啡]"); Console.WriteLine("加入一份意式浓缩咖啡"); Console.WriteLine("加入热牛奶"); Console.WriteLine("装入500ml的杯子"); Console.WriteLine("制作完毕"); } }
可以看到上述三种咖啡的制作流程基本相同。由于每种咖啡都需要加入一份意式浓缩咖啡,所以这一步可以由抽象类实现。大部分但不是所有咖啡都需要加入水或牛奶,所以这一步可以在抽象类中定义为钩子操作在由子类选择性重写。所有的咖啡都需要装到杯子中,但不同的咖啡需要装入不同容量的杯子中,所以这一步定义为原语操作由子类实现。如下。
public abstract class CoffeTemplate { public void MakeCoffe() { this.AddEspresso(); //加入一是浓缩咖啡 this.AddLiquid(); //加入液体 this.IntoContainer(); //装入容器 } private void AddEspresso() { Console.WriteLine("加入一份意式浓缩咖啡"); } /// <summary> /// 钩子操作,由子类选择性重写 /// </summary> protected virtual void AddLiquid() { } /// <summary> /// 原语操作,由子类实现 /// </summary> protected abstract void IntoContainer(); } public class Espresso : CoffeTemplate { protected override void IntoContainer() { Console.WriteLine("装入30ml的杯子"); } } public class Americano : CoffeTemplate { protected override void IntoContainer() { Console.WriteLine("装入500ml的杯子"); } protected override void AddLiquid() { Console.WriteLine("加入烧开的纯净水"); } } public class Latte : CoffeTemplate { protected override void IntoContainer() { Console.WriteLine("装入500ml的杯子"); } protected override void AddLiquid() { Console.WriteLine("加入热牛奶"); } } static void Main(string[] args) { Console.WriteLine("开始制作:[意式浓缩咖啡]"); CoffeTemplate coffe = new Espresso(); coffe.MakeCoffe(); Console.WriteLine($"制作完毕{Environment.NewLine}"); Console.WriteLine("开始制作:[美式咖啡]"); coffe = new Americano(); coffe.MakeCoffe(); Console.WriteLine($"制作完毕{Environment.NewLine}"); Console.WriteLine("开始制作:[拿铁咖啡]"); coffe = new Latte(); coffe.MakeCoffe(); Console.WriteLine($"制作完毕{Environment.NewLine}"); Console.ReadKey(); }
在抽象类CoffeTemplate中定义了一个模板方法MakeCoffe,该方法先后调用了函数AddEspresso、钩子函数AddLiquid以及原语函数IntoContainer。因为意式浓缩的制作不需要添加液体,所以在子类Espresso中不需要重写虚AddLiquid函数只实现了父类中的IntoContainer函数。而其他子类则根据自身的需要分别实现和重写了IntoContainer、AddLiquid函数。以上就是一个简单的模板方法模式的Demo。
模板方法模式的关键在于它的反向控制结构。有时我们在扩展父类函数时会忘记在子类中调用父类的函数,而这种反向控制结构可以帮助我们避免这种情况的发生。只需要在父类中增加并调用一个钩子函数并由其子类实现即可。就像示例中子类重写的函数AddLiquid就是对CoffeTemplate类中的MakeCoffe函数的扩展。
补充
模板方法模式也叫做模板模式。个人认为这个叫法不大准确。因为在该模式中子类的存在是作为对父类的一种扩展,并且只负责扩展的实现,这些扩展的调用时机则由父类控制。换句话说,在父类的某个公开的函数中调用了一些待实现和可重写的函数等待着子类的实现。这个公开的函数代表着一个业务的基本框架,也就是所谓的模板函数。而父类只是作为这个模板函数的载体。所以叫模板方法更加准确一些。
总结
模板方法模式是一种提高代码复用性和扩展性的技术,它封装了业务中不变的部分,可变的部分则交由子类实现。这样可以在不改变业务结构的前提下重写某些特定的业务细节。但每一个不同的实现都需要新增一个子类,导致系统中类的个数增加,增加了系统的复杂度。
以上,就是我对模板方法模式的理解,希望对你有所帮助。
示例源码:https://gitee.com/wxingChen/DesignPatternsPractice
系列汇总:https://www.cnblogs.com/wxingchen/p/10031592.html
本文著作权归本人所有,如需转载请标明本文链接(https://www.cnblogs.com/wxingchen/p/10271983.html)