最近利用 Asp.Net Core 的 MiddleWare 思想对公司的古老代码进行重构, 在这里把我的设计思路分享出来, 希望对大家处理复杂的流程业务能有所帮助.
背景
一个流程初始化接口, 接口中根据传入的流程类型, 需要做一些不同的工作.
1. 有的工作是不管什么类型的流程都要做的(共有), 有的工作是某一流程特有的.
2. 各个处理任务基本不存在嵌套关系, 所以代码基本是流水账式的.
3. 流程的种类较多, 代码中 if 或者 switch 判断占了很大的篇幅.
4. 这些处理工作大致可分为三大类, 前期准备工作(参数的校验等), 处理中的工作(更新数据库, 插入数据等), 扫尾工作(日志记录, 通知等)
Asp.Net Core 中的 MiddleWare
注意第二条, 流水账式的代码, 这让我想到管道模型, 而 Asp.Net Core 的 MiddleWare 正是放在这个管道中的.
看下图:
有 middleware1,middleware2,middleware3 这三个中间件放在一个中间件的集合 (PipeLine, 管道) 中并有序排列, Request 请求 1 从流向 2 载流向 3, 随之产生的 Response 从底层依此流出.
这个 Request 和 Resopnse 就封装在我们经常看到的 Context 上下文中, Context 传入到中间件 1, 中间件 1 处理后再传出 Context 给中间件 2>>>> 一直这样传出去, 直到传到最后一个.
我们经常在 startup 的 configure 中调用的 app.use()方法, 其实也就是向这个集合中添加一个 middleware,Context 进入后, 必须被该 middleware 处理.
不知道我这么说, 大家有没有这种管道模型处理任务的概念了?
代码解读
不懂? 没关系, 那我们结合代码看看.
上面说过, 每个 MiddleWare 会把 Context 从自己的身体里面过一遍并主动调用下一个中间件.
所以, 中间件是什么? 是一个传入是 Context, 传出也是 Context 的方法吗? 不是!
是一个传入是委托, 传出也是委托, 而这传入传出的委托的参数是 Context, 该委托如下:
- /// <summary>
- /// 管道内的委托任务
- /// </summary>
- /// <param name="context"></param>
- /// <returns></returns>
- public delegate Task PipeLineDelegate<in TContext>(TContext context);
所以中间件是下面这样的一个 Func, 它肩负起了调用下一个中间件 (委托) 的重任:
Func<PipeLineDelegate<TContext>, PipeLineDelegate<TContext>>
而管道又是什么呢? 是 Func 的集合, 如下:
IList<Func<PipeLineDelegate<TContext>, PipeLineDelegate<TContext>>> _components = new List<Func<PipeLineDelegate<TContext>, PipeLineDelegate<TContext>>>();
我们再 Startup 方法里面的 Configure 方法里面的 Use 是在做什么呢? 其实就是在给上面的管道_components 添加一个 func, 如下:
- public IPipeLineBuilder<TContext> Use(Func<PipeLineDelegate<TContext>, PipeLineDelegate<TContext>> func)
- {
- _components.Add(func);
- return this;
- }
但是在今天的 Use 中呢, 我还想对原有的 Use 进行一次重载, 如下:
- public IPipeLineBuilder<TContext> Use(Action<TContext> action, int? index = null)
- {
- Func<PipeLineDelegate<TContext>, PipeLineDelegate<TContext>> pipleDelegate = next =>
- {
- return context =>
- {
- action.Invoke(context);
- return next.Invoke(context);
- };
- };
- if (index.HasValue)
- if (index.Value> _components.Count)
- throw new Exception("插入索引超出目前管道大小");
- else
- {
- _components.Insert(index.Value, pipleDelegate);
- }
- else
- {
- _components.Add(next =>
- {
- return context =>
- {
- action.Invoke(context);
- return next.Invoke(context);
- };
- });
- }
- return this;
- }
可以看到, 重载之后, 传入的变成了 Action<TContext> action, 因为我想外部专注于自己要真正处理的业务, 而调用下一个 middleware 的事情封装到方法内部, 不用外部来关心了, 并且, 可以通过传入的 index 指定插入的中间件的位置, 以此来控制业务的执行顺序.
最后, 需要把传入的委托链接起来, 这就是管道的 Build 工作, 代码如下:
- public PipeLineDelegate<TContext> Build()
- {
- var requestDelegate = (PipeLineDelegate<TContext>)(context => Task.CompletedTask);
- foreach (var func in _components.Reverse())
- requestDelegate = func(requestDelegate);
- return requestDelegate;
- }
到这里, 管道相关的差不多说完了, 那我, 我如何利用上面的思想来处理我的业务呢?
处理业务
处理示意图
步骤:
Ø 初始化三条处理管道(根本是 New 三个 List<Task > 集合, 对应前期准备工作集合, 处理中工作的集合, 扫尾工作的集合).
Ø 向三条管道中注入公共的处理任务.
Ø 根据传入的流程类型动态加载对应的处理方法 Handle().
Ø Handle 方法向三条管道中注入该类型的流程所对应的特有任务.
Ø Build 三条管道.
Ø 依此执行准备工作管道 =>处理中管道 =>处理后管道.
上面步骤可以概括成下面的代码.
- private void InitApproveFlow(ApproveFlowInitContext context)
- {
- var beforePipeLineBuilder = InitBeforePipeLine();
- var handlingPipeLineBuilder = InitHandlingPipeLine();
- var afterPipeLineBuilder = InitAfterPipeLine();
- RegisterEntityPipeLine(context.flowType, beforePipeLineBuilder, handlingPipeLineBuilder, afterPipeLineBuilder);
- var beforePipeLine = beforePipeLineBuilder.Build();
- var handlingPipeLine = handlingPipeLineBuilder.Build();
- var afterPipeLine = afterPipeLineBuilder.Build();
- beforePipeLine.Invoke(context);
- handlingPipeLine.Invoke(context);
- afterPipeLine.Invoke(context);
- }
其中, RegisterEntityPipLine()方法根据 flowType 动态加载对应的类, 所有类继承了一个公共的接口, 接口暴露出了 Handle 方法.
- private void RegisterEntityPipeLine(string flowType, IPipeLineBuilder<ApproveFlowInitContext> beforePipeLineBuilder,
- IPipeLineBuilder<ApproveFlowInitContext> handlingPipeLineBuilder,
- IPipeLineBuilder<ApproveFlowInitContext> afterPipeLineBuilder)
- {
- var handleClassName = ("类名的前缀" + flowType).ToLower();
- var type = AppDomain.CurrentDomain.GetAssemblies()
- .Where(a => a.FullName.Contains("程序及名称"))
- .SelectMany(a =>
- a.GetTypes().Where(t =>
- t.GetInterfaces().Contains(typeof(类继承的接口名称))
- )
- ).FirstOrDefault(u =>
- u.FullName != null && u.Name.ToLower() == handleClassName
- );
- if (type == null)
- throw new ObjectNotFoundException("未找到名称为 [" + handleClassName + "] 的类");
- var handle = (类继承的接口名称)_serviceProvider.GetService(type);
- handle.Handle(beforePipeLineBuilder, handlingPipeLineBuilder, afterPipeLineBuilder);
- }
Handle 方法里面又做了什么呢?
- public void Handle(IPipeLineBuilder<ApproveFlowInitContext> beforePipeLineBuilder, IPipeLineBuilder<ApproveFlowInitContext> handlingPipeLineBuilder, IPipeLineBuilder<ApproveFlowInitContext> afterPipeLineBuilder)
- {
- HandleBefore(beforePipeLineBuilder);
- Handling(handlingPipeLineBuilder);
- HandleAfter(afterPipeLineBuilder);
- }
分别向三个管道中添加 前, 中, 后 对应的任务.
Q&A
Q1: 如果处理任务依赖于上一个处理任务的处理结果怎么办?
PipeLineDelegate<TContext> 中的 TContext 是一个对象, 可以向该对象中添加对应的属性, 上游任务处理任务并对 Context 中的属性赋值, 供下游的任务使用.
Q2: 如果某一个任务需要在其他任务之前执行怎么办(需要插队)?
PipeLineBuilder.Use() 中, 有 Index 参数, 可以通过该参数, 指定插入任务的位置.
Q3: 如果保证管道的通用性(不局限于某一业务)?
TContext 是泛型, 可以不同的任务创建一个对应的 TContext 即可实现不同业务下的 PipleLine 的复用.
有什么上面没涉及的问题欢迎大家在下方留言提问, 谢谢.
来源: https://www.cnblogs.com/CoderAyu/p/9452444.html