上一篇php
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 製做優美的程序架構的各個方面,讓咱們再深刻一些細節。在本章,咱們將討論如何解耦各類處理函數:隊列處理函數、事件處理函數,甚至其餘「事件型」的結構如路由過濾器。api
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.架構
大部分的「處理函數」能夠被看成傳輸層組件。也就是說,隊列觸發器、被觸發的事件、或者外部發來的請求等均可能調用處理函數。能夠把處理函數理解爲控制器,避免在裏面堆積太多具體業務邏輯實現。app
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:框架
接下來咱們看一個例子。考慮有一個隊列處理函數用來給用戶發送手機短信。信息發送後,處理函數還要記錄消息日誌來保存給用戶發送的消息歷史。代碼應該看起來是這樣:ide
<!-- 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.this
將裏面的邏輯抽出成爲一個單獨的「服務」類,咱們便可將短信發送邏輯和Laravel的隊列解耦。這樣咱們就能夠在應用的任何位置發送短信了。咱們將其解耦的過程,也令其變得更易於測試。
So, let's examine an alternative:
那麼咱們來稍微改一改:
<!-- 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'); } }
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,分別譯做傳輸層和轉換層。其實他們應當指代的同一種東西。