前幾天看到一個(gè)知乎的網(wǎng)友提問如何在業(yè)務(wù)中避免出現(xiàn)復(fù)雜的if...else...邏輯,其中一個(gè)答友回答需要去看大型框架的實(shí)現(xiàn).由于個(gè)人認(rèn)為ZF3(ZendFramework3的簡(jiǎn)寫)的事件驅(qū)動(dòng)模塊實(shí)現(xiàn)的很優(yōu)雅,有很多值得借鑒的地方,并且恰好解決了這位網(wǎng)友的疑問.
0x00. 什么是事件驅(qū)動(dòng)
一句話解釋:先綁定,后觸發(fā)的邏輯實(shí)現(xiàn).
舉個(gè)栗子:
小明
是個(gè)廚師.如果他工作在一家炒菜館.顧客
進(jìn)店,點(diǎn)了叫A
,B
,C
的三道菜,小明得到店小二
后立即從自己的大腦中回憶這三種菜的做法,并排序.接著通過一系列嫻熟的操作,將三道菜按照預(yù)先的排序呈現(xiàn)給顧客.
此時(shí)來了第二個(gè)顧客,點(diǎn)了
D
菜和S
湯以及主食N
,這時(shí)候小明又被觸發(fā)D,S,N背后的動(dòng)作,最終完成任務(wù).
在上面這個(gè)例子中:
顧客
:作為事件的綁定者,可以決定口味清淡與否,或者不加某種作料.
A
,B
,C
:綁定后的事件.
店小二
:負(fù)責(zé)通知,其實(shí)就是持有事件并且觸發(fā)的管理者.
小明
:負(fù)責(zé)出發(fā)后的邏輯實(shí)現(xiàn).
D
,S
,N
:下一個(gè)顧客綁定的事件.
這就是事件驅(qū)動(dòng),總結(jié)來說就是將一系列基礎(chǔ)操作邏輯封裝好,綁定給一個(gè)事件,當(dāng)這個(gè)事件被觸發(fā)時(shí),回調(diào)之前封裝好的邏輯.
1. 小明的操作就是事件的具體內(nèi)容.
2. 菜單上的菜名就是事件的名字.
3. 顧客來吃飯就是觸發(fā)一系列事件的條件.
4. 菜品就是整個(gè)事件的返回值.
如果小明工作在一家快餐店,情況將完全不同.他先將所有的菜做好,然后直接賣個(gè)顧客.
這就成了緩存機(jī)制,可能資源質(zhì)量有限,無法達(dá)到顧客最想要的程度,但是速度快.
但是飯店一般的做法就是現(xiàn)將永恒不變的資源緩存好,然后剩下的邏輯通過事件驅(qū)動(dòng)的方法,達(dá)到定制化要求.
0x01. 實(shí)現(xiàn)原理
在ZF3中,通過zend-eventmanager子模塊管理整個(gè)框架中的事件.將每個(gè)階段分拆成若干事件.
例如,在程序初始化階段(調(diào)用
Zend\Mvc\Application::init($applicationConfig)
),需要加載所有模塊,這時(shí)候zend-modulemanager將這階段常規(guī)觸發(fā)的事件定義為下列4個(gè):
loadModules
:主要負(fù)責(zé)出發(fā)下面兩個(gè)事件.
loadModule
:實(shí)例化每個(gè)模塊入口,并讀取主要配置,依賴檢查,模塊初始化,向bootstrap階段綁定事件等等.
mergeConfig
:合并所有模塊的配置文件.
loadModules.post
:配置所有servicemanager(Controller和Router等模塊的servicemanager).
然后在適當(dāng)?shù)臅r(shí)機(jī)由zend-eventmanager觸發(fā).
實(shí)現(xiàn)這種類似lazy service的原理是所有綁定在事件下的邏輯都是callable類型,其中包括帶有__invoke()的對(duì)象
,閉包
,數(shù)組構(gòu)成的callable
等.
0x02. 名詞解釋
如果你不了解ZF3的事件驅(qū)動(dòng),建議先移步zend-eventmanager手冊(cè)看個(gè)大概.
你可能已經(jīng)部分或者全部的閱讀過zend-eventmanager的源碼,相信如果你在某個(gè)階段一定有疑問,在zend-eventmanager中,諸如Event,Listener,EventManager,SharedEventManager到底是什么意思,以下逐一先做解釋:
Listener
:存放著主要業(yè)務(wù)邏輯的對(duì)象,在事件下綁定的多半就是Listener的方法(小明炒菜的各種操作).
Event
:容器,負(fù)責(zé)存放Listener中可能用到的對(duì)象,可能是整個(gè)框架的ServiceManager,也可能是一個(gè)變量(廚房,有工具,有原材料).
EventManager
:事件驅(qū)動(dòng)的管理者,負(fù)責(zé)綁定事件,存放事件,觸發(fā)事件等(店小二,負(fù)責(zé)通知小明開始按照要求做菜).
SharedEventManager
:有時(shí)候需要為別的階段綁定事件.例如在ZF3加載模塊的時(shí)候要向bootstrap階段添加事件怎么辦?因?yàn)閎ootstrap不僅和加載模塊是兩個(gè)完全獨(dú)立的事件,而且在加載模塊的時(shí)候bootstrap事件還不為EventManager所知.這時(shí)候就要將事件暫時(shí)存放在SharedEventManager中,在稍后EventManager讀取bootstrap事件的時(shí)候會(huì)去查看SharedEventManager中有沒有此階段的邏輯,如果有的話一并觸發(fā)(如果顧客想給旁邊桌的顧客點(diǎn)一道菜,負(fù)責(zé)將這件事記錄下來的本子就是SharedEventManager).
0x03. zend-eventmanager
這一節(jié)將從實(shí)例化zend-eventmanager模塊開始,介紹這一模塊的使用方法.
安裝:
composer require zendframework\zend-eventmanager
然后在自己的項(xiàng)目中:
require(__DIR__ . '/vendor/autoload.php');
use Zend\EventManager\EventManager;
$events = new EventManager();
這就得到了zend-eventmanager模塊的入口實(shí)例.這時(shí)就使用其提供的接口,添加,觸發(fā)事件.
例1:綁定一個(gè)輸出hello world的匿名函數(shù)給一個(gè)叫simpleEvent的事件.
//綁定
$events->attach('simpleEvent', function () { echo 'hello world'; });
//觸發(fā)
$events->trigger('simpleEvent');
//輸出:
//hello world
例2:綁定兩個(gè)匿名函數(shù),一個(gè)輸出hello,另一個(gè)輸出XiaoMing,要求兩個(gè)按照固定順序執(zhí)行,即hello Xiaoming.
這時(shí)候就涉及到attach()方法的第三個(gè)參數(shù),優(yōu)先級(jí).
//綁定
$events->attach('simpleEvent', function () { echo 'hello '; }, 100);
//再次綁定
$events->attach('simpleEvent', function () { echo 'XiaoMing'; }, 99);
//按照優(yōu)先順序觸發(fā)
$events->trigger('simpleEvent');
//輸出:
//hello XiaoMing
例3:如果我們要向綁定的方法中傳入?yún)?shù)怎么辦?例如,我們用參數(shù)來代替hello后的輸出內(nèi)容.
//綁定并獲取默認(rèn)的Event容器
$events->attach('simpleEvent', function ($event) {
echo 'hello ' . $event->getParams()['name'];
}, 100);
//觸發(fā),并傳入?yún)?shù)
$events->trigger('simpleEvent', null, ['name' => 'Lily']);
//輸出
// hello Lily
這里例子中,出現(xiàn)了一個(gè)新東西Event,Event是一個(gè)容器,用于封裝所有Listener(其實(shí)就是綁定的那個(gè)匿名函數(shù))需要的數(shù)據(jù).
具體來說,Event是zend-eventmanager提供的用于封裝Listener所需上下文,變量等的一個(gè)容器,可以自己定義(稍后介紹),也可以像上文三個(gè)例子中的,讓zend-eventmanager自動(dòng)生成一個(gè)默認(rèn)的Event.
默認(rèn)的Event包括target和params以及name三大部分,target一般用于存放上下文,可以使用getTarget(),setTarget()兩個(gè)接口來操作,剩下兩部分接口類似,都是標(biāo)準(zhǔn)的get,set.params用于存放trigger中的第三個(gè)數(shù)組參數(shù),以上例為例,是指['name' => 'Lily']
,而name則是當(dāng)前事件的名字,也就是上例中的simpleEvent.
zend-eventmanager在觸發(fā)綁定Listener的時(shí)候會(huì)講Event作為參數(shù)傳入.
這就是將參數(shù)傳入Listener中的方法.
trigger中的第二個(gè)參數(shù)就是target.
例4:例3的另外一種觸發(fā)方式.自己構(gòu)建Event,然后觸發(fā).
//綁定
$events->attach('simpleEvent', function ($event) {
//調(diào)用自己實(shí)現(xiàn)的接口
echo 'hello ' . $event->getUser();
});
use Zend\EventManager\Event;
//自己構(gòu)建Event,這是一種偷懶的方法,不推薦,讀者請(qǐng)引用
//Zend\EventManager\EventInterface認(rèn)真構(gòu)建
class MyEvent extends Event
{
protected $user;
public function setUser(string $user)
{
$this->user = $user;
}
public function getUser()
{
return $this->user;
}
}
$myEvent = new MyEvent;
//設(shè)置好事件的名字
$myEvent->setName('simpleEvent');
//自己實(shí)現(xiàn)的接口
$myEvent->setUser('Lucy');
//觸發(fā)
$events->triggerEvent($myEvent);
//輸出:
//hello Lucy
這里自己添加了接口getUser()
和setUser()
,用于在Listener中更便捷的訪問到變量.
同時(shí)還使用了另外一種觸發(fā)方式triggerEvent()
,用于直接觸發(fā)Event對(duì)象.
例5:如果需要從Listener中獲取數(shù)據(jù)怎么辦?也就是說Listener的返回值如何接收?
//綁定
$events->attach('simpleEvent', function ($event) {
//調(diào)用自己實(shí)現(xiàn)的接口
return 'hello ' . $event->getUser();
});
use Zend\EventManager\Event;
//自己構(gòu)建Event,這是一種偷懶的方法,不推薦,讀者請(qǐng)引用
//Zend\EventManager\EventInterface認(rèn)真構(gòu)建
class MyEvent extends Event
{
protected $user;
public function setUser(string $user)
{
$this->user = $user;
}
public function getUser()
{
return $this->user;
}
}
$myEvent = new MyEvent;
//設(shè)置好事件的名字
$myEvent->setName('simpleEvent');
//自己實(shí)現(xiàn)的接口
$myEvent->setUser('Lucy');
//觸發(fā)
$responses = $events->triggerEvent($myEvent);
//獲得返回值
echo $responses->pop();
//輸出:
//hello Lucy
由此可見zend-eventmanager將所有Listener的返回值全部封裝在$responses
中,使用current()
,next()
,pop()
,isEmpty()
等接口順利取出返回值(詳見Zend\EventManager\ResponseCollection
).
例6:在綁定了多個(gè)事件的時(shí)候,如何條件的停止事件繼續(xù)觸發(fā)?
到目前為止,我們可以順利的使用zend-eventmanager模塊了,但是,一個(gè)事件一旦被觸發(fā),其Listener將被盡數(shù)執(zhí)行,有沒有方法在某種條件下停止執(zhí)行呢?
zend-eventmanager還提供了兩種觸發(fā)方式triggerUntil()
和triggerEventUntil()
,用于條件停止事件繼續(xù)執(zhí)行.
//綁定
$events->attach('simpleEvent', function () { return 'Lucy'; }, 100);
//再次綁定
$events->attach('simpleEvent', function () { return 'XiaoMing'; }, 99);
$events->attach('simpleEvent', function () { return 'Lily'; }, 98);
//當(dāng)遇到返回值XiaoMing時(shí)停止執(zhí)行
$responses = $events->triggerUntil(function ($name) {
if($name === 'XiaoMing')
return TRUE;
return FALSE;
},'simpleEvent');
while(! $reponses->isEmpty()) {
echo $responses->pop();
}
//輸出:
//XiaoMing
//Lucy
由上例可見,最后一個(gè)綁定的Listener沒有執(zhí)行.
triggerUntil()
和triggerEventUntil()
兩個(gè)借口和trigger()
以及triggerEvent()
分類似,只不過加了第一個(gè)參數(shù),一個(gè)判斷是否停止事件繼續(xù)執(zhí)行的匿名函數(shù),傳入這個(gè)匿名函數(shù)的參數(shù)是當(dāng)前Listener的返回值.
另外,Event的stopPropagation()
接口被調(diào)用也會(huì)停止當(dāng)前事件繼續(xù)執(zhí)行.這個(gè)比較簡(jiǎn)單,不做演示.
例7:假設(shè)有兩個(gè)事件event1和event2,event1先執(zhí)行,如何在event1執(zhí)行時(shí)動(dòng)態(tài)的給event2添加Listener?
use Zend\EventManager\EventManager;
use Zend\EventManager\SharedEventManager;
//實(shí)例化SharedEventManager
$shared = new SharedEventManager;
//實(shí)例化EventManager并注入SharedEventManager
$events = new EventManager($shared);
//綁定event1的Listener
$events->attach('event1', function () { echo 'listener1 from event1'; }, 100);
$events->attach('event1', function () { echo 'listener2 from event1'; }, 99);
//利用SharedEventManager向event2綁定Listener,標(biāo)識(shí)為SharedEvent
$events->getSharedManager()->attach('sharedEvent', 'event2', function () { echo 'listener3 from event1 shared'; });
//觸發(fā)event1
$events->trigger('event1');
//輸出:
//listener1 from event1
//listener2 from event1
SharedEventManager剛好可以滿足本例需求,SharedEventManager有兩個(gè)指標(biāo)來確定一個(gè)Listener何時(shí)觸發(fā),Identifiers
和事件名
,也就是上例中的'sharedEvent'和'event2'.EventManager取得SharedEventManager中的事件時(shí),會(huì)對(duì)比EventManager當(dāng)前的Identifiers和所觸發(fā)的事件名,然后針對(duì)的觸發(fā)Listener.
以下是出發(fā)event1為event2綁定的Listener,標(biāo)識(shí)為SharedEvent.
//設(shè)置當(dāng)前EventManager的標(biāo)識(shí),以取得SharedEventManager對(duì)應(yīng)的事件.
$events->setIdentifiers(['sharedEvent', 'otherIdentifiers']);
$events->attach('event2', function () { echo 'listener4 from event2'; }, -100);
$events->trigger('event2');
//輸出:
//listener3 from event1 shared
//listener4 from event2
以上就是zend-eventmanager的主要用法.
嘗試解答那位網(wǎng)友的問題,是不是可以通過事件驅(qū)動(dòng)來解決問題?
業(yè)務(wù)中遇到較為冗長(zhǎng)if...else...時(shí),可以將其中的邏輯全部封裝為多個(gè)獨(dú)立的Listener,然后綁定到設(shè)計(jì)好的多個(gè)事件中,然后使用
triggerUntil()
和triggerEventUntil()
,傳入用于判斷的匿名函數(shù).或者根據(jù)情況動(dòng)態(tài)的綁定事件.