1. 程式人生 > >監聽者模式在系統中的應用 —— 事件匯流排

監聽者模式在系統中的應用 —— 事件匯流排

監聽者模式 是一種比較常見的設計模式。

在日常的開發中,我們所使用的 事件 就是一種符合 監聽者模式 的功能。

對 監聽者模式 還不太明白的同學可以通過 WinForm 開發來理解這一概念。

在 WinForm 模式下,事件的使用率是非常高的,窗體中的每一個 Controller 都提供了大量的事件,諸如 ClickDoubleClickLoadFocus 等等。

為什麼會這樣設計呢?

因為,當你編寫一個與業務無關 控制元件 的時候,你應當只編寫與 顯示 相關的程式碼,以 Button 為例。

編寫一個 Button 關心的是如何畫出符合尺寸大小的按鈕,什麼顏色,什麼邊框,字的位置。至於按下這個按鈕需要執行什麼,你在編寫 Button 還不知道,必須交給 外面 去處理。

所以使用 事件 將點選的訊號傳送出去,交給外面去處理。

在我們編寫業務的時候會用到事件嗎?

很少有人會在業務程式碼中使用 事件,一個常見的資料操作流程如下:

  1. 前臺通過 Http 請求提交資料
  2. 通過 WebApi 框架內部的排程,執行某個 Controller 上的某個 Method
  3. 開發人員校驗提交資料的有效性。可能是通過直接在 Controller 中實現,也可能通過 AOP 等形式實現
  4. 將資料交由服務層處理
  5. 服務層經一定處理,將資料交由持久層處理
  6. 持久層將資料持久化

從流程上看,整個開發過程自始至終都是實現業務的過程,不像 Button 那樣,有業務相關的,有業務無關的,可以通過事件進行分離。

但事實上,業務與業務之間也是需要分離的。

舉個例子

當我們將系統中的一個使用者刪除時,大體需要做以下三件事

  1. 檢查是否可以刪除這個使用者。比如是否存在只能由此使用者才能處理的待辦事項等其它場景。
  2. 刪除使用者資料。這裡可能是物理刪除、也可能是邏輯刪除。
  3. 刪除後操作。清除刪除了此使用者後,可能存在的 孤島資料。比如該使用者的個性化配置、頭像檔案、個人網盤等等。

上述 3 個步驟中,只有 2. 適合在形如 UserService 中完成。
其它兩項並不適合,原因如下

  • UserService 是一個可能極可能被其它 Service 依賴的介面,如果在此處依賴其它 Service 就會出現迴圈依賴的現象
  • 當你在開發 UserService.Delete(User user) 時,其餘的功能肯定是沒有被開發出來的。此時開發者,還沒有能力去實現這 1. 、2. 裡的功能
  • 難維護,你可以想象你要維護形如下面的程式碼是不是就頭大。
public class UserService : IUserService
{
    private readonly SomeService1 someService1;
    private readonly SomeService2 someService2;
    private readonly SomeService3 someService3;

    public UserService(SomeService1 someService1,
                       SomeService2 someService2,
                       SomeService3 someService3)
    {
        this.someService1 = someService1;
        this.someService2 = someService2;
        this.someService3 = someService3;
        // ...
        // ...
        // ...
    }

    public void Delete(User user)
    {
        someService1.CheckCanDeleteUser(user);
        someService2.CheckCanDeleteUser(user);
        someService3.CheckCanDeleteUser(user);
        //...
        //...
        //...
        // you can add more checker here
        // but you should inject the component in the Ctor.

        this.userRepo.Delete(user);

        someService4.CleanUserData(user);
        someService5.CleanUserData(user);
        someService6.CleanUserData(user);

        // ...
        // ...
        // ...
        // you can add more cleaner here
        // but you should inject the component in the Ctor.
    }
}

形如上面的程式碼很難維護,程式碼行數也一定超出了人性化範疇。

更重要的,在一個真正的生產環境中,連上面的例子都做不到 :

  1. 不是每一個人在開發 SomeServiceX 的時候,都會留有一個 CheckCanDeleteUser 或 CleanUserData 的方法,名稱可能不太一樣,或者壓根就沒有,畢竟這不是它的業務邏輯範疇。
  2. 每個人在編寫自己範疇的 SomeServiceX 時,也不知道系統中 哪個資料 在 哪個操作 時需要自己來配合。如果每當有一個 需求 被提出都要去做一個的時候,那 SomeServiceX 也會變得非常臃腫,可能會比較像的樣子
public class SomeServiceZ : ISomeServiceZ
{
    public void CheckCanDeleteUser(User user){}

    public void CheckCanDeleteEvent(Event @event){}

    public void CheckCanChangeEventDate(Event @event, DateTime newDate);

    public void CheckCanDeleteOrder(Order order);

    public void CheckCanModifyDeliveryDate(Order order, DateTime new DeliveryDate);

    // ...
    // ...

    public void CleanOrderData(Order order);

    public void CleanUserData(User user);

    public void CleanEventData(Event @event);

    //...
    //...
}

很容易發現,最後這個 Service 不是在為自己 服務 ,而是在為系統中各種各樣的其它操作 服務

我們需要 事件

