参考书——《HeadFirst设计模式》
和书中一样,用冲咖啡和沏茶来说这个设计模式
冲咖啡: 沏茶: 制作饮料:
step1 烧水 烧水 烧水
step2 用沸水冲咖啡粉 用沸水冲茶叶 ===========》 用沸水冲
step3 倒入杯中 倒入杯中 倒入杯中
step4 加糖加奶 加柠檬 加适当的调料
我们看到step1和step3是一样的,换而言之,可以具体实现,step2和step4虽然不一样但是我们可以进行提取,也就是抽象。等等,想到了什么?!抽象类 P.s:抽象类和接口的区别http://dev.yesky.com/436/7581936.shtml
public abstract class Beverage {
final void prepareRecipe() {
boilwater();
brew();
pourIncup();
addCondiments();
}
abstract void brew();
abstract void addCondiments();
void boilwater() {
System.out.println("烧水");
}
void pourIncup() {
System.out.println("倒入杯中");
}
}
模板方法——就是这里的prepareRecipe(),它定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
“炫酷的地方”:1)Beverage类主导一切,它拥有这个算法,并且保护这个算法;
2)对子类来说,Beverage类的存在,可以提高代码的复用;
3)算法存在一个地方,便于维护;
4)模板方法提供了一个框架,可以让其他的饮料插进来,新的饮料只要实现自己的方法就行;
5)Beverage类专注在算法本身,而由子类提供完整的实现。
定义:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
其实,模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中任何步骤都可以是抽象的,由子类负责实现,这可以确保算法的结构保持不变,同时由子类提供部分实现。模板方法本身和这些抽象的原语操作的具体实现之间解耦了。
有了这个抽象类,我们就可以写具体的Coffee和Tea类了,代码如下:
public class Coffee extends Beverage {
void brew() {
System.out.println("用沸水冲咖啡");
}
void addCondiments() {
System.out.println("加糖加奶");
}
}
到这里,发现问题了,冲杯Coffee,前三个步骤是必须的,但是添加调料就不是必须的了,有的人不喜欢给咖啡里加糖加奶,怎么办?
基于这个问题我们引出"钩子"方法:
在上面的链接的这篇文章里,有一段话“在abstract class的定义中,我们可以赋予方法的默认行为。”
对此我的理解:给一个方法赋予默认行为,这个方法是子类可以重写也可以不重写,子类可以通过重写来提升方法的性能,这样的方法就可以成为“钩子”方法。有人会说,普通的方法(没有被final修饰)也可以做到可以重写也可以不重写啊。难道都是“钩子”方法?!
在别人的博客看到这样的话:“继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。”
所以,我觉得“钩子”方法和普通方法,其实是个度的问题,普通方法,就如上面这段话的红色字所说,它有它自己规定的契约,不太希望被改变。而“钩子”方法是规定了默认行为(这个默认行为甚至可以为空,“钩子”方法什么都不做),最为宽泛的,子类可以在其上进行规定。
接下来,我们看看如何用"钩子"方法解决上面的问题——有的人不喜欢给咖啡里加糖加奶,怎么办?
先来看Beverage类:
public abstract class Beverage {
final void prepareRecipe() {
boilwater();
brew();
pourIncup();
if(customerWantsCondiments()){
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilwater() {
System.out.println("烧水");
}
void pourIncup() {
System.out.println("倒入杯中");
}
boolean customerWantsCondiments() {
return true;
}
}
这个customerWantsCondiments()就是一个“钩子”方法,只会返回true,不做别的事,子类可以覆盖也可以不覆盖。
再来看Coffee类:
public class Coffee extends Beverage {
void brew() {
System.out.println("用沸水冲咖啡");
}
void addCondiments() {
System.out.println("加糖加奶");
}
boolean customerWantsCondiments() {
String answer = getUserInput();
if(answer.toLowerCase().startsWith("y")){
return true;
}else{
return false;
}
}
public String getUserInput() {
String answer = null;
System.out.println("需要加糖加奶么?(y/n)");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try{
answer = in.readLine();
}catch(IOException e){
System.out.println("抱歉,出错了!");
}
if(answer==null){
answer = "no";
}
return answer;
}
}
问题解决了!“钩子”方法竟然能够作为条件控制,影响抽象类中的算法流程,实在不赖吧
![微笑](http://static.blog.csdn.net/xheditor/xheditor_emot/default/smile.gif)
这里我们总结下“钩子”方法的几个作用:
1)可以让子类实现算法中可选的部分;
2)在“钩子”方法对子类的实现并不重要的时候,子类可以对它置之不理;
3)让子类能够有机会对模板方法中即将发生的(or刚刚发生的)步骤做出反应;
4)也可以让子类有能力为抽象类做一些决定(上面的代码用的就是这个)。
P.S:
好莱坞原则:别调用我们,我们会调用你。
在好莱坞原则下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。也就是说高层组件对低层组件采用了好莱坞原则。
好莱坞原则和模板方法模式
还是用我们上述的例子来说。
Beverage是我们的高层组件,它能够控制饮料的冲泡算法,只有在需要子类实现某个方法时,才调用子类。
饮料的客户代码只依赖Beverage抽象,而不依赖具体的Tea或Coffee,这可以减少整个系统的依赖。