应用架构篇
简介
我们已经学习了通过 Laravel 构建架构良好的应用的各个方面,接下来,让我们再深入探讨一些细节。在本章,我们将讨论如何解耦各种处理器:队列处理器、事件处理器,甚至其他「类似事件」的结构,比如路由过滤器。
大部分的「处理器」可以被当作传输层组件。也就是说,它们通过队列处理器、被触发的事件、或者外部发来的请求等接收调用。这样一来,我们可以将这些处理器理解为控制器,同样需要避免在它们内部堆积太多具体的业务逻辑实现。
解耦处理器
首先,我们看一个例子。假设有一个队列处理器用来给用户发送手机短信。信息发送后,处理器会记录消息日志以便保存给用户发送过的所有消息历史。对应代码如下:
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 队列解耦。这样我们就可以在应用的任何位置发送短信了。此外,解耦的同时也令其变得更易于测试。
那么,我们按照这个思路重构前面的代码:
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
的实现注入到该方法里,这样我们可以更容易对该流程进行测试。现在,我们已经重构了短信发送逻辑,接下来,让我们来重写队列处理器:
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 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 中可以通过中间件实现类似代码。
Route::filter('premium', function()
{
return Auth::user() && Auth::user()->plan == 'premium';
});
乍一看这个路由过滤器没什么问题啊。这么简单的过滤器能有什么错误?然而,即使是在这么小的一个过滤器中,我们却将应用实现的细节暴露了出来。我们在该过滤器中手动检查了 plan
变量的值,这使得将业务逻辑中「套餐方案」的表示值硬编码到了路由/传输层。现在,如果想调整「高级套餐」在数据库或用户模型的表示值,竟然需要同步修改这个路由过滤器!
所以,我们需要调整这段代码:
Route::filter('premium', function()
{
return Auth::user() && Auth::user()->isPremium();
});
微小的调整带来的是巨大的好处,并且代价也很小。我们将判断用户是否订阅高级套餐的逻辑放到了用户模型类里,这样就从路由过滤器里移除了所有实现细节。我们的过滤器不再需要知道具体怎么判断用户是不是订阅高级套餐了,取而代之的,它只要把这个问题抛给用户模型即可。现在,如果我们想调整高级套餐在数据库里的表示值,也不必再去更新路由过滤器了!
在这里我们又一次讨论了职责的概念。记住,要始终明确一个类的职责边界,该知道什么,不该知道什么,并适时进行调整和优化。避免在传输层(如处理器)中直接编写应用的业务逻辑代码。
1 Comment
学习了,谢谢学院君的好文,细节翻译到位。