laravel 函數(shù)解耦

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.

在本章,我們將討論如何解耦各種處理函數(shù):隊(duì)列處理函數(shù)、事件處理函數(shù),甚至其他“事件型”的結(jié)構(gòu)如路由過(guò)濾器。

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.

大部分的“處理函數(shù)”可以被當(dāng)作傳輸層組件。也就是說(shuō),隊(duì)列觸發(fā)器、被觸發(fā)的事件、或者外部發(fā)來(lái)的請(qǐng)求等都可能調(diào)用處理函數(shù)??梢园烟幚砗瘮?shù)理解為控制器,避免在里面堆積太多具體業(yè)務(wù)邏輯實(shí)現(xiàn)

Decoupling Handlers 解耦處理函數(shù)

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:

接下來(lái)我們看一個(gè)例子??紤]有一個(gè)隊(duì)列處理函數(shù)用來(lái)給用戶發(fā)送手機(jī)短信。信息發(fā)送后,處理函數(shù)還要記錄消息日志來(lái)保存給用戶發(fā)送的消息歷史。代碼應(yīng)該看起來(lái)是這樣:

<!-- 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.

簡(jiǎn)單審查下這個(gè)類,你可能會(huì)發(fā)現(xiàn)一些問(wèn)題。首先,它難以測(cè)試。在fire方法里直接使用了Twilio_SMS類,意味著我們沒(méi)法注入一個(gè)模擬的服務(wù)(譯者注:即一旦測(cè)試則必須發(fā)送一條真實(shí)的短信)。第二,我們直接使用了Eloquent,導(dǎo)致在測(cè)試時(shí)肯定會(huì)對(duì)數(shù)據(jù)庫(kù)造成影響。第三,我們沒(méi)法在隊(duì)列外面發(fā)送短信,想在隊(duì)列外面發(fā)還要重寫(xiě)一遍代碼。也就是說(shuō)我們的短信發(fā)送邏輯和Laravel的隊(duì)列耦合太多了。

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.

將里面的邏輯抽出成為一個(gè)單獨(dú)的“服務(wù)”類,我們即可將短信發(fā)送邏輯和Laravel的隊(duì)列解耦。這樣我們就可以在應(yīng)用的任何位置發(fā)送短信了。我們將其解耦的過(guò)程,也令其變得更易于測(cè)試。
那么我們來(lái)稍微改一改:

<!-- 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:

在本重構(gòu)的例子中,我們將短信發(fā)送邏輯抽出到User模型里。同時(shí)我們將SmsCourierInterface的實(shí)現(xiàn)注入到該方法里,這樣我們可以更容易對(duì)該方法進(jìn)行測(cè)試?,F(xiàn)在我們已經(jīng)重構(gòu)了短信發(fā)送邏輯,讓我們?cè)僦貙?xiě)隊(duì)列處理函數(shù):

<!-- 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:

你可以看到我們重構(gòu)了代碼,使得隊(duì)列處理函數(shù)更輕量化了。它本質(zhì)上變成了隊(duì)列系統(tǒng)和你真正的業(yè)務(wù)邏輯之間的轉(zhuǎn)換層。這可是很了不起!這意味著我們可以很輕松的脫離隊(duì)列系統(tǒng)來(lái)發(fā)送短信息。最后,讓我們?yōu)槎绦虐l(fā)送邏輯寫(xiě)一些測(cè)試代碼:

<!-- 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'; //譯者注: 應(yīng)當(dāng)為 phone_number
        $user->sendMessage($courier, 'Test');
    }
}

Other Handlers 其他處理函數(shù)

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.

使用類似的方式,我們可以改進(jìn)和解耦很多其他類型的“處理函數(shù)”。將這些處理函數(shù)限制在轉(zhuǎn)換層的狀態(tài),你可以將你龐大的業(yè)務(wù)邏輯和框架解耦,并保持整潔的代碼結(jié)構(gòu)。為了鞏固這種思想,我們來(lái)看看一個(gè)路由過(guò)濾器。該過(guò)濾器用來(lái)驗(yàn)證當(dāng)前用戶是否是交過(guò)錢的高級(jí)用戶套餐。

