我想提醒大家的是,设计模式只是前人总结的一些经验套路,实际上还是要在开发项目中慢慢体会,不可成为设计模式的中毒患者,强行照搬设计模式的一些规则。
我们将要深入封装算法块,好让子类可以在任何时候都可以讲自己挂接进算法里。另外我们会介绍一个新的面向对象设计原则"好莱坞原则".
我们举个栗子:
茶和咖啡的冲泡方式非常相似,下面是冲泡法:
咖啡冲泡法
(1) 把水煮沸
(2) 用沸水冲泡咖啡
(3) 把咖啡倒进杯子
(4) 加糖和牛奶
茶冲泡法
(1) 把水煮沸
(2) 用沸水浸泡茶叶
(3) 把茶倒进杯子
(4) 加柠檬
我们将写一些代码来创建咖啡和茶
直接看代码部分
咖啡类
public class Coffee {
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");
}
}
茶类
public class Tea {
void prepareRecipe(){
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
public void boilWater(){
System.out.println("Boiling water");
}
public void steepTeaBag(){
System.out.printn("Steeping the tea");
}
public void pourInCup(){
System.out.println("Pouring into cup");
}
public void addLemon(){
System.out.println("Adding Lemon");
}
}
可以看到有一些重复的代码,这样我们可以将共同部分抽取出来,放进一个基类
但是,我们还忽略了咖啡和茶冲泡都是使用了相同的算法:
(1) 把水煮沸
(2) 用热水泡咖啡或茶
(3) 把饮料倒进杯子
(4) 在饮料内加入适当的饮料
我们重新将基类中的prepareRecipe()方法写成
void prepareRecipe(){
boilWater();
brew();
pourInCup();
addCondiments();
}
示例代码:
基类
public abstract class CaffeineBeverage {
//prepareRecipe()被声明为final,我们不希望子类覆盖这个方法,因为这个是个算法模板
final void prepareRecipe(){
//封装了算法的步骤
boilWater();
brew(); //将茶和咖啡第二步骤抽象成一个方法
pourInCup();
addCondiments(); //在茶或咖啡中加调料
}
// 这两个方法必须声明为抽象,留给茶或咖啡子类去做
abstract void brew();
abstract void addCondiments();
//对于茶或咖啡都适用的方法
void boilWater(){
System.out.println("Boiling water");
}
void pourInCup(){
System.out.println("Pouring into cup");
}
}
基类子类茶类和咖啡类
public class Tea extends CaffeineBeverage {
public void brew(){
System.out.println("Steeping the tea");
}
public void addCondiments(){
System.out.println("Adding Lemon");
}
}
public class Coffee extends CaffeineBeverage{
public void brew(){
System.out.println("Dripping Coffee through filter");
}
public void addCondiments(){
System.out.println("Adding Sugar and Milk");
}
}
prepareRecipe()方法就是模板方法,定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
下面是我们的模板方法模式出场了:
模板方法模式: 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类
可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
类图如下:
模板方法的优点:
可以将代码的复用最大化,另外模板方法专注在算法本身,而由子类提供完整的实现
我们看一下抽象基类可以有哪些类型的方法:
abstract class AbstractClass {
final void templateMethod(){
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
//这两个抽象方法由具体的子类实现
abstract void primitiveOperation1();
abstract void primitiveOperation2();
//它可以被模板方法直接使用或者被子类使用
final void concreteOperation(){
//这里是实现
}
void hook(){
//什么都不做,或者缺省默认一些操作
}
}
hook()方法为"hook"(钩子),子类可以视情况决定要不要覆盖它们。
钩子的定义:
钩子是一种被声明在抽象类中的方法,但只有空或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。
钩子的用法:
1.钩子可以让子类实现算法中可选的部分,或者在钩子对子类的实现并不重要的时候,子类可以对此钩子置之不理。
2.让子类能够有机会对模板方法中某些即将发生的(或刚刚发生的)步骤做出反应。
3.钩子可以让子类有能力为其抽象类作一些决定。
下面看示例代码:
基类
public abstract class CaffeineBeverageWithHook {
void prepareRecipe(){
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()){
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater(){
System.out.println("Boiling water");
}
void pourInCup(){
System.out.println("Pouring into cup");
}
//钩子方法,子类可以覆盖这个方法,但也可以不
boolean customerWantsCondiments(){
return true;
}
}
可以看到默认算法步骤是有addCondiments()方法也就是加调料环节的。
咖啡类
public class CoffeeWithHook extends CaffeineBeverageWithHook {
public void brew(){
System.out.println("Dripping coffe through filter");
}
public void addCondiments(){
System.out.println("Adding Sugar and Milk");
}
//覆盖了钩子,提供了自己的功能
public boolean customerWantsCondiments(){
String answer = getUserInput();
}
private String getUserInput(){
String answer = null;
System.out.print("Would you like milk and sugar with your coffee (y/n)? ");
//询问用户需要调料吗?
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try{
answer = in.readLine();
} catch(IOException ioe){
System.err.println("IO errir trying to read your answer");
}
if (answer == null){
return '"no';
}
return answer;
}
}
测试类public class BeverageTestDrive {
public static void main(String args[]){
CoffeeWithHook coffeeHook = new CoffeeWithHook();
System.out.println("\nMaking coffee...");
coffeeHook.prepareRecipe();
}
}
结果
Making coffee...
Boiling water
Dripping coffe through filter
Pouring into cup
Would you like milk and sugar with your coffee (y/n)? y
Adding Sugar and Milk
可以看到确实钩子可以作为条件控制,影响抽象类中的算法流程。
下面我们来介绍一个新的设计原则,好莱坞原则
好莱坞原则(Hollywood principle):
Don't call me, I'll call you. 即别调用我们,我们会调用你。
高层组件对待低层组件的方式是:"别调用我们,我们会调用你",防止"依赖腐败"的方法。
高层组件是CaffeineBeverage类
在工厂模式那一节我们讲解了依赖倒置原则, 他和好莱坞原则比较相似
1.两者都是在于解耦,但是依赖倒置原则更加注重如何在设计中避免依赖。
2.依赖倒置原则教我们尽量避免使用具体类,而多使用抽象。而好莱坞原则是用在创建框架或组件上的一种技巧。
用模板方法来排序
Java数组类的设计者提供给我们一个便利的模板方法用来排序。
具体的实现代码请看API文档和源代码,这里给出一些核心的代码
这里有两个方法,共同提供排序的功能
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 dest[],
int low, int high, int off){
for(int i = low; i < high; i++){
for(int j = i; j > low &&
((Comparable)dest[j-1]).compareTo((Comparable)dest[j])>0; j--){
swap(dest, j, j - 1); //swap已经在数组类中定义了 public static void swap(list<?> list, int i, intj)
//在指定列表的指定位置处交换元素
}
}
}
我们接下来要比较鸭子
我们先要看Arrays类的静态方法sort方法
方法原型是 public static void sort(object[] a]
要注意的是: 根据元素的自然顺序对指定对象数组按升序进行排序。数组中的所有元素都必须实现Comparable
接口,此外数组中的所有元素都必须是可相互比较的。
所以我们需要让鸭子类实现Comparable接口,才能通过sort方法进行排序
public class DuckSortTestDrive{
public static void main(String[] args){
Duck[] ducks = {
new Duck("Daffy", 8),
new Duck("Dewey", 2),
new Duck("Howard", 7),
new Duck("Louie", 2),
new Duck("Donald", 10),
new Duck("Huey", 2)
};
System.out.println("Before sorting:");
display(ducks);
Arrays.sort(ducks);
System.out.println("\nAfter sorting:");
display(ducks);
}
public static void display(Duck[] ducks){
for(int i = 0; i < ducks.length; i++){
System.out.println(ducks[i]);
}
}
}
结果:
Before sorting:
Daffy weights 8
Dewey weights 2
Howard weights 7
Louie weights 2
Donald weights 10
Huey weights 2
After sorting:
Dewey weights 2
Louie weights 2
Huey weights 2
Howard weights 7
Daffy weights 8
Donald weights 10
sort()方法控制算法,没有类可以改变这一点,sort()依赖一个Comparable类提供compareTo()的实现
策略模式和模板方法都封装算法,一个用组合,一个用继承。工厂方法是模板方法的一个特殊版本
下次我们将介绍一个新的模式:迭代器和组合模式 我在以前学习C++、Python、Java语言的时候曾经多次用到迭代器,
但是始终不知道具体的实现原理和内容,下次我们将揭露迭代器到底是怎么工作的。