宣告:本文並非博主原創,而是來自對《Laravel 4 From Apprentice to Artisan》閱讀的翻譯和理解,當然也不是原汁原味的翻譯,能保證90%的原汁性,另外因為是理解翻譯,肯定會有錯誤的地方,歡迎指正。
歡迎轉載,轉載請註明出處,謝謝!
應用體系結構:解耦事件處理器
介紹
現在我們已經介紹了很多使用Laravel 4構建健壯應用的特性,下面來深入挖掘更多的細節。本章我們將討論諸如佇列、事件這些眾多事件處理器的解耦,也包括類似“類事件”結構的路由過濾。
別堵塞了傳輸層
大多數“事件處理器”被當作_傳輸層_元件。換言之,佇列處理、事件觸發器、或者一個外來請求都被用來呼叫某些呼叫處理。要像處理控制器一樣處理這些事件處理器,並避免在其中涉及太多業務邏輯。
解耦事件處理器
開始本命題前,我們來使用一個示例。假想下把佇列處理器用來傳送SMS訊息給使用者。在傳送訊息之後,處理器講傳送了的訊息記錄成歷史以便我們知道有哪些使用者收到了這些訊息。程式碼實現如下:
class SendSMS{
public function fire($job, $data)
{
$twilio = new Twilio_SMS($apiKey);
$twilio->sendTextMessage(array(
`to`=> $data[`user`][`phone_number`],
`message`=> $data[`message`],
));
$user = User::find($data[`user`][`id`]);
$user->messages()->create(array(
`to`=> $data[`user`][`phone_number`],
`message`=> $data[`message`],
));
$job->delete();
}
}
僅測試這塊程式碼,就可能遇到一些問題。首先,測試困難。Twilio_SMS
類是在fire
方法中例項化的,這意味著我們無法使用注入的方式模擬服務。其次,在處理器中我們直接用到了Eloquent模型,這就給測試帶來了另外一個問題,我們必須在方法中進行真正的資料庫訪問。最後,我們在佇列之外無法進行SMS訊息傳送。我們的SMS訊息傳送邏輯完全糅合在Laravel佇列中了。
通過將邏輯提取到某一“服務”中的方法,我們可以將應用中的SMS訊息傳送邏輯從Laravel的佇列服務中解耦出來。從而可以在應用中的任何地方傳送訊息。當我們進行了這種解耦處理,這種重構也是我們的程式碼變得更加具有可測性。
讓我們來修改下程式碼:
class User extends Eloquent {
/**
* Send the User an SMS message
*
* @param SmsCourierInterface $courier
* @param string $message
* @return SmsMessage
*/
public function sendSmsMessage(SmsCourierInterface $courier, $message)
{
$courier->sendMessage($this->phone_number, $message);
return $this->sms()->create(array(
`to`=> $this->phone_number,
`message`=> $message,
));
}
}
在這個重構的程式碼例項中,我們將傳送訊息的邏輯提取到User
模型中。同時向該方法中注入SmsCourierInterface
介面實現邏輯,使我們更好的測試邏輯中的方方面面。重構了簡訊傳送邏輯之後,再對佇列進行重構:
class SendSMS {
public function __construct(UserRepository $users, SmsCourierInterface $courier)
{
$this->users = $users;
$this->courier = $courier;
}
public function fire($job, $data)
{
$user = $this->users->find($data[`user`][`id`]);
$user->sendSmsMessage($this->courier, $data[`message`]);
$job->delete();
}
}
在重構的示例中,可以看到,佇列服務已經足夠輕量。它在佇列和我們_真正的_應用邏輯之間已經足夠符合_傳輸層_這個概念。贊!這意味著我們可以在佇列之外輕易的傳送訊息。最後,讓我們編寫一些測試程式碼:
class SmsTest extends PHPUnit_Framework_TestCase {
public function testUserCanBeSentSmsMessages()
{
/**
* Arrage ...
*/
$user = Mockery::mock(`User[sms]`);
$relation = Mockery::mock(`StdClass`);
$courier = Mockery::mock(`SmsCourierInterface`);
$user->shouldReceive(`sms`)->once()->andReturn($relation);
$relation->shouldReceive(`create`)->once()->with(array(
`to` => `555-555-5555`,
`message` => `Test`,
));
$courier->shouldReceive(`sendMessage`)->once()->with(
`555-555-5555`, `Test`
);
/**
* Act ...
*/
$user->sms_number = `555-555-5555`;
$user->sendMessage($courier, `Test`);
}
}
其他事件處理器
我們可以改進很多這種型別的“事件處理器”。將他們限定為簡單的“傳輸層”來使用,能將複雜的業務邏輯很好的組織和解耦到框架之外。為了鞏固下這種思想,下面我們舉例一個路由過濾器,用它來驗證使用者是否為我們的“高階”訂閱使用者。
Route::filter(`premium`, function()
{
return Auth::user() && Auth::user()->plan == `premium`;
});
乍看像是沒什麼問題。這麼小的程式碼能有啥問題呢?然而,在這麼小的過濾中,也能意識到我們將應用的實現細節暴漏了出來。注意,我們在過濾中進行對plan
屬性進行了檢測。“級別”的檢測邏輯層緊緊的揉進了路由、傳輸層。如果我們將“高階”訂閱使用者的套餐存放到資料庫或者使用者模型中,這裡又必須對我們的路由過濾器進行修改!
相應的,做些小的改編:
Route::filter(`premium`, function()
{
return Auth::user() && Auth::user()->isPremium();
});
這樣小的改編帶來的效果是明顯的,付出的代價也是小的。通過在模型中對使用者是否屬於高階訂閱使用者的判斷,我們將路由中的檢測邏輯解耦了出來。我們的過濾程式不在負責檢測使用者訂閱級別的職責。相應的,它只需簡單的詢問使用者模型即可。現在,如果訂閱級別的判斷存放在資料庫中,路由過濾不需要更改任何程式碼!
該誰負責?
我們又一次討論了_職責_的概念。牢記,一個類應有的職責是什麼,和他涉及的範圍是明確的。儘量避免在事件處理器中摻雜太多的業務邏輯。