<!-- 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!

猛一看這路由過(guò)濾器沒(méi)什么問(wèn)題啊。這么簡(jiǎn)單的過(guò)濾器能有什么錯(cuò)誤?然而就是是這么小的過(guò)濾器,我們卻將我們應(yīng)用實(shí)現(xiàn)的細(xì)節(jié)暴露了出來(lái)。要注意我們?cè)谠撨^(guò)濾器里是寫(xiě)明了要檢查plan變量。這使得將“套餐方案”在我們應(yīng)用中的代表值(譯者注:即plan變量的值)暴露在了路由/傳輸層里面?,F(xiàn)在我們?nèi)粝胝{(diào)整“高級(jí)套餐”在數(shù)據(jù)庫(kù)或用戶模型的代表值,我們竟然就需要改這個(gè)路由過(guò)濾器!
讓我們簡(jiǎn)單改一點(diǎn)兒:

<!-- 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!

小小的改變就帶來(lái)巨大的效果,并且代價(jià)也很小。我們將判斷用戶是否使用高級(jí)套餐的邏輯放在了用戶模型里,這樣就從路由過(guò)濾器里去掉了對(duì)套餐判斷的實(shí)現(xiàn)細(xì)節(jié)。我們的過(guò)濾器不再需要知道具體怎么判斷用戶是不是高級(jí)套餐了,它只要簡(jiǎn)單的把這個(gè)問(wèn)題交給用戶模型?,F(xiàn)在如果我們想調(diào)整高級(jí)套餐在數(shù)據(jù)庫(kù)里的細(xì)節(jié),也不必再去改動(dòng)路由過(guò)濾器了!

Who Is Responsible? 誰(shuí)負(fù)責(zé)?

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.

在這里我們又一次討論了責(zé)任的概念。記住,始終保持一個(gè)類應(yīng)該有什么樣的責(zé)任,應(yīng)該知道什么。避免在處理函數(shù)這種傳輸層直接編寫(xiě)太多你應(yīng)用的業(yè)務(wù)邏輯。
譯者注:本文多次出現(xiàn)transport layer, translation layer,分別譯作傳輸層和轉(zhuǎn)換層。其實(shí)他們應(yīng)當(dāng)指代的同一種東西。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi閱讀 7,841評(píng)論 0 10
  • “我有一個(gè)文學(xué)夢(mèng),不想只是在夢(mèng)里!” 寫(xiě)自我介紹時(shí),有一項(xiàng)內(nèi)容是問(wèn)“加入007的理由是什么”,我想為了...
    阿雪姑釀閱讀 954評(píng)論 3 3
  • 三年不動(dòng)手寫(xiě)日志了,忙著接收操蛋的現(xiàn)實(shí)的“洗禮”,忙著在現(xiàn)實(shí)的泥濘里摸爬滾打,弄得一身塵土, 忙著折騰,忙著長(zhǎng)大。...
    姍姍何來(lái)遲閱讀 484評(píng)論 3 7
  • 周杰倫和昆凌大婚仿佛還是昨天的事,但如今他們已經(jīng)是幸福的四口之家了。周杰倫從一個(gè)桀驁不馴的少年變成小心翼翼抱著閨女...
    八卦CJ社長(zhǎng)閱讀 949評(píng)論 0 0
  • 前面講了servlet入門(mén)實(shí)踐現(xiàn)在開(kāi)始介紹jsp入門(mén)實(shí)踐,開(kāi)發(fā)環(huán)境的搭建請(qǐng)參考我前面的tomcat的文章,jsp入...
    伊豚wall閱讀 3,428評(píng)論 2 56

友情鏈接更多精彩內(nèi)容