解耦你的事件系統(基於事件驅動的設計、使用方式)

英文原版: 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):咱們翻轉了依賴的方向.與其從別的包中直接依賴一個類(或者這個案例中的接口),咱們不如定義咱們本身的接口.而後咱們建立一個適配器來融合咱們的接口和別的接口.

總結

你其實不須要依賴什麼東西. 這個理論中,你的包不須要依靠任何其餘的包.相反,它能夠定義全部類型的接口.(做者的話太多了 無關的就不翻譯了)

相關文章
相關標籤/搜索