使用Pipeline抽象业务生命周期流程
上篇关于流程引擎的文章还是快两年以前的《微服务业务生命周期流程管控引擎》,这中间各种低代码平台层出不穷,虽然有些仅仅是OA+表单的再度包装,但有些的确是在逻辑和操作单元层面进行了真正的高度抽象,形成产品底座,上层支持更大灵活程度的自定义应用搭建。当然这个不是本篇文章的主题,核心希望给各位朋友表达的还是对于逻辑流程抽象的关注,特别是现在的很多应用是在围绕行业展开,如何在各种杂乱的功能点之中,做到杂而不乱,又如何通过核心的控制模块提供标准化的接入,我通过OSS.Pipeline 这个引擎框架类库,和大家分享一下我的思路(如有不足,还望指出探讨),需要说明的是,此框架是类库级引擎,不依赖数据库等存储,代码详见Gitee或Github。
一. 我们抽象的是什么?
二. 逻辑推进和业务单元解耦思路
三. Pipeline 管道的设计实现
四. 使用Pipeline完成示例
一. 我们抽象的是什么?
首先,这个不是一个有着标准答案的问答题,只是用来开始这篇文章的发散思考。当然每个有着一定开发经验的过来人也可能都有自己不同的见解,我也做开发多年,从早些年的单纯CRUD(ctrl+c,ctrl+v 也干了不少),到后来参与复杂的业务逻辑,一直到自己全程负责打造产品。个人总结来看,常规业务产品的开发工作包含两个方面:1. 单点业务的操作(依然是CRUD为主),处理数据的存储和展示。 2. 点与点的连接(变数的部分),处理数据的流向。
单点的操作非常的简单,就是诸如保存文件,保存数据库,第三方接口调用等等。很多项目的复杂,主要是集中在第二点,不同的业务有着不同的生命周期事件点组成,特别是再配合不同的公司管理流程加入,即便是相同的业务,系统数据的流向也各有不同,在我有限的经验里,基本是在处理和抽象这一层面。(当然业务单元的粒度大小也是相当重要的,只是这个难度更容易解决)
二. 逻辑推进和业务单元解耦思路
如何解耦,这个每个人的方式方法多种多样,我先以简单的订单支付成功并且需要发送短信和邮件为例,看下演进的过程:
在早期,消息队列还没有大规模使用的时候,处理方式如图

这个时候,功能完全是放在一块的,开发简单快速,但功能耦合且性能低下。随着消息队列这些基础解决方案的使用,我们进行第二版快速改造:

这个时候发送邮件和短信耗时的部分通过消息队列转移至独立的服务处理,暂时提升了性能可用问题,但因为邮件和短信因为参数内容不同,依然需要拼接消息去操作两个不同的队列,耦合度依然存在。我们再更新第三版:

通过这一版,我们回到一个队列,增添了订单支付成功Hook服务,订单支付更新的方法内部不需要关注任何其他逻辑细节,仅需添加成功事件队列,逻辑进一步解耦,同时为后续的扩展提供了空间(在hook服务和具体的短信邮件之间依然可以通过队列处理,这里不做进一步说明)。
此时我们梳理一下当前的数据流:

虽然我们在第三版已经有了很大改善,但是我们可以看到,所有信息流的流动,依然是依赖上一个节点的显示调用。如果这里我们再添加错误重试,操作日志等需求,还是会或多或少的侵入业务代码之中。所以我们能不能更进一步处理,比如订单支付更新只关注更新,无需关注事件是通过消息队列还是异步线程传递给下游。成功HooK方法只需要关注消息组装分发,无需关注是否失败重试。
按照设想,可以得出如下图所示:

如果我们能通过上边的图示将个业务单元完全独立出来,那么在每个业务单元之间可以随时插入新的模块而相互之间不受干扰,且能根据实际的情况进行异常的介入处理。这也是我设计OSS.Pipeline的初衷。
三. Pipeline 管道的设计实现
通过上边的订单支付演进过程,基本展示了我的基本思路,这里我们将示例再次简化,方便继续讲解具体的实现

