监听者模式在系统中的应用 —— 事件总线

监听者模式 是一种比较常见的设计模式。

在日常的开发中,我们所使用的 事件 就是一种符合 监听者模式 的功能。

监听者模式 还不太明白的同学可以通过 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 的时候,都会留有一个 CheckCanDeleteUserCleanUserData 的方法,名称可能不太一样,或者压根就没有,毕竟这不是它的业务逻辑范畴。
  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 / DICotnroller 中。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.AppStarterIOC / 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 所提供的 事件总线 功能是完全能够满足需求的。

相关链接

posted on 2020-04-12 16:56  清水栞  阅读(331)  评论(0编辑  收藏

导航