解耦你的事件系统(基于事件驱动的设计、使用方式)

mot

英文原版: Decoupling your (event) system — PHP & Symfony
作者: Matthias Noback
作者博客: PHP & Symfony About PHP and Symfony2 development
翻译: mot
参考资料: The Principles of OOD

关于接口分离,依赖反转和包的稳定性

假定你正在创建一个可以复用的包.在这个包里面,你想用事件来构建和实现外部hook内部代码的功能.你会看到很多可以使用的事件管理器.当然你已经对 Symfony EventDispatcher component 有了某种程度的熟悉了,你决定把它加入到你包的composer.json文件中:

{
    "name": "my/package"
    "require": {
        "symfony/event-dispatcher": "~2.5"
    }
}

你的依赖图看起来应该是这样的:
请输入图片描述
引入这个包也不是什么问题都没有:所有要使用 my/packages 的人都要在它们的项目里引入 symfony/event-dispatcher ,这意味着在他们的项目里可能也有他们现成的事件分发器( event-dispatcher ) , 例如: Laravel , Doctrine , Symfony , 这样就没办法理解了 , 特别是因为各种事件分发器或多或少都做着类似的事情.

这个情况对大多数开发者来说可能只是小问题 , 当composer尝试解决版本约束的时候 , 这个额外的第三方库的依赖可能造成一些更严重的问题 , 因为可能有别的组件已经依赖了symfony/event-dispatcher , 并且它的require版本是 >=2.3 , <2.5 等等 ( 而你当前用的是2.4 )

Symfony EventDispatcher的缺陷

这些使用问题中最常见的情况是,当它遇到设计模式,依赖于一个具体的库 ( 比如 Symfony EventDispatcher )并不是特别好的选择 . 你可能使用诸如EventDispatcherInterface的接口在你的代码里,有点臃肿:

namespace Symfony\Component\EventDispatcher;
interface EventDispatcherInterface
{
    public function dispatch($eventName, Event $event = null);

    public function addListener($eventName, $listener, $priority = 0);
    public function addSubscriber(EventSubscriberInterface $subscriber);
    public function removeListener($eventName, $listener);
    public function removeSubscriber(EventSubscriberInterface $subscriber);
    public function getListeners($eventName = null);
    public function hasListeners($eventName = null);
}

这个接口基本违背了 Interface Segregation Principle 接口分离原则 , 这个意味着它服务了太多不同类型的客户:大部分的客户只是使用 dispatch() 方法来分发一个事件 , 而其它的客户可能只是使用 addListener()addSubscriber() .
剩下的方法对于客户来说可能都不会去使用,或者只是用来帮助debug的客户(在Symfony的基础代码中快速浏览一下,来确认一下这个地方有这个疑虑).

从我个人来看,我认为这个不是特别的好,因为事件会变成Event类的对象.我理解为什么Symfony这样做(基本上因为这个类有stopPropagation()和isPropagationStopped()两个方法来允许事件监听器来停止事件分发器去通知剩下的监听器) , 我从来都没有这样做过,或者期待这样的情况在我代码里出现.仅仅只是像最原始的观察者模式指定的那样,所有监听器(观察者)都应该可以对当前的情况做出反应.

我也不喜欢相同的事件类被用于不同的事件里面(只是名字不同而已,也就是dispatch()里的第一个参数不同).我更喜欢每个事件的类型都有自己的class.因此,对我这样就说得通了: 只是传递一个事件到dispatch()方法中,允许它来返回它自己的名字,这个名字无论如何都会是相同的.这个事件本身返回的名字又可以被用来决定需要被通知到得事件监听器.

根据这些反对的理由来设计Symfony EventDispatcher(事件分发器),我们更好得到了如下干净的接口(Interface):

namespace My\Package;

interface Event
{
    public function getName();
}

interface EventDispatcher
{
    public function dispatch(Event $event);
}

这个接口就真正的做好了能够在my/package中仅仅使用两个接口来达到目标.

