Laravel最佳實踐 -- 事件驅動程式設計

kevinyan發表於2018-06-12

在這篇文章中我們將瞭解到什麼是“事件驅動程式設計”以及在Laravel中如何開始構建一個事件驅動應用,同時我們還將看到如何通過事件驅動程式設計來對應用程式的邏輯進行解耦。

在開始之前,先說明一下這篇文章主要是闡述事件驅動這種程式設計思維和理念的,所以不會涉及到Laravel Events的方方面面。如果你需要更全面地瞭解Laravel Events和它的各種用法可以訪問Laravel Events文件來了解詳細資訊。

何為事件驅動程式設計

在我們深入事件驅動應用之前,我們先看一下在維基百科裡對事件驅動程式設計的定義:

事件驅動程式設計是一種程式設計模式,其中的程式流由諸如使用者動作(滑鼠點選,按鍵)、感測器輸出或來自其他程式/執行緒的訊息等事件來決定確定。事件驅動程式設計是圖形使用者介面和其他應用程式(例如JavaScript Web應用程式)中使用的主要範例,用於執行某些操作來響應使用者輸入。

事件驅動應用程式會響應使用者的動作,然後執行對應的程式碼來響應使用者的動作。

Laravel Events

通過上面的定義,事件是發生在應用程式中的動作。Javascript的事件是像滑鼠點選、滑鼠懸浮、按下鍵盤這樣的使用者動作。在Laravel中事件是發生在應用程式中的動作,像郵件通知、記錄日誌、使用者註冊、CRUD操作等。Laravel Events系統提供了簡易的觀察者模式實現,讓開發者能夠訂閱和監聽發生在應用中的動作。

應用中有些事件是由Laravel框架自動發起。比如說當使用Eloquent Model執行create、save、update或者delete操作時Laravel將分別發起createdsavedupdated、和deleted事件。如果需要的話我們可以監聽這些事件從而執行相應的程式碼來完成自己的需求。除了Laravel框架自動發起的事件,我們還可以根據自己應用的需要讓Laravel發起我們自己定義的事件。比如說你可以發起一個userRegistered事件,在事件處理程式中傳送使用者驗證郵件好讓新註冊的使用者能夠驗證自己的郵箱。

發起一個事件並不會讓應用程式執行任何相應的操作,我們必須在事件處理程式中對被髮起的事件進行相應地回應。Laravel Events由兩部分組成Event HandlerEvent ListenerEvent Handler中包含了發起事件相關的資訊。Event Listener監聽事件物件並對事件進行回應,Event Listener是我們實現事件邏輯的地方。在Laravel中Event類檔案被存放在app/Events目錄,Listener類檔案被存放在app/Listeners目錄。

為何使用事件驅動程式設計

我們已經瞭解事件驅動應用和Laravel Events的概念了,你可能會好奇為什麼要採用事件驅動這種方法來構建你的應用程式。我們來看一下事件驅動程式設計帶來的收益。

首先,事件是一種解耦應用程式各個方面的好方法,因為單個事件可以有多個不依賴於彼此的監聽器。通過解耦,不會因為你使用了不適合域邏輯的程式碼而汙染了程式碼庫。其次,由於應用程式是鬆散耦合的,你可以輕鬆擴充套件應用程式的功能,而不必打亂/重寫應用程式或應用程式的某些其他功能。

應用示例

現在假設新使用者註冊了我們的應用程式後,應用程式會給使用者傳送一封歡迎郵件,同時會自動給使用者訂閱應用上的每週新聞簡報。在不應用事件驅動方式的情況下程式碼往往是如下這樣:

// without event-driven approach

public function register(Request $request)
{
    // validate input
    $this->validate($request->all(), [
      'name' => 'required',
      'email' => 'required|unique:users',
      'password' => 'required|min:6|confirmed',
    ]);

    // create user and persist in database
    $user = $this->create($request->all());

    // send welcome email
    Mail::to($user)->send(new WelcomeToSiteName($user));

    // Sign user up for weekly newsletter
    Newsletter::subscribe($user->email, [
      'FNAME': $user->fname,
      'LNAME': $user->lname
    ], 'SiteName Weekly');

    // login newly registered user
    $this->guard()->login($user);

    return redirect('/home');
}
複製程式碼

你可以看到傳送歡迎郵件和訂閱新聞簡報的邏輯緊密耦合到了register方法裡, 根據關注點分離原則register方法不應該關心傳送歡迎郵件和訂閱新聞簡報的具體實現。你可能會覺得傳送歡迎郵件和訂閱新聞放到register方法裡也沒什麼,但是如果在註冊時除了傳送郵件還要給使用者傳送簡訊呢?繼續寫在register方法裡:

