说起“钩子(Hook)”这个名词,可能身为开发的同学大都听说过。所谓Hook机制,是从Windows编程中流行开的一种技术。其主要思想是提前在可能增加功能的地方埋好(预设)一个钩子,这个钩子并没有实际的意义,当我们需要重新修改或者增加这个地方的逻辑的时候,把扩展的类或者方法挂载到这个点即可。这可能不太好理解,接下来就开始以PHP为例讲述下钩子到底是什么。
讲到“钩子”,一定要提前说明的是一种设计模式,那就是行为型设计模式中的模板方法模式,明白了它的话,会让我们更容易理解钩子。
一.什么是模板方法模式
模板方法模式是一种基于继承的代码复用,它是一种类行为型模式,在其结构中只存在父类与子类之间的继承关系。通过使用模板方法模式,可以将一些复杂流程的实现步骤封装在一系列基本方法中,在抽象父类中提供一个称之为模板方法的方法来定义这些基本方法的执行次序,而通过其子类来实现某些步骤,从而使得相同的算法框架可以有不同的执行结果。
简单而言,这个模式更像是进行婚礼活动(父类),按照某一个安排的流程进行,只是每个不同的婚礼(子类)来说,虽然流程是一样的,但是每个流程中,每个婚礼都有自己的玩法,例如结婚都要接亲,都要被伴娘拦住玩游戏,玩的是什么可能都不一样,然后接回家以后要进行一些礼节,可能有的递茶,有的吃饺子什么的各不相同,以代码的形式说明如下
abstract class AbstractWedding
{
// 定义婚礼顺序
public function templateMethod()
{
$this->appendGame();
$this->appendMarriage();
}
// 婚礼的流程行为需要实现的
abstract protected function appendGame();
abstract protected function appendMarriage();
}
// 子类的婚礼进行实现
class Wedding1 extends AbstractWedding
{
protected function appendGame()
{
// 找鞋,喝酒
}
protected function appendMarriage()
{
// 教堂宣誓
}
}
class Wedding2 extends AbstractWedding
{
protected function appendGame()
{
// 发红包
}
protected function appendMarriage()
{
// 亲吻新娘
}
}
// 客户
class Client
{
public function __construct()
{
$wedding = new Wedding1();
$wedding->templateMethod();
}
}
要注意的是,在处理的过程中,要遵循反向控制接口(“好莱坞原则”),这个原则是指父类调用子类的操作,而子类不调用父类的操作。好莱坞原则于模版方法设计模式紧密相关,因为它在父类中实现,除了templateMethod方法外,父类的其他方法都是抽象和受保护的方法。所以,尽管客户实例化一个具体类,但是它调用了父类中实现的方法。
二.什么时候使用模板方法
如果已经明确算法中的一些步骤,不过这些步骤可以采用多种不同的方法进行实现,就可以使用模板方法模式。如果算法的步骤不变,可以把这些步骤交给子类去实现。这种情况下,可以使用设计模式来组织抽象类中的基本操作,然后子类去做这些基本操作所需要的具体过程。
还有一种用法可能稍微复杂一些,可能需要把子类共同的行为放在一个类里面,以避免代码重复,毕竟每次实现的子类不回是完全不同的,估计很快就会产生重复代码。
最后一点,就是可以使用模板方法模式来控制子类拓展,也就是我们本次需要说的钩子。可以利用钩子控制拓展,只在钩子操作所在的某些位置允许拓展。
三.模板方法设计模式中的钩子
有的时候,模板方法函数中可能有一个不想要的步骤,例如,在我们购买商品的时候要计算最终价格,商品价格+运费+服务产生费用,不过在有些活动中,顾客商品价格满100元就可以免运费。这里就要使用到模板方法的钩子。
也就是说,在模板方法设计模式中,利用钩子可以将一个方法作为模板方法的一部分,不过不一定会用到这个方法。换句话说,它是方法的一部分,不过它包含一个钩子,可以处理例外的情况,子类可以为算法增加一个可选元素,这样以来,尽管仍然按照父类模板方法建立的顺序执行,但是有可能并不完全按照模板方法期望的那样动作。
不过这可能有点违背之前说的需要遵守的“好莱坞原则”,因为子类没有遵循父类设置的顺序,好莱坞原则要求只有父类能够改变框架。钩子的话更像一个“后门”,进行处理例外的情况。
举个钩子的简单的例子,就拿免运费的例子进行实现。注意,虽然子类可以改变钩子的行为,但是仍然要遵守模板方法中定义的执行顺序。
// 建立钩子
abstract class IHook
{
protected $purchased;
protected $hookSpecial;
protected $shippingHook;
protected $fullCost;
public function templateMethod($total, $special)
{
$this->purchased = $total;
$this->hookSpecial = $special;
$this->addTax();
$this->addShippingHook();
$this->cost();
}
protected function addTax();
protected function addShippingHook();
protected function cost();
}
抽象类IHook定义了几个抽象方法,并且确定了他们的执行顺序,这里hook方法放到了中间,实际上它可以放在顺序中的任意位置。$special代表的是是否免运费。
// 实现钩子
class Calc extends IHook
{
protected function addTax()
{
$this->cost = $this->purchased + ($this->purchased * 0.07);
}
protected function addShippingHook()
{
if(!$this->hookSpecial) {
// 这里设置变量为了更好理解,其实按照题意应该是 if($this->cost > 100)
$this->cost += 5;
}
}
protected function cost()
{
return $this->cost;
}
}
addTax()和cost()都是标准方法,只有一个实现,不过addShippingHook()的实现有所不同,其中有一个条件来确定是否要增加运费,这个就是钩子。 客户 Client类具体使用不做说明了。其实就是一个设置和调用了。
四.优缺点
优点
1.提高代码复用性 ,将相同部分的代码放在抽象的父类中
2.提高了拓展性 ,将不同的代码放入不同的子类中,通过对子类的扩展增加新的行为
3.实现了反向控制,通过一个父类调用其子类的操作,通过对子类的扩展增加新的行为,实现了反向控制 & 符合“开闭原则”
缺点
引入了抽象类,每一个不同的实现都需要一个子类来实现,导致类的个数增加,从而增加了系统实现的复杂度。
你理解中的“钩子”是什么样的呢?