Symfony事件分发器: 好的部分

当然,我们也想要使用Symfony EventDispatcher.总的说我们对它很熟悉了,但是这里还有更好的选择,像延迟加载监听器,而且当在一个Symfony应用中使用的时候它提供一个比较简单的方法来通过服务标签(Service中的tags值)来挂钩到监听器.

介绍适配器

因此,如果我们想要用我们自己的事件分发器API,这些API都是通过接口描述来定义的.但是我们也想要使用Symfony EventDispatcher,Symfony EventDispatcher有一个不一样的API.这个问题的解决办法就是创建一个适配器,这个适配器桥接了两个不一样的接口之间的差异(参考著名的适配器模式).在我们的案例中:

use My\Package\Event;
use My\Package\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class SymfonyEventDispatcher implements EventDispatcher
{
    private $eventDispatcher;

    public function __construct(EventDispatcherInterface $eventDispatcher)
    {
        $this->eventDispatcher = $eventDispatcher;
    }

    public function dispatch(Event $event)
    {
        // call the relevant listeners registered to the Symfony event dispatchers

        ...
    }
}

这个可以是一个手段来了解发生了什么,因为所有这些名字都相似.但是正像你看到的,一个Symfony事件分发器作为一个构造方法的参数来注入到适配器类里.当当前的dispatch()方法被使用的时候,这个适配器把调用转移到Symfony事件分发器(也就是刚注入的SymfonyEventDispatcher).

好了,这个并不是很直接的来转移dispatch()的调用到Symfony事件分发器,当我们早些时候看到它的方法应该像是这样的:

namespace Symfony\Component\EventDispatcher;

interface EventDispatcherInterface
{
    public function dispatch($eventName, Event $event = null);
    ...
}

但是不幸的是我们没有一个Symfony\Component\Event的对象在这里,我们也不想要其它的,因为这样又会再引入耦合到Symfony组件中.

幸运的是接口有别的方法我们可以来弥补这个缺陷:getListeners(),
这个意味着我们可以获取我们所有的监听器然后手动的通知它们:

class SymfonyEventDispatcher implements EventDispatcher
{
    ...

    public function dispatch(Event $event)
    {
        $listeners = $this->eventDispatcher->getListeners($event->getName());

        foreach ($listeners as $listener) {
            call_user_func($listener, $event);
        }
    }
}

我们现在已完全的避免了Symfony Event的使用.在我们的包里,我们只需要使用我们自己的EventDispatcher接口跟Event接口.这个意味着我们可以去掉对symfony/event-dispatcher包的依赖.

这个适配器类需要在它自己的包中,才能够完全减小我们的包跟symfony/event-dispatcher的耦合.减小我们包与其它组件之间耦合度的方法就是用适配器: my/package-symfony-bridge, 或者 my/package-symfony-event-dispatcher, 等等.
这个包的依赖图看起来还不错:
图片描述
如果此前你已经读了关于包得设计原则,你将会知道这个是非常好的一个包的系列因为my/package无论如何都不依赖于symfony/event-dispatcher.实际上,它也不依赖任何组件.我们称之为独立的包.同时,会是依赖于my/package,这会让my/package更健壮.

同时我也要指出基本上我们已经应用了一个古老而且大众化的设计原则:Dependency inversion principle(http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod):我们翻转了依赖的方向.与其从别的包中直接依赖一个类(或者这个案例中的接口),我们不如定义我们自己的接口.然后我们创建一个适配器来融合我们的接口和别的接口.

总结

你其实不需要依赖什么东西.
这个理论中,你的包不需要依靠任何其他的包.相反,它可以定义所有类型的接口.(作者的话太多了 无关的就不翻译了)

阅读 3.7k

404 Page Not Found
if (is.ie) { window.location.href="config/error.htm"; }
1.2k 声望
16 粉丝
0 条评论
1.2k 声望
16 粉丝
文章目录
宣传栏