- 封装算法
定义
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
优缺点
应用场景
Java数组类的设计者提供给我们一个方便的模板方法用来排序。
public static void sort(Object[] a) {
Object aux[] = (Object[])a.clone();
mergeSort(aux, a, 0, a.length, 0);
}
//将这个方法想象成一个模板方法
private static void mergeSort(Object src[], Object desc[], int low, int high, int off) {
for(int i = low; i < high; i++) {
for(int j = i; j > low && ((Comparable)desc[j-1]).compareTo((Comparable)desc[j]) > 0; j--) {
swap(desc, j, j-1);
}
}
return;
}
sort()的设计者希望它应用于所有的数组,所以他们把sort()变成静态方法,这样一来,任何数组都可以使用这个方法。他使用起来和在超类中是一样的。因为sort()并不是真正定义在超类中,所以sort()方法需要知道你已经实现了compareTo()方法,否则就无法进行排序;所以我们需要实现Comparable接口,实现这个接口所声明的方法compareTo()。
public class Duck implements Comparable {
String name;
int weight;
public Duck(String name, int weight) {
this.name = name;
this.weight = weight;
}
@Override
public String toString() {
return "Duck [name=" + name + ", weight=" + weight + "]";
}
@Override
public int compareTo(Object o) {
Duck otherDuck = null;
if(o instanceof Duck) {
otherDuck = (Duck) o;
if(this.weight < otherDuck.weight) {
return -1;
}
if(this.weight == otherDuck.weight) {
return 0;
}
return 1;
}
return 0;//这一步应该抛相应异常
}
}
钩子的使用场景:
public class MyApplet extends Applet {
String message;
//init钩子用来进行applet的初始化动作,它会在applet一开始的时候被调用一次
public void init() {
message = "Hello World";
repaint();
}
//这个start钩子可以在applet正要被显示在网页上时,让applet做一些动作
public void start() {
message = "Now I'm Starting up!";
repaint();
}
//如果用户跳转到别的网页,这个stop钩子会被调用,然后applet就可以在这里做一些事情停止他的行动
public void Stop() {
message = "Oh, now I'm Stopped";
repaint();
}
//applet正在被销毁(如:关闭浏览器),这个钩子会被调用
public void destory() {
//applet正在被销毁
}
public void paint(Graphics g) {
g.drawString(message, 5, 15);
}
}
例子
咖啡和茶的冲泡案例
步骤 | 茶的冲泡法 | 咖啡冲泡法 |
0 | 把水煮沸 | 把水煮沸 |
1 | 用沸水浸泡茶叶 | 用沸水冲泡咖啡 |
2 | 把茶倒进杯子 | 把咖啡倒进杯子 |
3 | 加柠檬 | 加糖和牛奶 |
茶的初始代码
public class Tea {
public void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
public void boilWater() {
System.out.println("Boiling water");
}
public void steepTeaBag() {
System.out.println("Steeping the tea");
}
public void pourInCup() {
System.out.println("Pouring into cup");
}
public void addLemon() {
System.out.println("adding Lemon");
}
}
冲咖啡的初始代码
public class Coffee {
public void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void boilWater() {
System.out.println("Boiling water");
}
public void brewCoffeeGrinds() {
System.out.println("Dripping Coffee through filter");
}
public void pourInCup() {
System.out.println("Pouring into cup");
}
public void addSugarAndMilk() {
System.out.println("Adding Sugar and Milk");
}
}
初始代码出现了重复代码,因此我们需要清理一下设计方案了。似乎把相同的部分提取出来放在一个基类里是个不错的主意。
提取出相同点:
步骤 | 操作 |
---|---|
0 | 把水煮沸 |
1 | 用热水冲泡 |
2 | 把饮料倒进杯子 |
3 | 在饮料内加入适当的调料 |
我们可以抽象出一个咖啡因饮料基类
public abstract class CaffeineBeverage {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
abstract void brew();
abstract void addCondiments();
public void boilWater() {
System.out.println("Boiling water");
}
public void pourInCup() {
System.out.println("Pouring into cup");
}
}
新的茶类与咖啡类
public class Tea extends CaffeineBeverage{
@Override
void brew() {
System.out.println("Steeping the tea");
}
@Override
void addCondiments() {
System.out.println("adding Lemon");
}
}
public class Coffee extends CaffeineBeverage {
@Override
void brew() {
System.out.println("Dripping Coffee through filter");
}
@Override
void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
}
在咖啡因饮料基类中,prepareRecipe()是我们的模板方法。模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
模板方法由超类主导一切,并受超类保护;复用性最大化;维护修改容易;超类专注于算法本身,而由子类提供完整的实现。
让我们看看类图:
类名 | 方法 | 描述 | |
抽象类 | AbstractClass | templateMethod() primitiveOperation1() primitiveOperation2() | 模板方法templateMethod() 执行操作方法 primitiveOperation1() primitiveOperation2() |
具体类 | ConcreteClass | primitiveOperation1() primitiveOperation2() | 这个具体类实现抽象的操作 |
抽象类
abstract class AbstractClass {
final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
abstract void primitiveOperation1();
abstract void primitiveOperation2();
final void concreteOperation() {//声明为final,这样子类就无法覆盖它
//具体实现
}
void hook() { }//可以有“默认不做事”的钩子(hook)方法,子类视情况决定要不要覆盖它们
}
钩子是一种被声明在抽象类中的方法,但是只有空的或默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。
先看一个具体例子:
public abstract class CaffeineBeverage {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if(customerWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
final void boilWater() {
System.out.println("Boiling water");
}
final void pourInCup() {
System.out.println("Pouring into cup");
}
boolean customerWantsCondiments() {//这是一个钩子,通常是空或默认实现
return true;
}
}
当你的子类“必须”提供算法中的某个方法或步骤的实现时,就是用抽象算法。如果算法的这个部分是可选的,就用钩子。如果是钩子的话,子类可以选择实现,但不强制。
对比
序号 | 模式 | 叙述 |
---|---|---|
0 | 模板方法 | 子类决定如何实现算法中的步骤 |
1 | 策略 | 封装可互换的行为,然后使用委托来决定要采用哪一个行为 |
2 | 工厂方法 | 由子类决定实例化哪个具体类 |
设计原则
封装变化
多用组合,少用继承
针对接口编程,不针对实现编程
为交互对象之间的松耦合设计而努力
类应该对扩展开放,对修改关闭
依赖抽象,不依赖具体类
最少知识原则:只和朋友交谈
好莱坞原则:别找我,我会找你(由超类主控一切,当它们需要的时候,自然会去调用子类)
总结
"模板方法"定义了算法的步骤,把这些步骤的实现延迟到子类。
模板方法模式为我们提供了一种代码复用的重要技巧。
模板方法的抽象类可以定义具体方法、抽象方法和钩子。
为了防止子类改变模板方法中的算法,可以将模板方法声明为final。
好莱坞原则告诉我们,将决策权放到高层模块中,以便决定如何以及何时调用低层模块。
策略模式和模板方法模式都封装算法,一个用组合,一个用继承。
工厂方法是模板方法的一种特殊版本。
小知识
好莱坞原则:
别调用我们,我们会调用你(由超类主控一切);
当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。
在好莱坞原则下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。
依赖倒置原则与好莱坞原则的关系:
依赖倒置原则教我们尽量避免使用具体类,而多使用抽象(接口)。而好莱坞原则是用在创建框架或组件上的一种技巧,好让低层组件能够被挂钩计算中,而且又不会被高层组件依赖低层组件。两者的目标都是解耦