系列文章
[Head First设计模式]山西面馆中的设计模式——装饰者模式
[Head First设计模式]山西面馆中的设计模式——观察者模式
[Head First设计模式]山西面馆中的设计模式——建造者模式
[Head First设计模式]饺子馆(冬至)中的设计模式——工厂模式
[Head First设计模式]抢票中的设计模式——代理模式
引言
第一天上班,没什么任务,就学习了下模版方法模式,这里也是现学现卖,模版方法给我的感觉是似曾相识,总感觉用过,而当时并不知道是模版方法,挺悲催的。年后第一天,吃饭是个大问题,好不容易找到一个米线馆,人非常的多,只能边等边思考模版方法模式了,跟以前一样,对于吃货来说,只有将知识和吃联系在一起,才能记得更牢。
模版方法模式是最为常见的几个模式之一,模版方法模式需要开发抽象类和具体子类的设计师之间的写作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师负责给出这个算法的各个逻辑步骤。
继承常常作为功能复用的主要工具,这时继承有被滥用的危险。所以,我们有一个设计原则:多用组合,少用继承
继承
是不是继承就根本不应该使用呢?事实上对数据的抽象、继承、封装和多态是面向对象语言的最重要特性。继承不应当被滥用,并不意味着继承根本就不该使用。在GoF书中,绝大多数模式是将依赖于继承的实现转换为基于对象的组合和聚合来实现的。模版方法模式是很少用继承来实现的模式中的一个!而且模版方法模式:鼓励恰当的使用继承。此模式可以用来改写一些拥有相同功能的相关类,将可复用的一般性的行为代码移到基类里面,而把特殊化的行为代码移到子类里面。熟悉模版方法模式便成为一个重新学习继承的好地方。
书中的例子
咖啡因饮料
咖啡类
1 public class Coffee 2 { 3 public void PrepareRecipe() 4 { 5 BoilWater(); 6 BrewCoffeeGrinds(); 7 PourInCup(); 8 AddSugarAndMilk(); 9 } 10 //每个方法都实现算法中的一个步骤:煮沸水,冲泡咖啡,把咖啡倒进杯子,加糖和牛奶 11 public void BoilWater() 12 { 13 14 Console.WriteLine("Boiling water"); 15 } 16 public void BrewCoffeeGrinds() 17 { 18 Console.WriteLine("Dripping Coffee through filter"); 19 } 20 public void PourInCup() 21 { 22 Console.WriteLine("Pouring into cup"); 23 } 24 public void AddSugarAndMilk() 25 { 26 27 Console.WriteLine("Adding Sugar and Milk"); 28 } 29 }
茶类
1 public class Tea 2 { 3 public void PrepareRecipe() 4 { 5 BoilWater(); 6 //第二步和第四步与咖啡的实现不同,其他的都一样 7 SteepTeaBag(); 8 PourInCup(); 9 AddLemon(); 10 } 11 //每个方法都实现算法中的一个步骤:煮沸水,冲泡咖啡,把咖啡倒进杯子,加糖和牛奶 12 public void BoilWater() 13 { 14 15 Console.WriteLine("Boiling water"); 16 } 17 //泡茶专用 18 public void SteepTeaBag() 19 { 20 Console.WriteLine("Steeping the tea"); 21 } 22 public void PourInCup() 23 { 24 Console.WriteLine("Pouring into cup"); 25 } 26 //泡茶专用 27 public void AddLemon() 28 { 29 30 Console.WriteLine("Adding Lemon"); 31 } 32 }
第一版设计
星巴兹咖啡和茶冲泡的分析
星巴兹咖啡和茶冲泡采用了相同的算法:
- 把水煮沸
- 用热水泡咖啡或茶
- 把饮料倒进杯子
- 在饮料内加入适当的调料
- 抽象PrepareRecipe()
1、我们遇到的问题是:茶使用SteepTeaBag()和AddLemon()方法,而咖啡使用BrewCoffeeGrinds()和AddSugarAndMilk()方法。
2、无论是咖啡的冲泡,还是茶的浸泡,都是用沸水泡,我们给它一个新的方法名称,比如说Brew()。同样,无论是咖啡加糖和牛奶,还是茶加柠檬,都是加调料,我们也给它一个新的方法名称AddCondiments()。这样,新的prepareRecipe()方法看起来就象这样:
1 public void prepareRecipe() 2 { 3 BoilWater(); 4 Brew(); 5 PourInCup(); 6 AddCondiments(); 7 }
3、CaffeineBeverage(咖啡因饮料)超类:
1 public abstract class CaffeineBeverage 2 { 3 public void PrepareRecipe() 4 { 5 BoilWater(); 6 //步骤2和步骤4被泛化为Brew()和AddCondiments()。 7 Brew(); 8 PourInCup(); 9 AddCondiments(); 10 } 11 //这两个方法声明为抽象的,是因为咖啡和茶的做法不同,需要子类实现。 12 public abstract void Brew(); 13 public abstract void AddCondiments(); 14 public void BoilWater() 15 { 16 Console.WriteLine("Boiling water"); 17 } 18 public void PourInCup() 19 { 20 Console.WriteLine("Pouring into cup"); 21 } 22 23 }
4、咖啡和茶都依赖于超类(咖啡因饮料)处理冲泡法。
1 public class Coffee : CaffeineBeverage 2 { 3 public override void Brew() 4 { 5 Console.WriteLine("Dripping Coffee through filter"); 6 } 7 8 public override void AddCondiments() 9 { 10 Console.WriteLine("Adding Sugar and Milk"); 11 } 12 }
1 public class Tea : CaffeineBeverage 2 { 3 public override void Brew() 4 { 5 Console.WriteLine("Steeping the tea"); 6 } 7 8 public override void AddCondiments() 9 { 10 Console.WriteLine("Adding Lemon"); 11 } 12 }
我们做了什么?
模版方法分析
刚刚实现的就是模板方法模式。模板方法定义了一个算法步骤,并允许子类为一个或多个步骤提供实现。我们再看看咖啡因饮料类的结构(下页)。
模板方法如何工作(以泡茶为例)
模版方法模式的定义
定义一个操作中算法的骨架,而将这些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。
模板方法模式类图
钩子(Hook)
钩子是一种声明为抽象类的方法,但只有空的或默认的实现。有了钩子,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。
1 public abstract class AbstractClass 2 { 3 //细节或略...... 4 void Hook(){}//这是一个抽象类中不做任何事情的具体方法,即钩子 5 }
对模版方法挂钩
1 public abstract class CaffeineBeverageWithHook 2 { 3 public void PrepareRecipe() 4 { 5 BoilWater(); 6 //步骤2和步骤4被泛化为Brew()和AddCondiments()。 7 Brew(); 8 PourInCup(); 9 //有了钩子,能决定要不要覆盖方法,如果不提供自己的方法,抽象类会提供一个默认的实现 10 if (CustomerWantsCondiments()) 11 { 12 AddCondiments(); 13 } 14 15 } 16 //这两个方法声明为抽象的,是因为咖啡和茶的做法不同,需要子类实现。 17 public abstract void Brew(); 18 public abstract void AddCondiments(); 19 public void BoilWater() 20 { 21 Console.WriteLine("Boiling water"); 22 } 23 public void PourInCup() 24 { 25 Console.WriteLine("Pouring into cup"); 26 } 27 /// <summary> 28 /// 这是一个钩子,子类可以覆盖这个方法,但不一定这么做 29 /// </summary> 30 /// <returns></returns> 31 public virtual bool CustomerWantsCondiments(){ 32 return true; 33 } 34 }
使用钩子
为了使用钩子,我们在子类中覆盖它。在这里,钩子控制咖啡因饮料是否执行某部分算法。或更确切的说是在饮料中要不要加进调料。
public class CoffeeWithHook : CaffeineBeverageWithHook { public override void Brew() { Console.WriteLine("Dripping Coffee through filter"); } public override void AddCondiments() { Console.WriteLine("Adding Sugar and Milk"); } public override bool CustomerWantsCondiments() { string answer = GetUserInput(); //覆盖了钩子。让用户输入对调料的决定。 if (answer.ToLower() == "y") { return true; } else { return false; } } private string GetUserInput() { string answer = null; Console.WriteLine("Would you like milk and sugar with your coffee (y/n)? "); answer = Console.ReadLine(); return answer; } }
测试
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 CoffeeWithHook coffeeHook = new CoffeeWithHook(); 6 Console.WriteLine("Making coffee......"); 7 coffeeHook.PrepareRecipe(); 8 Console.ReadLine(); 9 } 10 }
结果
好莱坞原则
好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你。
好莱坞原则可以防止“依赖腐败”。当高层组件依赖底层组件,底层组件又依赖高层组件,高层组件又依赖边侧组件,边侧组件又依赖高层组件......,依赖腐败就发生了。在这种情况下,没有人可以轻易搞懂系统是如何设计的。
在好莱坞原则下,允许底层组件挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些底层组件。即高层组件对底层组件的方式是:“别调用我们,我们会调用你”。
好莱坞原则与模版方法
米线馆的例子
云南米线分为:秀才米线,举人米线,状元米线等。但是他们的制作过程基本相同,只是配料不同罢了,同样可以将制作过程放在模版方法中。
1 /// <summary> 2 /// 米线类 3 /// </summary> 4 public abstract class RiceNoodle 5 { 6 public void MakeRiceNoodle() 7 { 8 9 Boil(); 10 AddRiceNoodle(); 11 AddGreensAndMeat(); 12 } 13 private void Boil() { 14 15 Console.WriteLine("煮水......"); 16 } 17 private void AddRiceNoodle() 18 { 19 Console.WriteLine("将米线加入砂锅中煮....."); 20 } 21 /// <summary> 22 /// 加配菜 一荤一素或者两荤两素等 23 /// </summary> 24 public abstract void AddGreensAndMeat(); 25 }
1 public class XiucaiNoodle:RiceNoodle 2 { 3 public override void AddGreensAndMeat() 4 { 5 Console.WriteLine("一荤一素"); 6 } 7 }
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Console.WriteLine("秀才米线......"); 6 XiucaiNoodle xiucai = new XiucaiNoodle(); 7 xiucai.MakeRiceNoodle(); 8 Console.ReadLine(); 9 } 10 }
结果
总结
模版方法理解起来还是比较轻松的,在项目中真的用到过,只是当时不知道它还有个漂亮的名字。
参考书:
Head First 设计模式