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

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

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

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

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

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

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

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的基础代码中快速浏览一下,来确认一下这个地方有这个疑虑).composer

从我我的来看,我认为这个不是特别的好,由于事件会变成Event类的对象.我理解为何Symfony这样作(基本上由于这个类有stopPropagation()和isPropagationStopped()两个方法来容许事件监听器来中止事件分发器去通知剩下的监听器) , 我历来都没有这样作过,或者期待这样的状况在我代码里出现.仅仅只是像最原始的观察者模式指定的那样,全部监听器(观察者)都应该能够对当前的状况作出反应.ui

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

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

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):咱们翻转了依赖的方向.与其从别的包中直接依赖一个类(或者这个案例中的接口),咱们不如定义咱们本身的接口.而后咱们建立一个适配器来融合咱们的接口和别的接口.

总结

你其实不须要依赖什么东西. 这个理论中,你的包不须要依靠任何其余的包.相反,它能够定义全部类型的接口.(做者的话太多了 无关的就不翻译了)

相关文章
相关标签/搜索