很多年前,windows workflow foundation还叫WWF,而直译过来的名称让很多人以为它就是用来开发工作流或者干脆就是审批流的。
博主当年还是个懵懂的少年,却也知道微软不会大力推一个面向如此具象的业务场景的技术,于是特地找了一本《WF本质论》,当看到“程序即数据”这个论断时,被深深震撼了。可能这只是作者的随意一写,但当时正是泛型方法、lamda表达式、匿名委托啥的开始出现的时候,作者的这一说法在某种程度上暗合了博主平常的编程思想。于是逻辑与数据,算法与结构,它们之间的界限在我眼里,在我心里,开始以更诡异的方式模糊了起来。
然而之后并未在工作上使用过WF,因此博主也就不再关注此项技术。如今重新翻看,突然发现官方的Workflow Team blog最近的更新都是两年前了,网上的资料都是N年前的,而且大多数是教大家怎么用。为何要用,何处要用,基本上没有太多介绍。到了现如今,似乎也被微软抛弃了,为此我还专门在博问里提了下,也没人知道具体详情。
很少有看到专门使用wf进行开发的场景,好用与否,在小众的群体里抽样得出结论也就显得不怎么可靠了。似乎wf在sharepoint环境中用起来比较协调,由于博主对sharepoint没有研究过,所以对此不好评论。如若在纯代码环境下开发,用wf自带的一套activity能画出看似清晰的流程图,然而关键的和外部交互的环节却仍然避免不了,需要考虑的因素大部分仍然需要代码去完成;而各种条件下流程的走向,使用activity的条件判断,又让人觉得没有直接写代码来的方便,比如ifelse,用代码可能只要在一个方法体中就能完成,而在流程图中却要使用两个activity,如果其中再有交互跳转的话,流程图的复杂程度未必在代码之下,而后期维护也难说容易。
不妨非恶意的揣测,经过几年时间,微软慢慢觉得,将代码间原本隐晦的逻辑关系抽离出来变成看得见的“流程”,对程序员来说,未必有多大的意义,于是就减少了这方面的投入。当然你也可以认为wf已经很成熟了,不过即使如此,博主还是倾向认为此种所谓成熟是因为找不到改进的方向,虽然“成熟”,但不怎么好用。
but,wf并非一无是处,wf的bookmark(书签机制)是最区别于传统开发的特性,个人认为也是最重要的特性。传统开发时,我们时不常的会遇到这种情形:判断流程是否走到特定环节以便决定下一步操作,其实就是判断A\B\C\D\...等排列组合变数,它们之间可能也是相互关联的,只要流程没有继续流转,那么每次启动都要进行同样的判断;是否可以在各变数满足条件时生成一个标签,当我们发现这个标签存在时,就可以去找标签对应的环节,就能马上进行后续操作了。实际开发中,我们常设置一个冗余的标识字段起这个作用。但单个字段未必能完全标识一个环节,而且也显得不够直观。wf中的bookmark与上述的标签类似,本质上,它是对委托异步回调炉火纯青的应用,更自然的描述是在变数尚未符合条件时的一种“等待”,常用于与外部交互,在流程设计时,也不会像传统代码操作(查询、判断、更新)标签那样显得突兀(这些操作wf都在更底层帮我们处理了)。
开头说到微软推出wf的本意并非用于开发业务上的工作流,我们甚至可以将任意有逻辑顺序的一段代码“wf化”,然而从业务场景来讲,wf的很多概念能映射其中,毕竟抽象和具象,都逃不离流程二字。因此项目前期或工期紧张,需要快速开发的时候,wf也能帮助程序员梳理流程——在[对]业务流程尚不清晰的时候,你会发现这非常有用——就算日后摈弃wf,大部分代码都可以复用,而由于各环节明确的流转关系,代码重构也较为容易。博主就是因为这个原因,在目前的一个项目中使用了wf,下面我会简单介绍相关要点和个人理解。
版本控制也是它的一个亮点,这个就不细说了。
由于本人对wf钻研不深,所述难免有误,请各位同学批评指正。另,本文并不展示任何项目代码,只以行业一般流程,展示博主在开发过程中的思索。
本项目主干是一手房置购流程,涉及到认筹、认购、成交、租赁四个环节,需求简化后如下(图是用visio画的,画得很挫,一直没找到实线弧形箭头):
1、环节流转
2、流转时需审核(可跨级审核,若申请人本身是最后环节审核人则直接通过)
3、审核不通过时将退回发起环节;审核过程中,申请人也可撤消此次申请
4、单个环节也有不同状态,比如认购有已认购和退认购两种状态,此类状态转换也需要审核;而内容变更也需要审核
5、同时至多只能存在一个审核中事项。比如认购内容变更审核中时,就不能发起转成交申请。
这是一个比较典型的工作流场景。刚接手这个项目时,我考虑用传统方式开发,不多久我发现自己在各种环节、状态、标识、互斥项间晕头转向,一团浆糊。即使流程图已经很清晰明白地摆在面前,将之赋予代码和数据,并使看似毫无关联的各块内容按照预期逻辑运行,似乎显得相当困难。特别是产品有时会一脸无奈地跟你说,认筹也可以转租赁,业务说的;隔天又说,不需要了。要维系代码之间“隐秘”的逻辑关系并快速应对需求变更,随着业务复杂度的提高,难度也更快的升高。
当然,对于熟练工来说,这点难度不算什么,毕竟我们每天都在做这种工作——将业务流程转化成层层调用的代码——我们还可以祭出设计模式、AOP、IOC、ORM啥的让代码看起来更清(fu)晰(za)。我们一直在接受面向对象、面向架构、面向服务的训练,而缺少真正面向流程编程的经验,我想这才是为什么微软当年会推广wf的原因(此为病句,意为强调)。
wf有三种内置流程:顺序流、Flowchart、StateMachine。本项目可考虑后两种,博主选择的是StateMachine,整出来的主流程如下:
看上去很凌乱,只能怪vs自带的wf设计器没有visio好用,其实上图就是第1幅流程图的wf版本。
再来看下每条连线的逻辑,以认购环节出去的连线为例,触发器如下:
可以看到,流程会依据外部信号决定下一步骤,流到下一个环节(转成交、转租赁),或者是退认购,这里还有变更的情况图中没放出来。最终会根据流程中各变量和参数值选择某个路线:
任何类型的审核不通过or申请人撤销就回到当前环节,和之前我们用visio画的流程图一样直观。不过,这里有个问题。博主刚接触statemachine时,选择entry还是trigger放置业务逻辑比较困惑,当时认为两者皆可,毕竟最终只是通过condition来指定状态的转变方向,用不着care condition在哪里生成;博主当时认为微软设计statemachine时,是将trigger当做bookmark activity的容器,以便于与外部决策进行交互,当然我们也可以将bookmark放在entry中,说到底这就是一个规范问题,不必深究。然而博主发现,若流程从一个state流向同一个state——即state指向自己——其实前后两个环节已不是同一个“实例”了 ,因此原state的变量等状态不会保存,entry会再次执行。为了避免这种状况,有停留在当前状态的情形,应该将业务逻辑写在entry中,不进入到触发后的流转,因为一旦流转,就算流转回当前状态,也是先出再进。当然,如果当前状态没有需要保留的信息,写在trigger亦可。
再来看看审核到底发生了什么。
这个更不用我多说了,不过我还是要多说两句。虽然上图很清晰地还原了对应的需求,但未必是最合适的做法,我最后还是将这些逻辑统一到一个activity里。前面说过,我们能将绝大多数多行代码wf化,but,复杂的流程需要借助wf,一个简单方法就能搞定的逻辑如果还硬要画出几道条条理理来,那就是偏执了。(话说,写程序的大多数人都是偏执的,非黑即白,我没说错吧)
对于前述需求的第5条,单条wf流程天生就能满足,不需要我们做额外的coding。思考一下,若几个审核可以并行,或流程流转到下一环节后,上一环节需要变更了,怎么办?用多流程或子流程试试吧。
代码活动中普通的成员变量,持久化在bookmark恢复后,值丢失不可用;若需要在持久化前后保持变量值,应该使用Variable,或应用Serializable特性,两者都是博主推测没试过。
有些事务不能失败了就全部回退重来,比如单据状态从a->b,经过多位领导审批同意后,结果因为最后一步提交不成功,让单据回退到a重审?这种情况,只能将最后一步重新提交,实际开发中,可将此类“脏提交”定时再提交。
一个activity里可create多个bookmark,当所有bookmark都恢复执行后,流程才继续往下。
wf在流程流转过程中,并不能返回数据给调用者,比如发起认筹转认购的申请,调用ResumeBookmark方法,并不能知晓是直接通过还是等待审核,需要另外查数据库得到状态结果。当然可以使用WorkflowApplication.Extensions与外部程序交互,但不能满足某些场景,比如客户端调用webapi,服务端action发起流程,action自然需要知道流程跑完后(暂停or完结)的结果是什么并返回给客户端;这时候WorkflowApplication.Extensions就然并卵了,除非改写底层,比如让wf能取消action的后续执行并将结果数据写入http连接。
流程改版or外部依赖项变化,wf下,要么新旧版本并行,要么研究如何将旧版本迁移到新版本,很多情况下没有纯代码控制来得方便。
博主观点:同步SaaS并非wf较适用的场景,wf适用于异步消息推送场景,比如外卖点餐状态、快递状态、银行资金流转等,客户端并不等待马上的结果,而是事项状态改变时接收服务端消息即可。当然硬要在同步环境下使用也可以,此时需要更松散的设计和底层框架的配合,以适应wf“封闭式流程”的特点。
需继续研究的点:
1、有些流程大同小异,能否封装成一个可配置的流程,在设计时进行简单的参数配置就能显示不同的流程步骤;比如a状态可转为b、c状态,而b只能转c状态,那么a,b在转换到下一个环节的判断逻辑就有少许差异了。这貌似只能通过元数据实现。
2、InstanceOwner,网上实在找不到更多关于它的介绍,目前可知是用于多宿主环境下,宿主对wf实例的lock。没找到给实例赋值InstanceOwner的直接途径,网上找到的基本上是下面两行代码:
InstanceView view = instore.Execute(instore.CreateInstanceHandle(), new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30));
instore.DefaultInstanceOwner = view.InstanceOwner;
不知是基于什么因素考虑,总是觉得不太理解是什么意思,为什么要以及以这种方式设置DefaultInstanceOwner,如果不设置的话,那么下面这行代码运行时就会报错:
WorkflowApplicationInstance instance = WorkflowApplication.GetInstance(Guid.Parse(flowinstance.InstanceID), instore);
框架应该完全可以在底层就自动给我们处理InstanceOwner相关的东东,比如下面这句在没设置DefaultInstanceOwner的时候就不会报错:
wfApp.Load(Guid.Parse(flowinstance.InstanceID));
如何能设置自己的InstanceOwner呢,maybe可以通过InstanceOwnerMetadata。
CreateWorkflowOwnerCommand createCommand = new CreateWorkflowOwnerCommand()
{
InstanceOwnerMetadata =
{
{ WorkflowHostTypeName, new InstanceValue(WFInstanceScopeName) },
}
};
不过设置完了,怎么获取又是一个问题,有兴趣的朋友可以研究下这个文件,也可以在官方WF_WCF_示例里找到。总之持久化这趟子水够深。
其它参考资料:
Loading persisted workflow instances with WorkflowApplication