public function register(Request $request)
{
    // validate input

    // create user and persist in database

    // send welcome email
    Mail::to($user)->send(new WelcomeToSiteName($user));

    // send SMS
    Nexmo::message()->send([
      'to' => $user->phone_number,
      'from' => 'SiteName',
      'text' => 'Welcome and thanks for signup on SiteName.'
    ]);

    // Sign user up for weekly newsletter
    Newsletter::subscribe($user->email, [
      'FNAME': $user->fname,
      'LNAME': $user->lname
    ], 'SiteName Weekly');

    // login newly registered user

    return redirect('/home');
}
複製程式碼

可以看到程式碼庫開始變得臃腫。現在讓我們看看採用事件驅動程式設計方法如何實現上述相同的功能。

// with event-driven approach

public function register(Request $request)
{
    // validate input
    $this->validate($request->all(), [
      'name' => 'required',
      'email' => 'required|unique:users',
      'password' => 'required|min:6|confirmed',
    ]);

    // create user and persist in database
    $user = $this->create($request->all());

    // fire event once user has been created
    event(new UserRegistered($user));

    // login newly registered user
    $this->guard()->login($user);

    return redirect('/home');
}
複製程式碼

一旦建立了使用者,UserRegistered事件就會被觸發。回想一下,我們之前提到,發起一個事件後應用並不會自己做任何事情,我們需要監聽UserRegistered事件並執行必要的操作。讓我們建立UserRegistered事件類和SendWelcomeMail以及SignupForWeeklyNewsletter監聽器類:

php artisan make:event UserRegistered
php artisan make:listener SendWelcomeMail --event=UserRegistered
php artisan make:listener SignupForWeeklyNewsletter --event=UserRegistered
複製程式碼

事件和監聽器之間的對應關係需要註冊到EventServiceProvider的$listen屬性裡:

protected $listen = [
    UserRegistered::class => [
        SendWelcomeMail::class,
        SignupForWeeklyNewsletter::class,
    ],
];
複製程式碼

開啟app/Events/UserRegistered.php檔案更新它的構造方法:

public $user;

public function __construct(User $user)
{
  $this->user = $user;
}
複製程式碼

宣告$user為public,它將被傳遞給監聽器,而監聽器可以用它來執行必要的邏輯。接下來,事件監聽器將在其handle方法中接收到事件例項。在handle方法中,我們可以執行響應事件的操作。

// app/Listeners/SendWelcomeMail.php
public function handle(UserRegistered $event)
{
  // send welcome email
  Mail::to($event->user)->send(new WelcomeToSiteName($event->user));
}


// app/Listeners/SignupForWeeklyNewsletter.php
public function handle(UserRegistered $event)
{
  // Sign user up for weekly newsletter
  Newsletter::subscribe($event->user->email, [
    'FNAME': $event->user->fname,
    'LNAME': $event->user->lname
  ], 'SiteName Weekly');
}
複製程式碼

可以看到通過事件驅動的方式我們讓register方法的程式碼儘可能的少並且專注於使用者註冊這件事上,其它的邏輯由UserRegistered事件的監聽器來負責,現在如果說我們想在使用者註冊後傳送簡訊給新註冊的使用者,我們所要做的就是建立一個新的事件監聽器來監聽UserRegistered事件何時被觸發

php artisan make:listener SendWelcomeSMS --event=UserRegistered

// app/Listeners/SendWelcomeSMS.php
public function handle(UserRegistered $event)
{
  // send SMS
  Nexmo::message()->send([
    'to' => $event->user->phone_number,
    'from' => 'SiteName',
    'text' => 'Welcome and thanks for signup on SiteName.'
  ]);
}
複製程式碼

注:記得要更新EventServiceProvider裡的$listen屬性

總結

在這篇文章中,我們已經能夠理解事件驅動的程式設計是什麼,事件驅動的應用程式是什麼以及Laravel事件是什麼。我們還研究了事件驅動應用程式的優勢。但是,像跟所有有積極影響的程式設計概念一樣,它也有缺點。事件驅動型應用程式的主要缺點是讓程式流變得複雜了,尤其一些剛接觸開發的人可能很難真正理解應用程式的流程。以上面的實現為例,通過register方法我們並不能直觀地看到程式在建立使用者後會向新使用者傳送一封歡迎郵件,並將其註冊到新聞通訊中。

所以在開發中應該根據場景創造性地使用它,利用它的優勢為你的應用程式解耦,而不是過度使用它。

本文已經收錄在系列文章Laravel核心程式碼學習裡,歡迎訪問閱讀。

相關文章