OSS.Pipeline 将所有的业务单元抽象为一个个节点,这些节点负责业务的具体执行,通过将这些Pipe组合形成业务的生命周期的流水线,即Pipeline。同时Pipeline本身也可作为一个独立的Pipe参与更上一个层级的业务流程之中(即子流水线)。通过将业务输出和逻辑输出的拆解,借助.Net 的泛型每一个管道都能定义独立的业务输入输出,和逻辑输入输出参数(有时,逻辑输入输出和业务输入输出虽然相同,但代表的含义不同),因为OSS.Pipeline是为了业务生命周期而设计,所以我参照了BPM中的组件命名方式,并扩展对应的组件基类供业务层选择使用,具体的可用组件实现请参照gitee代码介绍
下边我会用上边订单的示例,来搭建一个Pipeline示例。
四. 使用Pipeline完成示例
1. 定义支付更新活动
public class OrderPayReq
{
public long OrderId { get; set; }
public decimal PayMoney { get; set; }
} /// <summary>
/// 订单支付管道
/// OrderPayReq - 业务输入参数, bool - 业务输出执行成功失败, long - 逻辑输出订单Id
/// </summary>
internal class OrderPay : BaseActivity<OrderPayReq, bool, long>
{
protected override async Task<TrafficSignal<bool, long>> Executing(OrderPayReq para)
{
LogHelper.Info($"支付订单({para.OrderId})金额:{para.PayMoney} 成功"); await Task.Delay(10); // 返回执行成功,并告诉下级管道 订单Id
return new TrafficSignal<bool, long>(true, para.OrderId);
}
}
2. 定义支付成功后的Hook活动:
public class NotifyMsg
{
public string target { get; set; }
public string content { get; set; }
public bool is_sms { get; set; } // 假设不是短信就是邮件
} /// <summary>
/// 支付Hook
/// long-是上级管道传入的订单Id, bool - 业务输出执行成功失败, List<NotifyMsg> 需要发送的消息列表
/// </summary>
internal class PayHook : BaseActivity<long, bool, List<NotifyMsg>>
{
protected override async Task<TrafficSignal<bool, List<NotifyMsg>>> Executing(long para)
{
LogHelper.Info($"执行订单({para})Hook");
await Task.Delay(10); var msgs = new List<NotifyMsg>
{
new NotifyMsg() {target = "管理员", content = $"订单({para})支付成功,请注意发货"},
new NotifyMsg() {target = "用户", content = $"订单({para})支付成功,已经入服务流程", is_sms = true}
}; return new TrafficSignal<bool, List<NotifyMsg>>(true, msgs);
}
}
3. 定义发送活动
/// <summary>
/// 发送服务
/// NotifyMsg - 上级管道传递的业务输入参数, bool - 当前业务执行成功失败
/// </summary>
internal class Notify : BaseActivity<NotifyMsg, bool>
{
protected override async Task<TrafficSignal<bool>> Executing(NotifyMsg para)
{
LogHelper.Info($"发送{(para.is_sms?"短信":"邮件")}消息 :{para.target}:{para.content}"); await Task.Delay(10); return new TrafficSignal<bool>(true);
}
}
4,定义一个Pipeline,将上边的管道串联起来,同时定义一个Watcher,将管道执行过程中的事件记录下来
internal class OrderPayPipeline
{
private static readonly OrderPay _pay = new OrderPay();
private static readonly PayHook _payHook = new PayHook();
private static readonly Notify _notify = new Notify(); static OrderPayPipeline()
{
_pay
.AppendMsgFlow("order_pay_event") // 添加默认实现的异步消息队列中
.Append(_payHook) // 消息队列数据流向hook管道
.AppendMsgEnumerator() // Hook处理后有多条消息,添加消息枚举器
.Append(_notify); // 枚举后的单个消息体流入发送节点 // 添加日志,通过创建流水线,给流水线添加Watcher,会自动给下边的所有Pipe添加Watcher
_pay.AsPipeline(_notify, new PipeLineOption() { Watcher = new FlowWatcher() },"OrderPayPipeline");
} // 作为对外暴露接口
public Task<bool> PayOrder(OrderPayReq req)
{
return _pay.Execute(req);
}
} public class FlowWatcher : IPipeLineWatcher
{
public Task PreCall(string pipeCode, PipeType pipeType, object input)
{
LogHelper.Info($"进入 {pipeCode} 管道", "PipePreCall", "PipelineWatcher");
return Task.CompletedTask;
} public Task Executed(string pipeCode, PipeType pipeType, object input, WatchResult watchResult)
{
LogHelper.Info($"管道 {pipeCode} 执行结束,结束信号:{watchResult.signal}", "PipeExecuted", "PipelineWatcher");
return Task.CompletedTask;
} public Task Blocked(string pipeCode, PipeType pipeType, object input, WatchResult watchResult)
{
LogHelper.Info($"管道 {pipeCode} 阻塞", "PipeBlocked", "PipelineWatcher");
return Task.CompletedTask;
}
}
5. 添加业务实际调用,这里使用单元测试:
private static readonly OrderPayPipeline payLine = new OrderPayPipeline();
[TestMethod]
public async Task TestOrder()
{
var payRes =await payLine.PayOrder(new OrderPayReq() {OrderId = 111, PayMoney = 1000.00m});
await Task.Delay(100);
Assert.IsTrue(payRes); // 订单支付更新结果
}
最后这里业务执行的日志如下:
2022-09-13 Code: Key: Detail:支付订单(111)金额:1000.00 成功
2022-09-13 Code: Key: Detail:执行订单(111)Hook
2022-09-13 Code: Key: Detail:发送邮件消息 :管理员:订单(111)支付成功,请注意发货
2022-09-13 Code: Key: Detail:发送短信消息 :用户:订单(111)支付成功,已经入服务流程
通过Watcher记录操作日志如下:
2022-09-13 Code: Key:PipePreCall Detail:进入 SimpleMsgFlow`1 管道
2022-09-13 Code: Key:PipeExecuted Detail:管道 OrderPay 执行结束,结束信号:Green_Pass
2022-09-13 Code: Key:PipeExecuted Detail:管道 SimpleMsgFlow`1 执行结束,结束信号:Green_Pass
2022-09-13 Code: Key:PipePreCall Detail:进入 PayHook 管道
2022-09-13 Code: Key:PipeExecuted Detail:管道 PayHook 执行结束,结束信号:Green_Pass
2022-09-13 Code: Key:PipePreCall Detail:进入 MsgEnumerator`1 管道
2022-09-13 Code: Key:PipePreCall Detail:进入 Notify 管道
2022-09-13 Code: Key:PipePreCall Detail:进入 Notify 管道
2022-09-13 Code: Key:PipeExecuted Detail:管道 Notify 执行结束,结束信号:Green_Pass
2022-09-13 Code: Key:PipeExecuted Detail:管道 Notify 执行结束,结束信号:Green_Pass
2022-09-13 Code: Key:PipeExecuted Detail:管道 MsgEnumerator`1 执行结束,结束信号:Green_Pass
如果你已经看到这里,并且感觉还行的话可以在下方点个赞,或者也可以关注我的公总号(见二维码)

