原标题《如何利用观察者模式对代码进行解耦?》,创作于2019-05-21
背景
订单支付成功后,支付渠道会回调我们告知订单已支付,此时我们需要做一系列操作:
- 修改订单支付状态;
- 发送通知给商户;
- 加入队列进行推单;
- 其他
我们发现其实这些都是“订单已支付”这件事发生的一系列操作,“_一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新” _非常适合使用“事件-监听者”对这些业务逻辑解耦。很多框架都实现了这种模式的支持,但是如果没有,我们也可以手动实现这种模式,以达到解耦不同操作,弱化依赖关系,特别的解决了面向过程的面条代码问题。
另外,如果直接利用现在框架可能并没有这个问题。该案例亦可作为解耦方案分析学习。
实现
类图
步骤
创建事件基类
<?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-事件