Applied Architecture Decoupling Handlers 實用做法:解耦處理函數

Applied Architecture Decoupling Handlers 實用做法:解耦處理函數

Introduction 介紹

Now that we have discussed various aspects of sound application architecture using Laravel 4, Let’s dig into some more specifics. 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.

我們已經討論了用Laravel4製作優美的程序架構的各個方面,讓我們再深入一些細節。在本章,我們將討論如何解耦各種處理函數:隊列處理函數、事件處理函數,甚至其他“事件型”的結構如路由過濾器。

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的隊列解耦。這樣我們就可以在應用的任何位置發送短信了。我們將其解耦的過程,也令其變得更易於測試。

So, let’s examine an alternative:

那麼我們來稍微改一改:

<!-- lang: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(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變量的值)暴露在了路由/傳輸層裏面。現在我們若想調整“高級套餐”在數據庫或用戶模型的代表值,我們竟然就需要改這個路由過濾器!

Instead, let’s make a very simple change:

讓我們簡單改一點兒:

<!-- 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,分別譯作傳輸層和轉換層。其實他們應當指代的同一種東西。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章