在PHP中使用委托

PHP
此文是对 PHP 中委托实现以及原理的讨论,它结合了我在日常开发中的一些浅见。希望能带给大家一些新的思路,如果有一些不同意见,也欢迎大家一起讨论。

Delegate

如果你做过iOS开发,那么你一定对其中遍布的 Delegate 实现印象深刻。它提供了一种很好的非侵入式的扩展方法,使得你可以在当前的对象定义中加入对原生代码的扩展实现。在我看来,Delegate 有以下几个显而易见的好处

非入侵

正如上面所说,委托机制的实现是非入侵的。相比之下,不论是基于 prototype 或者是 extends 式的扩展,都存在破坏原有类的风险。让我们回忆一下在 Objective-C 里 Delegate 的使用方法

首先是声明委托

@interface SomeController () <
XXViewDelegate
>

以上代码声明了我们要在当前 SomeController 类中实现名为 XXViewDelegate 的委托,根据通常的命名规则判断,这个委托是对 XXView 的扩展。我们还可以发现,所谓的 Delegate 在代码表现上实际上就是定义好的接口(interface),所以实现委托实质上就是实现(implements)接口。

@implementation SomeController

- (void)viewDidLoad {
    self.xxView.delegate = self;
}

- (BOOL)xxViewRefresh {
    return YES;
}

这两个方法很容易理解,第一个方法把实现委托给当前类,第二个方法就是具体的委托实现了。通过以上简单的代码,我们就在一个完全不同类型的类(SomeController)中实现了对原有类(XXView)的扩展。原有类的实现没有被改变,我们也没有定义新的类,代价很小。同时,我们也观察到了另一个好处。

动态委托

也就是虽然委托的声明和实现都是静态的,但是委托这个动作却是动态的。

self.xxView.delegate = self;

我们随时可以撤销委托

self.xxView.delegate = nil;

甚至可以把它委托给其它对象

self.xxView.delegate = self.otherController;

这就大大增强了我们程序的灵活性。

使用范围

从上面的讨论看出,委托机制非常灵活,那么它是否可以完全替代传统的诸如继承这类的扩展方法呢?答案当然是否定的,它们的使用场景完全不一样。

继承的扩展多用于同构的类之间,什么意思?子类扩展父类,当然要用继承机制来扩展比较方便,直接实现父类的抽象方法或者重载相应方法就行了。而委托机制多用于不同对象之间,比如在上面的例子中一个 Controller 中要实现对 View 的扩展,你不可能继承它,但可以把这个扩展接口委托到当前的 Controller (或者另一个)对象来处理。

在 PHP 中使用

PHP 的代码很少用委托来实现扩展,现在大部分扩展的方式要不就是通过继承,要不就是通过实现内部回调钩子函数。前者的局限性不再赘述,后者的使用则要分开来看。

在诸如 WordPress 这类开源系统中,由于外部开发者往往很难(或者不需要)完全掌握其内部运行机制,而钩子函数这种弱约定的较少暴露其内部特性的扩展机制就比较适合了。

为什么说它是弱约定的?比如如下代码:

<?php
class A
{
    private $_hook;

    public function run($a, $b)
    {
        $hook = $this->_hook;
        return empty($hook) ? $a + $b : $hook($a, $b);
    }

    public function setHook(callable $hook)
    {
        $this->_hook = $hook;
    }
}

$a = new A();

$a->setHook(function ($a, $b) {
    return $a - $b;
});

echo $a->run(1, 2);

以上代码展现了一个简单的回调钩子系统,我们通过 setHook 方法可以给对象 $arun 方法进行扩展。但是由于 PHP 本身语言的局限性,我们定义的钩子函数是没有任何类型以及参数约定限制的。也就是说以下代码都可以运行(可能有警告)

<?php
$a->setHook(function ($a) {
    return $a;
});

$a->setHook(function () {
    return 2;
});

$a->setHook(function ($a, $b, $c) {
    return $a + $b + $c;
});

$a->setHook(function ($a, $b) {
    return $a . $b . 'hi';
});

这对于一个开源系统来说,其实是好事,因为它保证了一定程度的灵活性,又让核心系统不至于完全无法运行。但是却无法适用于标准化程度更高的企业级开发,企业级开发更愿意依赖语言或者框架内部的约束机制,而不会寄希望于文档(并不是指不需要文档),或者更加虚无缥缈的人员素质。这就是为什么会出现 TypeScript 这样的语言,要不然你很难对 JavaScript 这样的语言进行大规模企业级开发。

回到 PHP 上来,还是用上面的例子,如果我们用委托的方式来实现同样的扩展效果会怎么做呢?

interface AHookDelegate
{
    public function aHookMethod(int $a, int $b): int;
}

class A
{
    private $_hook;

    public function run($a, $b)
    {
        return empty($this->_hook) ? $a + $b : $this->_hook->aHookMethod($a, $b);
    }

    public function setHook(AHookDelegate $hook)
    {
        $this->_hook = $hook;
    }
}

使用上也特别简单

<?php
class B implements AHookDelegate
{
    public function run()
    {
        $a = new A();
        $a->setHook($this);
        echo $a->run(1, 2);
    }

    public function aHookMethod(int $a, int $b): int
    {
        return $a - $b;
    }
}

接口方法的强类型约束保证了扩展接口的统一性,这样的实现无疑比用钩子函数强壮得多。如果你使用 IDE 或者类型检查工具,也可以在程序编写的时候就发现可能的错误。

结语

以上仅仅非常基础地探讨了在 PHP 中使用委托方式扩展的可行性,在实际的工程运用中,可能会遇到更加复杂的情况。得益于 PHP 这些年在强类型约束方面的进步,我们并不能用老眼光来看待这个语言,以前不能做的很多事情,现在已经可以很好的实现。

已有 3 条评论
  1. 您好,能不能把你博客的nginx配置发我一份啊,非常感谢您。

  2. 海安人才网
    海安人才网

    现在学习php迟吗?我是新手

  3. 一只很大的菜鸟
    一只很大的菜鸟

    真牛逼,学到了,四年前的文章了....