这可能是会在日常工作中最最常用到的设计模式,没有之一。
以我过往的工作经验,无论是PHP的web框架程序,抑或是安卓本身的框架代码,都大量使用了模版方法模式,所以无论是从更容易的理解框架代码,还是让你使用类似的方式编写重用性更高的代码角度上说,掌握这个模式都是相当重要的。
Let's go.
茶与咖啡
假设我们要开一家奶茶店,需要对调制饮品进行规范化和程序化管理,首先我们自然需要观察现有的奶茶和咖啡冲泡流程。
class Tea:
def prepare(self):
self.boilWater()
self.addCoffee()
self.brewing()
self.addSugger()
def brewing(self):
print("brewing")
def boilWater(self):
print("boil water")
def addCoffee(self):
print("add coffee")
def addSugger(self):
print("add sugger")
class Coffee:
def prepare(self):
self.boilWater()
self.addTea()
self.brewing()
self.addLemon()
def brewing(self):
print("brewing")
def boilWater(self):
print("boil water")
def addTea(self):
print("add tea")
def addLemon(self):
print("add lemon")
import os
import sys
parentDir = os.path.dirname(__file__)+"\\.."
sys.path.append(parentDir)
from coffee_store_v1.src.coffee import Coffee
from coffee_store_v1.src.tea import Tea
coffee = Coffee()
tea = Tea()
coffee.prepare()
tea.prepare()
# boil water
# add tea
# brewing
# add lemon
# boil water
# add coffee
# brewing
# add sugger
可以看到,茶与咖啡的制作流程非常相似,都是烧水》加原料》冲泡》加辅料这四个步骤,唯一的区别不过是原料不同以及辅料不同罢了,烧水和冲泡的环节没有任何区别。
所以很自然地,我们会想到对上面的代码进行重构,以实现代码复用。
我们现在来看一下v2版本的奶茶店:
from abc import ABC, abstractmethod
class HotDrink(ABC):
def prepare(self) -> None:
self._boilWater()
self._addRawMaterial()
self._brewing()
self._addAuxiliary()
def _brewing(self) -> None:
print("brewing")
def _boilWater(self) -> None:
print("boil water")
def _addRawMaterial(self) -> None:
pass
def _addAuxiliary(self) -> None:
pass
from hot_drink import HotDrink
class Coffee(HotDrink):
def _addRawMaterial(self):
print("add coffee")
def _addAuxiliary(self):
print("add sugger")
from .hot_drink import HotDrink
class Tea(HotDrink):
def _addAuxiliary(self) -> None:
print("add lemon")
def _addRawMaterial(self) -> None:
print("add tea")
可以看到现在Tea
和Coffee
中的代码都精简了很多,大部分代码实现了复用,而且如果要引入新的热饮,也只需要实现小部分代码即可。
像上面这种将可复用的“算法骨架”集中于基类,仅预留“模版方法”用于子类实现的方式就称作模版方法模式。
模版方法模式
其UML表示也很简单:
通常我们需要一个抽象基类TempBase
管理算法骨架,具体的算法骨架会填充在类似recretMethod
这样的具体方法中,并且一般会避免子类无意中覆盖,所以在Java等语言中会使用final
关键字声明。为子类实现而预留的模版方法templateMethod
一般会声明为抽象方法,以要求子类必须实现相应的方法。
可能会有初学者担心无法明锐地在一开始察觉哪些部分代码会重复,需要使用模版方法模式进行代码复用,在我看来大可不必。事实上依据我的经验,大部分代码复用和设计模式的引入往往都是因为在编写代码的时候发现重复性的复制、粘贴代码行为过于频繁,这时候你会自然而然地想到是否可以套用合适的设计模式来进行代码重构。所以发觉需要使用设计模式进行重构并不困难,真正困难的是进行重构的勇气。重构往往意味着诸多麻烦和暂时的“工作停滞”,这在短期内是不会有明显收益或者影响工期的,但长期必然收益。并且随着时间推移,代码的大量堆叠,重构的困难将越来越大,所以如果你察觉了某些地方存在问题,那么就去重构吧。此外还需要注意的是,在是否重构和重构的过程中都需要和你的团队保持沟通,以确保代码设计的一致性,这是非常重要的。
钩子
模版方法模式中的抽象基类除了拥有具体方法和抽象方法以外,可能还会有“钩子方法”。这种方法同样可能会被子类所重写,但与模版方法不同的是,它们并非抽象方法,不强制要求子类必须重写,而是可选的。如果子类没有或者不需要重写,则会提供一个默认的行为。
from abc import ABC, abstractmethod
class HotDrink(ABC):
def prepare(self) -> None:
self._boilWater()
self._addRawMaterial()
self._brewing()
self._addAuxiliary()
if self._hasPackage():
print("package drink")
def _brewing(self) -> None:
print("brewing")
def _boilWater(self) -> None:
print("boil water")
def _addRawMaterial(self) -> None:
pass
def _addAuxiliary(self) -> None:
pass
def _hasPackage(self) -> bool:
return False
我们给热饮HotDrink
基类添加一个钩子方法_hasPackage
,可以看到,在骨架方法prepare
中使用此钩子方法的返回值来决定是否对饮料进行打包,而这个钩子方法本身提供一个默认的方式,即返回False
,也就是不打包饮料。如果热饮的子类需要对饮料进行打包,也相当容易:
from .hot_drink import HotDrink
class Coffee(HotDrink):
def _addRawMaterial(self):
print("add coffee")
def _addAuxiliary(self):
print("add sugger")
def _hasPackage(self) -> bool:
return True
只要重写_hasPackage
钩子方法并返回True
即可,如果不想打包就什么都不做就可以了。
可以看到钩子方法的方式比模版方法更为灵活,不需要强制子类必须重写相应的方法,这样做可以减轻子类实现模版方法模式的可能的工作量。
好莱坞原则
这里引申出一个新的设计模式原则:好莱坞原则。
这个原则指的是“不要打电话给我,我会打电话给你”。
应用在设计模式中,指地是子类的方法由基类进行调用,而非反过来。
就像上面饮料店的例子中展示的那样,核心的饮料调制程序由基类HotDrink
进行掌管,而子类只负责提供相应的“个性化子程序”,并且由基类HotDrink
决定是否需要以及何时调用。
这样的逻辑是清晰而且自然的,我们只需要将精力放在分析基类是如何设计的就可以弄懂整个饮料调制的过程。如果不是这样的话,比如某个具体的饮料会调用HotDrink
的方法并修改饮料调制过程,那我们的就不得不一一阅读每个子类,以弄懂整个饮料调制过程是否有类似的“幺蛾子”。
当然,和我们之前说的一样,这是原则而非规则,并非是一定要遵守的。
和依赖倒置原则相比,依赖倒置原则更多的是强调客户端程序以及子类之间可以依赖抽象基类进行解耦,而好莱坞原则则强调基类单方面对子类的依赖以让子类和父类的关系更加简单而容易掌控。
荒野中的模版方法
我喜欢《Head First 设计模式》将现实比作“荒野”的叫法,如果说GOF总结并提出的经典设计模式是学院派的话,那现实中的情形的确称得上是危机四伏的荒野。
当然,在荒野中我们不能指望所有东西和书本上是一模一样的。
Java中的数组排序
在Java的标准组件中,我们可以通过Arrays.sort
对一个数组进行排序:
package pattern8.sort_in_java;
import java.util.Arrays;
class Main{
public static void main(String[] args) {
int[] a = {2,3,1,5,8,3};
Arrays.sort(a);
for (int num : a) {
System.out.print(num);
System.out.print(" ");
}
}
}
下面我们自己实现一个Arrays.sort
,来说明Java的标准库是如何利用模版方法的思想来实现这个排序功能的。
package pattern8.sort_in_java_v2;
public class MyArrays {
/**
* 对给定的数组进行排序
*
* @param array
*/
public static void sort(Object[] array) {
if (array.length <= 1) {
return;
}
for (int i = array.length; i > 0; i--) {
for (int j = 1; j < i; j++) {
int x = j - 1;
int y = j;
Comparable xObj = (Comparable) array[x];
Comparable yObj = (Comparable) array[y];
if (xObj.compareTo(yObj) > 0) {
MyArrays.swap(array, x, y);
}
}
}
}
/**
* 交换数组中的两个元素
*
* @param array
* @param x 第一个元素的下标
* @param y 第二个元素的下标
*/
private static void swap(Object[] array, int x, int y) {
Object temp = array[x];
array[x] = array[y];
array[y] = temp;
}
}
在这里我实现了一个MyArrays
类进行排序工作,具体的排序方法sort
实现了对一个对象数组的排序,排序算法简单地使用冒泡排序完成,而排序依据则依赖于数组中的元素所实现的Comparable
接口。
为了测试,编写了一个Caracter
类,其中的属性attack
指代游戏角色的攻击力,正好利用此属性实现Comparable
接口并提供给排序程序。
package pattern8.sort_in_java_v2;
class Character implements Comparable {
String name;
int attack;
public Character(String name, int attack) {
this.name = name;
this.attack = attack;
}
public int compareTo(Object o) {
Character other = (Character) o;
if (this.attack > other.attack) {
return 1;
} else if (this.attack == other.attack) {
return 0;
} else {
return -1;
}
}
public String toString() {
return "Character(" + this.name + "," + this.attack + ")";
}
}
测试程序如下:
package pattern8.sort_in_java_v2;
class Main {
public static void main(String[] args) {
Integer[] a = { 2, 3, 1, 5, 8, 3 };
MyArrays.sort(a);
printArray(a);
Character[] characters = { new Character("saber", 10), new Character("lancer", 1),
new Character("assasin", 0) };
MyArrays.sort(characters);
printArray(characters);
}
private static void printArray(Object[] array) {
for (Object obj : array) {
System.out.print(obj);
System.out.print(" ");
}
System.out.print("\n");
}
}
如想进行自己调试,可以从Github仓库下载相关代码。
这里似乎看不到模版方法的影子,但实际上Comparable
接口就扮演了模版方法的角色,虽然它并不是在MyArrays
的子类中实现,而是在被排序的数组元素中,但是起到的作用是一致的,都是在排序算法骨架中预留的一个子程序,通过这种方式,我们不需要继承MyArrays
即可提供对所有类型的数组的排序,只需要其包含的元素实现了Comparable
接口即可。
Python中的排序
类似的,Python中同样存在类似的使用,并且因为语言特点的不同,Python中的排序实现更“Python式”:
class Character:
def __init__(self, name: str, attack: int) -> None:
self.name = name
self.attack = attack
def __str__(self) -> str:
return "Character({},{})".format(self.name, self.attack)
def __repr__(self) -> str:
return self.__str__()
from character import Character
characters: list = [Character("lancer", 1), Character(
"saber", 10), Character("assasin", 0)]
characters.sort(key=lambda a: a.attack)
print(characters)
可以看到,排序依据key=lambda a: a.attack
是使用了一个匿名函数的方式作为参数传递给了排序方法sort
。
同样的,我们也可以实现一个自己的sorted
函数。
from typing import Callable, Sequence
from character import Character
def mySorted(srcSeq: Sequence, key: Callable) -> list:
if len(srcSeq) <= 1:
return list(srcSeq)
i: int
j: int
srcList = list(srcSeq)
for i in range(len(srcList), 0, -1):
for j in range(1, i, 1):
x = j-1
y = j
itemX = srcList[x]
itemY = srcList[y]
if key(itemX) > key(itemY):
srcList[x] = itemY
srcList[y] = itemX
return srcList
characters: list = [Character("lancer", 1), Character(
"saber", 10), Character("assasin", 0)]
sortedCharacters = mySorted(characters, key=lambda a: a.attack)
print(characters)
print(sortedCharacters)
# [Character(lancer,1), Character(saber,10), Character(assasin,0)]
# [Character(assasin,0), Character(lancer,1), Character(saber,10)]
完整代码见Github仓库。
相比于Java中通过数组中的元素来实现模版方法,这里更为简单直接,直接将排序依据作为可调用对象(Callable
)传入,并直接在排序算法中进行调用key(itemX) > key(itemY)
。
可以看到,因为Python中的函数作为一类对象,可以作为函数参数进行传递,所以具有这种Java不具有的灵活方式。
但无论如何,都是实现了一种通过其他方式填充算法骨架的思想。
Java窗口程序
虽然Java的桌面UI框架swing好像一直就不怎么流行,到现在可以说几乎销声匿迹,但作为学习还是一个不错的框架,包括安卓等的API设计对swing也多有借鉴。
package pattern8.swing;
import java.awt.Graphics;
import java.awt.HeadlessException;
import javax.swing.JFrame;
public class MyFrame extends JFrame {
public MyFrame(String title) throws HeadlessException {
super(title);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(300, 300);
this.setVisible(true);
}
public void paint(Graphics g) {
super.paint(g);
String msg = "Hellow World";
g.drawString(msg, 100, 100);
}
public static void main(String[] args) {
MyFrame myFrame = new MyFrame("my frame");
}
}
运行效果是这样的:
这里的模版方法模式相当明显,paint
就是一个钩子方法,在绘制Java窗体组件的时候会进行调用,子类可以使用这个钩子方法绘制所需要的组件。
如果你接触过一点安卓开发,就会发现安卓框架也大量使用类似的设计,安卓页面的各种生命周期的调用都是通过各种钩子方法在子类中实现的。
现在我们对模版方法模式进行简单总结。
总结
模版方法模式是将算法封装和集中在基类中,并以抽象方法或者钩子方法的方式提供给子类进行“个性化”实现。
与策略模式相比,策略模式是将算法封装在一组具有相同接口的类中,并以组合的方式结合在客户端程序中,以实现在运行时可以灵活进行算法替换。
模版方法介绍完了,再次感慨,这的确是个非常实用的设计模式,谢谢阅读。
本系列的所有工程文件都托管在Github项目
文章评论