使用Pipeline抽象业务生命周期流程的更多相关文章
- 进阶:spring-bean生命周期流程
Bean的生成过程 主要流程图 1. 生成BeanDefinition Spring启动的时候会进行扫描,会先调用org.springframework.context.annotation.Clas ...
- django请求生命周期流程与路由层相关知识
目录 请求生命周期流程图 路由层之路由匹配 无名有名分组 反向解析 无名有名分组反向解析 路由分发 名称空间 请求生命周期流程图 django请求生命周期流程图 路由层之路由匹配 我们都知道,路由层是 ...
- http请求生命周期流程
https://mp.weixin.qq.com/s/fpA2CThk2L-YBw6z0k4rtw HTTP 请求/相应 1.客户端连接到Web服务器 一个HTTP客户端,通常是浏览器,与Web服务器 ...
- 生命周期感知 Lifecycle
奉上翻译原文地址: 处理生命周期 :翻译过程中加上了自己的一点理解.理解不对的地方直接评论就好. 生命周期感知组件可以感知其他组件的生命周期,例如 Activity,Fragment等,以便于在组件的 ...
- go mobile 得生命周期事件
生命周期事件,就是状态从一个阶段切换成另外一个状态时触发的事件.所以我们可以看到 lifecycle.Event 的定义如下: 生命周期一共有下面四个阶段: lifecycle.StageDead ...
- 简:Spring中Bean的生命周期及代码示例
(重要:spring bean的生命周期. spring的bean周期,装配.看过spring 源码吗?(把容器启动过程说了一遍,xml解析,bean装载,bean缓存等)) 完整的生命周期概述(牢记 ...
- 一步步实现:springbean的生命周期测试代码
转载. https://blog.csdn.net/baidu_37107022/article/details/76552052 1. 创建实体SpringBean public class Spr ...
- 【应用安全】S-SDLC安全开发生命周期
0x01 S-SDLC简介 OWASP Secure Software Development Lifecycle Project(S-SDLC)是OWASP组织首个由OWASP中国团队独立发布并主导 ...
- Android全面解析之Activity生命周期
前言 很高兴遇见你~ 欢迎阅读我的文章. 关于Activity生命周期的文章,网络上真的很多,有很多的博客也都讲得相当不错,可见Activity的重要性是非常高的.事实上,我猜测每个android开发 ...
随机推荐
- fiddle如何使用代理
前言 Fiddle作为抓包工具深受程序员的喜爱,可能在代理方面有些会感觉到迷惑的,可以通过本文的学习来掌握一些基本的知识. Fiddle介绍 Fiddler是位于客户端和服务器端的HTTP代理,也是目 ...
- Rabbimtq消息传递对象
对象序列化即可.
- SQL Server数据库 备份A库,然后删除A库,再还原A库,此时数据库一直显示“正在还原”的解决方法
SQL Server数据库 备份A库,然后删除A库,再还原A库,此时数据库一直显示"正在还原"的解决方法: A库一直显示"正在还原". 在这种状态下,由于未提交 ...
- Future源码一观-JUC系列
背景介绍 在程序中,主线程启动一个子线程进行异步计算,主线程是不阻塞继续执行的,这点看起来是非常自然的,都已经选择启动子线程去异步执行了,主线程如果是阻塞的话,那还不如主线程自己去执行不就好了.那会不 ...
- Detecting Rumors from Microblogs with Recurrent Neural Networks(IJCAI-16)
记录一下,很久之前看的论文-基于RNN来从微博中检测谣言及其代码复现. 1 引言 现有传统谣言检测模型使用经典的机器学习算法,这些算法利用了根据帖子的内容.用户特征和扩散模式手工制作的各种特征,或者简 ...
- windows配置skywalking集群
一.zookeeper 准备配置三个zookeeper,因为我是单台模拟,所以需要使用不同的端口,使用版本是apache-zookeeper-3.6.3-bin (必须是3.5+) 1.第1个zook ...
- zookeeper Caused by: java.lang.IllegalArgumentException: myid file is missing
zookeeper 安装集群,配置文件和data和其它都没有问题. 则需要在data里面下添加一个myid 文件 myid里面的数据个service一致 比如.cnf里面配置是 dataDir=/us ...
- 基于ABP实现DDD--领域服务、应用服务和DTO实践
什么是领域服务呢?领域服务就是领域对象本身的服务,通常是通过多个聚合以实现单个聚合无法处理的逻辑. 一.领域服务实践 接下来将聚合根Issue中的AssignToAsync()方法[将问题分配给用 ...
- 微信安装包从0.5M暴涨到260M,为什么我们的程序越来越大?
最近,微信安装包从v1.0的0.5M暴涨到V8.0的 260M引起大家热议,为什么我们开发的程序越来越大?本文做一个简单的讨论.(本文主要根据B站科技老男孩<逆向工程微信安装包,11年膨胀575 ...
- Javascript 正则使用笔记
# 一.如何创建正则表达式对象 # 1.通过RegExp构造函数来创建.i代表忽略大小写,g代表全局搜索(非全局搜索正则只匹配第一次符合的内容,全局搜索可以匹配多次). var reg = new R ...