本文继续剖析基于Notes/Domino的文档工作流系统的设计和代码,以方便用户能应用和创建自己的工作流。(CSDN的下载资源一旦上传就不能修改,很不方便,现已将下载地址改到GitHub,若发现下载有问题,请与我联系。)
在前文对工作流建模时,我们抽象出以下几类对象:
工作流:每个工作流实例都存放工作流名称、当前节点等信息,并负责处理流程各节点的操作。
采用工作流的业务对象:在这里也就是采购单。
工作流的配置对象:不同工作流实例以及各个节点的操作的实际差异都是从配置数据中读取的。
最后一类对象体现为工作流、节点和操作三种配置文档,在上文已经介绍。前两类对象在我们的Notes工作流里保存于同一个文档,即流程文档主文档合一。现在我们就来看看与这个文档对应的表单设计和代码类。
流程子表单
流程文档的字段都包含在FlowControls子表单内,主文档对应的则是主表单,这样只要将该子表单嵌入主表单,主文档就具备了运行工作流的数据。
应用它创建你的工作流时需注意以下几点:
1. 能进行工作流的操作的用户有三类,一是流程在当前节点的处理人,保存在FlowControls子表单的PersonInCharge字段里。二是能进行像取消Cancel这样的操作的特殊用户,保存在FlowHandlers字段里。三是特权用户角色,像本演示流程用的[IT]角色,能进行上述两类操作。这个[IT]不宜配置,所以在Actions按钮的隐藏公式和代码里都直接用的。所以须将它换成你的系统里代表特权用户的角色。
2. 流程名称保存在FlowName字段里。因为位于通用的子表单内,无法直接写入特定的流程名称,所以在创建主文档时,要将应用的流程名称写入该字段。例如:
Dim doc As NotesDocument
‘CreateDoc is a function in LS library lsNotes
Set doc=CreateDoc("fmPurchasing")
doc.FlowName="Purchasing"
Dim ws As New NotesUIWorkspace
Call ws.EditDocument(True, doc)
3.当前状态保存在Status字段里。在表单上要显示当前状态的地方可以创建一个计算文本或显示时计算字段引用Status字段的值。
4.ActionOptions和Action用于用户点击流程操作时容纳操作选项和用户的选择。PersonInCharge和FlowHandlers分别保存流程的当前处理人和能进行特殊操作的用户。FlowEnds保证流程是否结束的标志,初始值为0,结束时为1。CreatedBy记录文档的创建者。FlowReaders保存文档的读者,FlowWriters保存除流程处理人之外的其他可能的编辑者。FlowComments用于临时容纳用户进行流程操作时写的意见。
5.当用户点击子表单上的流程操作Actions…时,系统需要从流程操作配置文档读取数据并计算出该用户可见的操作选项。完成此功能的代码类FlowActions保存在lsSimpleFlow脚本库中。
工作流类
本流程系统在客户端和XPages环境下运行使用的语言分别是LotusScript和Java。下面先讨论LotusScript下的实现。处理流程各节点操作的通用代码组成了三个类,都在lsSimpleFlow脚本库里。之所以用三个类,是为了在普通应用之外满足测试的需要。作为核心的后端代码都在AbstractFlow类中。一般流程操作都使用它的子类SimpleFlow,作用是从NotesUIDocument获取主文档,处理字段校验等涉及界面交互的任务。BatchFlow类在构造函数内传入主文档,没使用到任何前端类,可用于测试或批量审批。为了方便不妨将这些类称为流程引擎。
如前文所述,这些类能完成以下流程的通用功能:
- 校验必填字段。
- 修改流程文档的权限,包括有关的读者域、作者域、存取控制区段。
- 添加操作记录。
- 修改配置的业务字段。
- 发送邮件通知相关处理人。
实际工作流往往会有特殊的需求,这就要求能以某种方式扩展流程类,基本上可概括为在流程提交前后执行一定的逻辑,不妨分别称为QuerySubmit和PostSubmit的业务逻辑。在Lotus Notes中理论上有很多种方式可考虑。
1. 将特定流程的代码写在配置文档里。
因为LotusScript是一种解释型的脚本语言,流程引擎可以像读取其他配置数据一样以文本的形式读取这些代码,再解释执行。
缺点是这些代码不在Designer内编写,没有语法校验、颜色标记、格式化等帮助,运行时若出错很难调试,以及没有经过预处理速度也会稍慢。
2. 在流程类的代码中添加运行特定流程逻辑的函数。
例如在流程类的Submit方法中调用QuerySubmit和PostSubmit函数,它们或与流程引擎同位于一脚本库lsSimpleFlow,或在lsSimpleFlow引用的另一脚本库中。
缺点是通用的流程类对特定的流程逻辑产生了依赖。lsSimpleFlow脚本库无法单独部署到某个应用程序里。如果QuerySubmit之类的函数保存在lsSimpleFlow脚本库里,一旦流程引擎的代码需要更正或升级,就不能简单刷新该脚本库。如果这些特定的逻辑存于lsSimpleFlow脚本库引用的另一个脚本库内,则该库的名称须写死,并且逻辑上让通用代码引用特定代码的设计奇怪不易理解。
3. 采用事件机制在流程引擎中调用特定流程的代码。
要实现流程引擎与特定流程代码之间的分离,在许多程序语言中都会采用所谓事件的机制。LotusScript也支持事件,但仅限于NotesUIDocument等对象预定义的事件。为了让自定义类能够支持事件机制,我们需要自己编写代码。在33. 面向对象的LotusScript(六)之为自定义对象模拟事件和Java、LotusScript和JavaScript中的自定义事件编程等几篇文章里我讨论了在LotusScript实现事件机制的两种途径。以两者中较优的类似Java中自定义事件的实现方式为例,要在流程引擎的Submit方法前模拟QuerySubmit事件,需要编写一个包含QuerySubmit方法的FlowEventHandler类,然后调用流程引擎的AddEventHandler方法传入该类的一个实例,最后在Submit方法中调用该实例的QuerySubmit方法。
4. 在通用流程类的子类中写入特定流程的代码
在AbstractFlow类的Submit方法中调用空的QuerySubmit和PostSubmit函数。在SimpleFlow的子类内,如我们演示的采购工作流的PurchaseFlow,再此两函数中写入实际的代码。这类似于Java中的实现接口。
与采取事件机制相比,此方法也要求为某个特定的流程写一个类。另一个好处是有需要时在子类中可以覆盖通用流程类的其他方法,例如流程操作配置文档难以满足某一操作选择下一节点的复杂要求时,可以覆盖GetNextNode方法,在其中写入所需的逻辑。
5. 在一个引用流程引擎的脚本库中以函数而不是类的方式编写特定流程的代码
上述两种方式中的类代码都是在某个引用lsSimpleFlow库的脚本库里编写的,那么是否一定要以面向对象的形式呢,直接以一组函数的方式编写特定流程的代码,在主函数中按次序调用通用流程类和其它函数如何?
与方法4相比,此途径只有几个细微的劣处:
因为PostSubmit的逻辑从在SimpleFlow类的Submit方法内调用变为从主函数中调用Submit方法后运行,原本在Submit方法内完成的前端文档NotesUIDocument的一系列Reload、Refresh、AutoRefresh操作也宜移至PostSubmit函数内。再加上QuerySubmit和PostSubmit函数里必然会用到主文档等其他对象,必须写代码获得,而不能如在子类中那样直接引用。
通常QuerySubmit的逻辑是在Submit方法内的表单校验通过后再运行,以函数的方法顺序调用QuerySubmit和SimpleFlow对象时就无法做到这一点。
综合以上讨论,我们选择方法4——在通用流程类的子类中写入特定流程的代码。特定流程类的代码容纳于lsLocalFlow脚本库中,此脚本库名不得不固定下来,因为在调用它的FlowControls子表单需要确定地引用它的名称。子表单的Actions操作最终调用的是这个脚本库里的SubmitFlow函数:
Function SubmitFlow(flow As String, curNode As String, action As String)
If Not IsDebuggingLS() Then
On Error Goto ErrorHandler
End If
‘Dim objFlow As New SimpleFlow(flow,curNode,action)
Dim objFlow As New PurchaseFlow(flow, curNode, action)
Call objFlow.Submit
Exit Function
ErrorHandler:
Call objFlow.RollBack()
MessageBox Error & " occured when flow submitted at line " & CStr(Erl),64,"Error " & CStr(Err)
Exit Function
End Function
从上面的代码可见,如果当前流程没有特殊的业务逻辑,可以使用通用的流程类SimpleFlow,否则便要用一个写好的SimpleFlow的子类,即此处的PurchaseFlow。最后谈谈这里被包含在If语句中的错误处理。
错误处理和模拟事务
上面的代码如果应用普通的错误处理,普通用户看到的错误信息会稍微友好一些。但因为骨干代码都包含在Submit方法里,捕捉到的错误发生的行几乎注定就是这一行,而这对确定真正出错的代码帮助不大。并且因为错误被捕捉了,打开Notes客户端的调试LotusScript工具也没用。如此看来,似乎不应该添加错误处理代码。但此处有一个微妙的问题。流程类的Submit方法执行时如果发生错误,很有可能已经修改了当前主文档,甚至已经修改了文档的状态和存取控制字段的值,在这样的数据“破损”状态下,用户关闭文档时如果保存,则将产生一个无法解释其数据和进行后续操作的异常文档,例如流程已处于下一节点,但文档权限并未改变。通用的流程引擎类尚可以精心编写反复测试,尽量减少出错的可能。但是特定于某流程的代码,同样可能造成上述问题,它们的质量却取决于各自的作者。
在使用其他编程语言和关系型数据库时,保证数据不会处于此种不一致状态的解决方法是事务(transaction)。一个事务内的一系列状态改变,或者成功,或者失败时回滚至原位。LotusScript和Notes数据库不支持此功能。我们能做到的只是尽量模拟,所以AbstractFlow类中有一个RollBack方法,可以将主文档的所有域值恢复到保存在一个临时备份文档中的初始状态。SubmitFlow函数里添加错误处理代码的目的就是利用RollBack方法防止错误发生时主文档的数据处于不确定状态。剩下的问题便是出错时如何调试,最好有一个便捷的途径在不修改代码的情况下使错误处理代码失效。这就是IsDebuggingLS和一个工具代理Tools\Toggle LS Error Handler的由来。它们通过设置和检查一个特殊的Notes环境变量“DEBUG_LS”来控制错误处理代码是否生效。后者将环境变量“DEBUG_LS”的值在0和1之间切换,前者检查该值是否为1。这样在SubmitFlow函数里,如果检查到处于“调试”状态,就不进行错误处理,此时便能用调试器跟踪错误。