laravel 函数解耦

Introduction 介绍

In this chapter, we'll discuss tips for decoupling various handlers like queue and event handlers, as well as other "event-like" structures such as route filters.

在本章,我们将讨论如何解耦各种处理函数:队列处理函数、事件处理函数,甚至其他“事件型”的结构如路由过滤器。

Don't Clog Your Transport Layer 不要堵塞传输层

Most "handlers" can be considered transport layer components. In other words, they receive calls through something like queue workers, a dispatched event, or an incoming request. Treat these handlers like controllers, and avoid clogging them up with the implementation details of your application.

大部分的“处理函数”可以被当作传输层组件。也就是说,队列触发器、被触发的事件、或者外部发来的请求等都可能调用处理函数。可以把处理函数理解为控制器,避免在里面堆积太多具体业务逻辑实现

Decoupling Handlers 解耦处理函数

To get started, let's jump right into an example. Consider a queue handler that sends an SMS message to a user. After sending the message, the handler logs that message so we can keep a history of all SMS messages we have sent to that user. The code might look something like this:

接下来我们看一个例子。考虑有一个队列处理函数用来给用户发送手机短信。信息发送后,处理函数还要记录消息日志来保存给用户发送的消息历史。代码应该看起来是这样:

<!-- lang: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();
    }
}

Just by examining this class, you can probably spot several problems. First, it is hard to test. The Twilio_SMS class is instantiated inside of the fire method, meaning we will not be able to inject a mock service. Secondly, we are using Eloquent directly in the handler, thus creating a second testing problem as we will have to hit a real database to test this code. Finally, we are unable to send SMS messages outside of the queue. All of our SMS sending logic is tightly coupled to the Laravel queue.

简单审查下这个类,你可能会发现一些问题。首先,它难以测试。在fire方法里直接使用了Twilio_SMS类,意味着我们没法注入一个模拟的服务(译者注:即一旦测试则必须发送一条真实的短信)。第二,我们直接使用了Eloquent,导致在测试时肯定会对数据库造成影响。第三,我们没法在队列外面发送短信,想在队列外面发还要重写一遍代码。也就是说我们的短信发送逻辑和Laravel的队列耦合太多了。

By extracting this logic into a separate "service" class, we can decouple our application's SMS sending logic from Laravel's queue. This will allow us to send SMS messages from anywhere in our application. While we are decoupling this process from the queue, we will also refactor it to be more testable.

将里面的逻辑抽出成为一个单独的“服务”类,我们即可将短信发送逻辑和Laravel的队列解耦。这样我们就可以在应用的任何位置发送短信了。我们将其解耦的过程,也令其变得更易于测试。
那么我们来稍微改一改:

<!-- lang:php -->
class User extends Eloquent {
    /**
     * Send the User an SMS message
     *
     * [@param](https://my.oschina.net/u/2303379) SmsCourierInterface $courier
     * [@param](https://my.oschina.net/u/2303379) string $message
     * [@return](https://my.oschina.net/u/556800) SmsMessage
     */
    public function sendSmsMessage(SmsCourierInterface $courier, $message)
    {
        $courier->sendMessage($this->phone_number, $message);
        return $this->sms()->create(array(
            'to'=> $this->phone_number,
            'message'=> $message,
        ));
    }
}

In this refactored example, we have extracted the SMS sending logic into the User model. We are also injecting a SmsCourierInterface implementation into the method, allowing us to better test that aspect of the process. Now that we have refactored this logic, let's re-write our queue handler:

在本重构的例子中,我们将短信发送逻辑抽出到User模型里。同时我们将SmsCourierInterface的实现注入到该方法里,这样我们可以更容易对该方法进行测试。现在我们已经重构了短信发送逻辑,让我们再重写队列处理函数:

<!-- lang: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();
    }
}

As you can see in this refactored example, our queue handler is now much lighter. It essentially serves as a translation layer between the queue and your real application logic. That is great! It means that we can easily send SMS message s outside of the queue context. Finally, let's write some tests for our SMS sending logic:

你可以看到我们重构了代码,使得队列处理函数更轻量化了。它本质上变成了队列系统和你真正的业务逻辑之间的转换层。这可是很了不起!这意味着我们可以很轻松的脱离队列系统来发送短信息。最后,让我们为短信发送逻辑写一些测试代码:

