### 簡介
我們已經學習了通過 Laravel 構建架構良好的應用的各個方面,接下來,讓我們再深入探討一些細節。在本章,我們將討論如何解耦各種處理器:隊列處理器、事件處理器,甚至其他「類似事件」的結構,比如路由過濾器。
> 大部分的「處理器」可以被當作傳輸層組件。也就是說,它們通過隊列處理器、被觸發的事件、或者外部發來的請求等接收調用。這樣一來,我們可以將這些處理器理解為控制器,同樣需要避免在它們內部堆積太多具體的業務邏輯實現。
### 解耦處理器
首先,我們看一個例子。假設有一個隊列處理器用來給用戶發送手機短信。信息發送后,處理器會記錄消息日志以便保存給用戶發送過的所有消息歷史。對應代碼如下:
```php
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();
}
}
```
簡單審查下這個類,你可能會發現一些問題。首先,它難以測試。在 `fire` 方法里直接實例化了 `Twilio_SMS` 類,意味著我們沒法注入一個模擬的服務。其次,我們直接在處理器中使用了 Eloquent 模型,導致在測試時肯定會對數據庫造成影響。最后,我們沒法在隊列以外發送短信。所有短信發送邏輯和 Laravel 隊列耦合在一起了。
通過將短信發送邏輯提取到一個單獨的「服務」類,就可以將其和 Laravel 隊列解耦。這樣我們就可以在應用的任何位置發送短信了。此外,解耦的同時也令其變得更易于測試。
那么,我們按照這個思路重構前面的代碼:
```php
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([
'to' => $this->phone_number,
'message' => $message,
]);
}
}
```
在重構后的示例代碼中,我們將短信發送邏輯提取到 `User` 模型類的 `sendSmsMessage` 方法中。同時我們將 `SmsCourierInterface` 的實現注入到該方法里,這樣我們可以更容易對該流程進行測試。現在,我們已經重構了短信發送邏輯,接下來,讓我們來重寫隊列處理器:
```php
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();
}
}
```
可以看到在重構后的代碼中,隊列處理器更加輕量化了。它實際上變成了隊列系統和真正的業務邏輯之間的轉換層。這非常好!意味著我們可以很輕松地在隊列系統之外發送短信。最后,讓我們為短信發送邏輯寫一段測試代碼:
```php
class SmsTest extends 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'; //譯者注: 應當為 phone_number
$user->sendMessage($courier, 'Test');
}
}
```
### 其他處理器
使用類似的方式,我們可以優化和解耦很多其他類型的「處理器」。通過將這些處理器限制為簡單的轉換層,你可以將繁重的業務邏輯整齊地組織起來,并且與框架的其他部分解耦。為了進一步加深理解,我們來看一個路由過濾器。該過濾器用來驗證當前用戶是否已經訂閱高級用戶套餐。
> 學院君注:路由過濾器在 Laravel 5 版本中已經廢棄,改為通過中間件來實現相應功能。所以在 Laravel 5 中可以通過中間件實現類似代碼。
```php
Route::filter('premium', function()
{
return Auth::user() && Auth::user()->plan == 'premium';
});
```
乍一看這個路由過濾器沒什么問題啊。這么簡單的過濾器能有什么錯誤?然而,即使是在這么小的一個過濾器中,我們卻將應用實現的細節暴露了出來。我們在該過濾器中手動檢查了 `plan` 變量的值,這使得將業務邏輯中「套餐方案」的表示值硬編碼到了路由/傳輸層。現在,如果想調整「高級套餐」在數據庫或用戶模型的表示值,竟然需要同步修改這個路由過濾器!
所以,我們需要調整這段代碼:
```php
Route::filter('premium', function()
{
return Auth::user() && Auth::user()->isPremium();
});
```
微小的調整帶來的是巨大的好處,并且代價也很小。我們將判斷用戶是否訂閱高級套餐的邏輯放到了用戶模型類里,這樣就從路由過濾器里移除了所有實現細節。我們的過濾器不再需要知道具體怎么判斷用戶是不是訂閱高級套餐了,取而代之的,它只要把這個問題拋給用戶模型即可。現在,如果我們想調整高級套餐在數據庫里的表示值,也不必再去更新路由過濾器了!
> 在這里我們又一次討論了職責的概念。記住,要始終明確一個類的職責邊界,該知道什么,不該知道什么,并適時進行調整和優化。避免在傳輸層(如處理器)中直接編寫應用的業務邏輯代碼。