这是我看Head first设计模式书籍之后想要总结的知识点,一方面是对自己学习的东西总结和提炼加强自己的理解和记忆,另一方面是给大家简化这本书,方便大家快速了解各种设计模式。我想提醒大家的是,设计模式只是前人总结的一些经验套路,实际上还是要在开发项目中慢慢体会,不可成为设计模式的中毒患者,强行照搬设计模式的一些规则。
我们将要深入封装算法块,好让子类可以在任何时候都可以讲自己挂接进算法里。另外我们会介绍一个新的面向对象设计原则"好莱坞原则".
我们举个栗子:
茶和咖啡的冲泡方式非常相似,下面是冲泡法:
咖啡冲泡法
(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 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 }
结果:
Before sorting:
Daffy weights 8
Dewey weights 2
Howard weights 7
Louie weights 2
Donald weights 10
Huey weights 2After 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语言的时候曾经多次用到迭代器,
但是始终不知道具体的实现原理和内容,下次我们将揭露迭代器到底是怎么工作的。