Whip Monstrous Code Into Shape - 03 Consider Domain Events

file

If it makes sense for your application, you might consider leveraging domain events and listeners to construct a complex set of functionality.


事件机制是一种很好的应用解耦方式,因为一个事件可以拥有多个互不依赖的监听器。举例来说,每次你把用户的订单发完货后都会希望给他发个 Slack 通知。这时候你就可以发起一个 OrderShipped 事件,它会被对应的监听器接收到再传递给 Slack 通知模块,这样你就不用把订单处理的代码跟 Slack 通知的代码耦合在一起了。

其实在文档中已经写的很明白,但是不知道大家是不是这样,至少我是,被文档中的例子所束缚,想的总是用来做一些发邮件、 HTTP 请求等比较耗时的工作,但是其实这是队列的作用,而事件是用来解耦的。

刚开始用事件的时候总是很奇怪,为什么要用事件,这跟写在控制器中根本就没什么区别嘛。等到后来控制器越来越臃肿的时候,才慢慢关注代码如何解耦、如何更优雅。

事件估计大家用的比较多,就不举例了,无非就是一个事件可以对应多个监听器,而这些监听器可以互相没有任何关系,达到解耦的目的。

获取事件的返回值

这个视频比较简单,我写完之后发现好像太没意思了,于是我就看了看源码,看能不能找到一些有趣的东西,还真的看到一个平常不太了解的:触发事件可以有返回值,这个在文档上好像没有看到,并且平时使用的时候也不太会这样用。

先看看 event 函数,很简单,就是调用 Illuminate\Events\Dispatcher::fire($event, $payload = [], $halt = false),事件看起来复杂,其实就是一个 Dispatcher,所有的绑定、fire等都是这个在处理。

function event(...$args)
{
    return app('events')->fire(...$args);
}

再看看 fire 方法,有三个参数:第一个是事件,可以是事件名也可以是一个事件实例;第二个参数是 payload(载荷?),也就是在 Listener handle方法要用到的参数,不仅仅是事件实例本身,如果有额外参数,可以用数组形式传进去;第三个参数默认是 false,设置为真时当 Listener 有返回值时就会中断事件的传递。

    public function fire($event, $payload = [], $halt = false)
    {

        // 如果 $event 是事件实例的话,使用该实例类名作为事件名,
        // 并使用该实例作为 listener handler 方法的参数

        if (is_object($event)) {
            list($payload, $event) = [[$event], get_class($event)];
        }

        // 这就是返回值数组...
        $responses = [];

        // 参数默认是空数组,如果传入了值的话也会转换成数组,方便使用 call_user_func_array 函数
        if (! is_array($payload)) {
            $payload = [$payload];
        }

        $this->firing[] = $event;

        // 是否要广播
        if (isset($payload[0]) && $payload[0] instanceof ShouldBroadcast) {
            $this->broadcastEvent($payload[0]);
        }

        foreach ($this->getListeners($event) as $listener) {
            $response = call_user_func_array($listener, $payload);

            // 如果 listener 有返回值并且 $halt 设置为 true 的话,
            // 直接返回结果,并且中断事件继续传播
            if (! is_null($response) && $halt) {
                array_pop($this->firing);

                return $response;
            }

            // 如果 listener 返回 false 的话中断事件继续传播
            if ($response === false) {
                break;
            }

            $responses[] = $response;
        }

        array_pop($this->firing);

        return $halt ? null : $responses;
    }

所以你可以获取 Listener 中返回的值数组:$responses = event(new UserRegistered($user));

修改事件传播时的 payload

在用事件的时候我在想一个问题,怎么在事件的传播中动态的修改事件的载荷,也就是 Listener handle 方法里面的参数,比如 UserRegistered($user) 事件来说,第一个监听器会修改 $user,然后第二个 Listener 的 handle 方法参数也是UserRegistered($user) ,修改后的$user 不会对第二个 Listener 起作用。

后来想想太简单了,因为 UserRegistered($user) 本身就是一个数据容器,$user 也是 public ,直接在第一个监听器处理完成后修改事件就行了 :$event->user = $user,因为在事件 fire 的时候不管多少个监听器,用的都是用一个事件实例。

这个也给我一个启发,虽然事件仅仅是一个数据容器,但是我们也可以在里面做一些数据的修改供 Listener 使用,说不定在有些情况下有奇效。

乱七八糟的东西

最后在这个视频中有一点我觉得记录一下,挺有意思的。

我们可以适当的把一些简单的逻辑放到模型中,当然太复杂的也不行,这样造成模型臃肿,如何选择一个最适合的方式,这应该是写出优雅代码必须要考虑的。

比如用户注册并触发事件,可以写在控制器中:

$user = User::create([
    'name' => 'example'
    'email' => 'test@example.com',
    'password' => bcrypt('123456'),
]);

event(new UserRegistered($user));

同样的,可以把这部分封装到模型中,在控制器中 User::register(...) 就行:

public static function register($attributes)
{
    $user = User::create($attributes);
    event(new UserRegistered($user));
}

这样做法会带来一个问题,增加理解难度,可能过个十天半月你去看控制器,哎,为什么控制器里面只有一个简单的 User::register(...) 方法?当然可以去查看 User::register 方法,同样的你也可以写个注释,就像这样:

/**
 * @event App\Event\UserRegistered
 */
public function store()
{
    return User::register();
}
本帖已被设为精华帖!
本帖由系统于 7年前 自动加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!