阅读 26

[重构实践]如何利用观察者模式对代码进行解耦?

原标题《如何利用观察者模式对代码进行解耦?》,创作于2019-05-21

背景

 订单支付成功后,支付渠道会回调我们告知订单已支付,此时我们需要做一系列操作:

  1. 修改订单支付状态;
  2. 发送通知给商户;
  3. 加入队列进行推单;
  4. 其他


我们发现其实这些都是“订单已支付”这件事发生的一系列操作,“_一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新” _非常适合使用“事件-监听者”对这些业务逻辑解耦。很多框架都实现了这种模式的支持,但是如果没有,我们也可以手动实现这种模式,以达到解耦不同操作,弱化依赖关系,特别的解决了面向过程的面条代码问题。

另外,如果直接利用现在框架可能并没有这个问题。该案例亦可作为解耦方案分析学习。

实现

类图

步骤

创建事件基类

<?php

namespace sdk\events\base;

use sdk\listeners\base\BaseListener;

abstract class BaseEvent
{
    /**
     * 订阅事件的监听者
     * @var array
     */
    protected $listens = [];

    public function __construct()
    {
        $this->dispatch();
    }

    /**
     * 分发事件
     */
    protected function dispatch()
    {
        if (is_array($this->listens) && count($this->listens)) {
            foreach ($this->listens as $item) {
                $listener = new $item();
                if ($listener instanceof BaseListener) {
                    $result = $listener->handle($this);
                }
            }
        }
    }

    /**
     * 手动注册一个监听者到事件
     * @param $listener
     * @return null
     */
    public function push($listener)
    {
        $result = null;
        if ($listener instanceof BaseListener) {
            $result = $listener->handle($this);
        }

        return $result;
    }

}
复制代码

创建监听者基类

<?php

namespace sdk\listeners\base;

abstract class BaseListener
{
    abstract public function handle($event);
}
复制代码

创建订单已支付事件类

<?php

namespace sdk\events;

use sdk\events\base\BaseEvent;
use sdk\listeners\NotifyAgentListener;

class OrderPaidEvent extends BaseEvent
{
   /**
    * @var string 已支付的订单号
    */
    public $order_id;

    /**
    * @var array
    * 自动注册的监听者,注意有先后顺序
    */
    protected $listens = [
        MarkOrderPaidListener::class,
        NotifyAgentListener::class,
    ];

    public function __construct(string $order_id, $promotion_amount)
    {
        $this->order_id = $order_id;
        parent::__construct();
    }
    
}

复制代码

添加监听者逻辑

修改订单状态

<?php

namespace sdk\listeners;

use sdk\events\OrderPaidEvent;
use sdk\listeners\base\BaseListener;

class MarkOrderPaidListener extends BaseListener
{
      /**
     * @param OrderPaidEvent $event
     * @return bool
     */
    public function handle($event)
    {
        // 修改数据库的代码
    }
    
}
复制代码

通知代理商

<?php

namespace sdk\listeners;

use sdk\events\OrderPaidEvent;
use sdk\listeners\base\BaseListener;

class NotifyAgentListener extends BaseListener
{
    /**
     * @param OrderPaidEvent $event
     * @return bool
     */
    public function handle($event)
    {
  		// ... 具体的处理逻辑
    }
}
复制代码


后续可以添加更多

使用

具体用法如下

<?php

use sdk\channel\yeebao\Pay as YeeBaoPay;
use sdk\events\OrderPaidEvent;
use sdk\listeners\PushYeebaoPaidOrderQueue;
use sdk\services\OrderService;
use sdk\tools\Func;

/**
 * 易宝渠道支付通知
 * Class route_pay_notify_yb
 */
class route_callback_yb extends BaseApi
{

    private $pay_type;

    private $yeebaoPay;

    public function __construct()
    {
        $this->yeebaoPay = new YeeBaoPay();
    }

    public function post()
    {
        try {
            $this->handle();
        } catch (Exception $exception) {
            $log = $exception->getMessage() . ':文件:' . $exception->getTraceAsString();
            Func::wrtLog('发生了异常', $log);
            die($exception->getMessage());
        }
    }
    
    private function handle()
    {
         $success_info = $this->getCallBackData();
        // 这里是关键点:实例化这个事件,就会自动触发BaseEvent的dispatch方法
        // 就会顺序执行在OrderPaidEvent类属性 $listens 注册的监听者,用handle方法执行逻辑操作
 		
        // 就是这一行逻辑,就能触发好几个监听者,监听者的处理逻辑独自维护,解耦了代码
        $event = new OrderPaidEvent($success_info['order_id']);
        
        // 如果还有一个逻辑,经过判断后想手动注册一个监听者 也可以
         $event->push(new OrderSomeHandleListener());
        
        // ...  实际业务逻辑判断比较多,在此省略
        // 组装返回的数据给上游
    }
    

    private function getCallBackData():array
    {
        // 接收数据处理
        return $success_info;
    }
}

复制代码

总结

使用面向对象技术,可以将这种依赖关系弱化

后续优化

注册的监听者顺序执行有一个缺点,就是万一中间有一个卡住,或者出现错误,就会阻碍后续的执行。
针对这种情况可以考虑将监听者的处理逻辑放入队列,异步执行。具体做法

<?php
// BaseEvent 中

/**
* $var bool 
*增加参数控制:默认同步
*/
protected $sync = true;

protected $link =  'default';

protected function dispatch()
    {
        if (is_array($this->listens) && count($this->listens)) {
            foreach ($this->listens as $item) {
                $listener = new $item();
                if ($listener instanceof BaseListener) {
                    if($listener->sync === true) {   
                           $listener->handle($this);
                    } else {
                       // 增加使用队列的逻辑,将$this 、 $listener 两个变量序列化放入 队列。队列驱动、链接都可以配置
                    }
                }
            }
        }
    }
复制代码

消息中间件实现方式考虑

灵感来源

之前用Laravel比较多,整个设计都可以参考 Lavavel-事件

扩展