有了事件,我們只要在 UserService.Delete(User user) 內編寫兩個事件 DeletingDeleted 即可。

剩下來的事只要交給監聽這些事件的類。

public class UserService : IUserService
{
    public event EventHandler<UserDeletingEventArgs> Deleting;
    public event EventHandler<UserDeletedEventArgs> Deleted;

    public void Delete(User user)
    {
        this.Deleting?.Invoke(this, new UserDeltingEventArgs(user));
        this.userRepo.Delete(user);
        this.Deleted?.Invoke(this, new UserDeletedEventArgs(user));
    }
}

當我們滿心歡喜的寫到這兒的時候,問題又來了。

我們 什麼時候、在哪兒 監聽這個事件。

在一個使用了 IOC / DI 的 Cotnroller 中。UserService 的例項是通過 建構函式 得到的。

private readonly IUserService userService;
public UserController(IUserService userService)
{
    this.userService = userService;

    // 不知道誰監聽這個事件
    // 如何把所有需要監聽這個事件的 Service 都注入進來
    // 那問題依賴沒有得到改善
    // this.userService.Deleting +=
}

由此看來 EventHandler 所提供的 事件 功能並不能很好的解決大系統中的這些問題。

事件匯流排

匯流排 一詞來源於 電腦硬體,電腦由很多的部件組成,他們之間有的著大量的資訊交換。
當滑鼠點下的時候,記憶體硬碟顯示器 都會產生變化。

很明顯,各種裝置之間不可能兩兩相連來 監聽 這些 事件。

每個裝置只要把自己產生的 資訊 發到一個類似於 大水管 的 匯流排 裡。

其它裝置各取所需的獲取這些 資訊 再處理自己的資訊。

我們基於同樣的思想可以在我們的應用系統裡實現這種功能。

Reface.AppStarter 中的事件匯流排

Reface.AppStarter 中提供了開箱即用的事件匯流排功能。

事件 的 發起者,與 事件 的 處理者 完全不需要了解對方是誰,甚至於不關心對方是否存在。

使用方法

1 定義一個事件型別

事件型別 是關聯 發起者處理者契約

  • 一個 處理者 只能處理一個 事件型別
  • 一個 事件型別 可以有多個或沒有 處理者

在 Reface.AppStarter 中定義 事件型別 ,只需要讓它繼承於 Event 型別,它的建構函式要求提供事件發起方的例項。

public class MyEvent : Reface.EventBus.Event
{
    public string Message { get; private set; }

    public ConsoleStarted(object source, string message) : base(source)
    {
        this.Message = message;
    }
}

除了 source ,你還定義更多的屬性,以便 事件處理者 可以得到更多的資訊。

2 發起事件

IEventBus 是發起事件的工具。

它的例項已經被註冊到了 Reface.AppStarter 的 IOC / DI 容器中了。

凡是通過 IOC / DI 建立的元件,都會自己注入 IEventBus 例項。我們回到之前 UserService 的例子

[Component]
public class UserService : IUserService
{
    private readonly IEventBus eventBus;

    public UserService(IEventBus eventBus)
    {
        this.eventBus = eventBus;
    }

    public void Delete(User user)
    {
        this.eventBus.Publish(new UserDeletingEvent(this, user));
        this.userRepo.Delete(user);
        this.eventBus.Publish(new UserDeletedEvent(this, user));
    }
}

在 事件發起者 這裡,不需要關心都有誰需要監聽這個事件,只要 釋出 事件即可。

事件匯流排會根據 事件處理者 所能處理的 事件類 進行分配。

3 監聽事件

事件的監聽是由 IEventListener<T> 完成的。泛型 T 就是事件型別。

該介面簡單易懂,只有一個方法需要實現,那就是監聽後要處理的內容。

注意 : 為了能夠讓 Reface.AppStarter 中的容器捕捉到你的 Listener ,你需要為其加上 [Listener] 特徵。

[Listener]
public class CheckUserDeleteByEventModule : IEventListener<UsrDeletingEvent>
{
    // 由於該元件依然是從 Reface.AppStarter 的容器中建立的,所以這裡也可以通過建構函式注入介面的例項
    public readonly IEventService eventService;

    public CheckUserDeleteByEventModule(IEventService eventService)
    {
        this.eventService = eventService;
    }

    // 你可以按最小的業務功能拆分你的事件監聽器
    // 比如刪除使用者
    // 你不需要寫一事件監聽器去檢查系統中所有業務單元同否允許刪除除使用者
    // 你可以按業務單元逐一實現這些檢查
    // 對於不能刪除的,只要丟擲異常即可
    public void Handle(UsrDeletingEvent @event)
    {
        if(doSomeCheck(@event.User))
        {
            throw new CanNotDeleteDataException(typeof(@event.User), @event.User.Id, "your reason");
        }
    }
}

最後

使用 事件匯流排 能大幅度減少系統的耦合度。

當系統的複雜度不斷提升時,還可以使用 訊息匯流排 。它們的基本原因是一樣的,只不過 訊息匯流排 為了 分散式高併發 做出了很多的優化。

但在 單體應用模式 下, Reface.AppStarter 所提供的 事件匯流排 功能是完全能夠滿足需求的。

相關連結

  • Reface.AppStarter