<!-- lang:php -->
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'; //译者注: 应当为 phone_number
        $user->sendMessage($courier, 'Test');
    }
}

Other Handlers 其他处理函数

We can improve many other types of "handlers" using this same approach to decoupling. By restricting all handlers to being simple translation layers, you can keep your heavy business logic neatly organized and decoupled from the rest of the framework. To drive the point home further, let's examine a route filter that verifies that the current user of our application is subscribed to our "premium" pricing tier.

使用类似的方式,我们可以改进和解耦很多其他类型的“处理函数”。将这些处理函数限制在转换层的状态,你可以将你庞大的业务逻辑和框架解耦,并保持整洁的代码结构。为了巩固这种思想,我们来看看一个路由过滤器。该过滤器用来验证当前用户是否是交过钱的高级用户套餐。

<!-- lang:php -->
Route::filter('premium', function()
{
    return Auth::user() && Auth::user()->plan == 'premium';
});

On first glance, this route filter looks very innocent. What could possibly be wrong with a filter that is so small? However, even in this small filter, we are leaking implementation details of our application into the code. Notice that we are manually checking the value of the plan variable. We have tightly coupled the representation of "plans" in our business layer into our routing / transport layer. Now, if we change how the "premium" plan is represented in our database or user model, we will need to change this route filter!

猛一看这路由过滤器没什么问题啊。这么简单的过滤器能有什么错误?然而就是是这么小的过滤器,我们却将我们应用实现的细节暴露了出来。要注意我们在该过滤器里是写明了要检查plan变量。这使得将“套餐方案”在我们应用中的代表值(译者注:即plan变量的值)暴露在了路由/传输层里面。现在我们若想调整“高级套餐”在数据库或用户模型的代表值,我们竟然就需要改这个路由过滤器!
让我们简单改一点儿:

<!-- lang:php -->
Route::filter('premium', function()
{
    return Auth::user() && Auth::user()->isPremium();
});

A small change like this has great benefits and very little cost. By deferring the determination of whether a user is on the premium plan to the model, we have removed all implementation details from our route filter. Our filter is no longer responsible for knowing how to determine if a user is on the premium plan. Instead, it simply asks the User model. Now, if the representation of premium plans changes in the database, there is no need to update the route filter!

小小的改变就带来巨大的效果,并且代价也很小。我们将判断用户是否使用高级套餐的逻辑放在了用户模型里,这样就从路由过滤器里去掉了对套餐判断的实现细节。我们的过滤器不再需要知道具体怎么判断用户是不是高级套餐了,它只要简单的把这个问题交给用户模型。现在如果我们想调整高级套餐在数据库里的细节,也不必再去改动路由过滤器了!

Who Is Responsible? 谁负责?

Again we find ourselves exploring the concept of responsibility. Remember, always be considering a class' responsibility and knowledge. Avoid making your transport layer, such as handler, responsible for knowledge about your application and business logic.

在这里我们又一次讨论了责任的概念。记住,始终保持一个类应该有什么样的责任,应该知道什么。避免在处理函数这种传输层直接编写太多你应用的业务逻辑。
译者注:本文多次出现transport layer, translation layer,分别译作传输层和转换层。其实他们应当指代的同一种东西。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi阅读 7,336评论 0 10
  • “我有一个文学梦,不想只是在梦里!” 写自我介绍时,有一项内容是问“加入007的理由是什么”,我想为了...
    阿雪姑酿阅读 862评论 3 3
  • 三年不动手写日志了,忙着接收操蛋的现实的“洗礼”,忙着在现实的泥泞里摸爬滚打,弄得一身尘土, 忙着折腾,忙着长大。...
    姗姗何来迟阅读 462评论 3 7
  • 周杰伦和昆凌大婚仿佛还是昨天的事,但如今他们已经是幸福的四口之家了。周杰伦从一个桀骜不驯的少年变成小心翼翼抱着闺女...
    八卦CJ社长阅读 890评论 0 0
  • 前面讲了servlet入门实践现在开始介绍jsp入门实践,开发环境的搭建请参考我前面的tomcat的文章,jsp入...
    伊豚wall阅读 3,370评论 2 56