声明:本文并非博主原创,而是来自对《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();
});
这样小的改编带来的效果是明显的,付出的代价也是小的。通过在模型中对用户是否属于高级订阅用户的判断,我们将路由中的检测逻辑解耦了出来。我们的过滤程序不在负责检测用户订阅级别的职责。相应的,它只需简单的询问用户模型即可。现在,如果订阅级别的判断存放在数据库中,路由过滤不需要更改任何代码!
该谁负责?
我们又一次讨论了_职责_的概念。牢记,一个类应有的职责是什么,和他涉及的范围是明确的。尽量避免在事件处理器中掺杂太多的业务逻辑。