使用pipeline设计模式实现用户积分任务需求
背景
背景就是产品经理提了一个需求,完成任务赠送积分(不能使用),签收后积分实际到账,如果遇到退款需要回收积分,任务是大概是这样的:
- 每天首次加入购物车赠送 10 积分
- 每天首单可以赠送 100 积分
- 购物累积金额达到 99 元赠送 100 积分
- 购物次数满 10 次赠送 100 积分
- 每日签到送 10 积分
- 还有很多奇奇怪怪的任务...
当加入购物车时赠送积分,任务结束,当购买商品时就有可能会同时命中多个条件同时赠送积分,命中的所有条件都赠送后,任务结束。
【使用pipeline设计模式实现用户积分任务需求】分析完需求,接下来就想如何实现,最简单的方法也就是
if else
实现:// 支付成功触发赠送积分
if ("当天首单") { // Reward shopping points }
if ("累积99元") { // Reward shopping points }
if ("买满10次") { // Reward shopping points }
// ...
提需求的时候产品已经想到二期的积分任务需求了,所以随着任务的增多,可维护性一定会降低,所以立马否决了用
if else
实现的想法紧接着想到了之前做支付时用到的「简单工厂」+ 「策略模式」经验,应该是有符合要求的设计模式能解决这类问题。因为整体流程是一条直线的流程,依次执行,就想到责任链模式。通过查询相关资料,责任链模式的变种「管道模式」似乎更适合应用至此。
管道模式
管道模式也称为流水线模式,英文:Pipeline。看到
Pipeline
这个单词非常熟悉,似乎在那里见过,思来想去,是在 Laravel 里面见过,之前分析 Laravel 依赖注入和控制反转 时见到过。Laravel 通过
Pipeline
实现 Middleware
: https://github.com/laravel/framework/blob/9.x/src/Illuminate/Foundation/Http/Kernel.php#L131use Illuminate\Routing\Pipeline;
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
继续往上追
Pipeline
的实现,发现 Laravel 是实现了一个 Pipleline 契约接口,实现了两个管道分别是公用的Pipleline和一个 Routing 相关的 Pipleline,其中Routing Pipleline是继承了公用的 Pipleline 重写了部分方法。Pipleline 契约接口
![上传中...]()
send()
需要传递的数据。through()
需要处理的任务via()
调用的方法名,默认为handel()
then()
对于返回数据的处理
编码 整体构建目录
├── PointTask
│├── OverRmb.php// 满 N 元任务
│├── SignIn.php// 签到任务
│├── TodayFirst.php// 每日首单任务
│├──
│├── PointTask.php// abstract 约束
│└── PointTaskService.php // 对外调用方法
既然要考虑到以后的修改以及通用性,那就要抽象出公用方法,统一继承实现。
经过分析主要方法有两个:分别是发送积分和回收积分,所以先抽象这两个方法。
abstract class PointTask
{
// 发送积分
abstract function send($next, $orderInfo);
// 回收积分
public function recycle($next, $orderInfo)
{
return $next($orderInfo);
}
}
因为有些任务是只有赠送,没有回收的情况,所以定义了
abstract
抽象方法,而不是 interface
,这样在具体任务的实现时可以不去实现 recycle
方法。- 每日首单任务
class TodayFirst extends PointTask { function send($next, $orderInfo) { // 有订单直接执行下一个任务 if (!app(PayOrderService::class)->isTodayFirst($orderInfo['orderSn'])) { return $next($orderInfo); } // 赠送积分 app(PayOrderService::class)->sendPoint(100); return $next($orderInfo); }function recycle($next, $orderInfo) { // 回收积分, code... $next($orderInfo); } }
- 买满多少钱赠送积分
class OverRmb extends PointTask { function send($next, $orderInfo) { // 小于 100 元直接执行下一个任务 if ($orderInfo['price'] < 100) { return $next($orderInfo); }// 赠送积分, code... return $next($orderInfo); }function recycle($next, $orderInfo) { // 回收积分, code... $next($orderInfo); } }
- 每日签到
class SignIn extends PointTask { function send($next, $orderInfo) { // 已签到直接执行下一个任务 if (app(UserService::class)->todayIsSinIn()) { return $next($orderInfo); }// 赠送积分, code... app(PayOrderService::class)->sendPoint(10); return $next($orderInfo); } }
PointTaskService
实现 Pipeline
的组织。对 Laravel 提供的 Pipeline
不太明白的朋友,可以参考下方的参考文章。PointTaskService
class PointTaskService
{
// 定义了可能同时触发的任务
public $shopping = [TodayFirst::class, OverRmb::class];
// 购物赠送积分
public function shoppingSend($orderSn) {
$orderInfo = app(PayOrderService::class)->getOrderInfoByOrderNo($orderSn);
return (new Pipeline(app()))
->send($orderInfo)
->via('send')
->through($this->shopping)
->thenReturn();
}// 购物退款回收积分
public function shoppingRecycle($orderSn) {
$orderInfo = app(PayOrderService::class)->getOrderInfoByOrderNo($orderSn);
return (new Pipeline(app()))
->send($orderInfo)
->via('recycle')
->through($this->shopping)
->thenReturn();
}// 每日签到
public function signIn() {
return (new Pipeline(app()))
->via('send')
->through(SignIn::class)
->thenReturn();
}
}
thenReturn() 方法
thenReturn()
方法是对Pipleline 契约接口的 then()
方法的包装,默认的返回值是调用 send()
时传入的参数,如果对返回值需要再进行处理,则可调用 then()
, 传入一个匿名函数进行处理。支付成功后调用:
if ($isPaid) {
// 赠送积分实效可以不用那么及时,可推到队列异步执行。
app(PointTaskService::class)->shoppingSend("0722621373");
}
退款成功后调用:
if ($isRefund) {
app(PointTaskService::class)->shoppingRecycle("0722621373");
}
每日签到调用:
if ($signIn) {
app(PointTaskService::class)->signIn();
}
文件看起来似乎挺多的,但条理还是比较清晰的:
- 如有新任务,则新建一个任务类继承
PointTask
实现send
方法,如有可能收回积分则再实现recycle
方法。 - 再在
PointTaskService
对外开放的 Service 中加入到指定位置,即可完成,不会影响到其他的业务逻辑。 - 已有的调用处也不用变动代码。
- 认真分析过的源码可能会忘记,但能在合适的时间回想起来就证明当时是有效的分析阅读。
- 平时缝缝补补的小需求遇到糟心的代码基本也是往上继续堆代码,但如果有机会接手完整的功能点,那就尽可能的写好点吧。
参考 Laravel 中的 Pipeline — 管道设计范式
Pipeline 管道操作实现请求中间件过滤
推荐阅读
- Mybatis的使用(4)
- 精心整理16条MySQL使用规范,减少80%问题,推荐分享给团队
- MVCC
- Authing|如何使用 Authing 单点登录,集成 Discourse 论坛()
- 接口interface关键字|接口interface关键字 抽象类和接口的使用 多态
- 如何使用 工商详细信息API
- 数据同步|Datax及Datax-web 下载使用
- 可能是西半球最好用的低代码平台 - Retool 功能介绍与使用体验
- 其他|postman 使用心得,为什么老是不对Post的时候
- Flutter有状态组件使用详解