阿拉丁和灯

Thoughts, stories and ideas.



业务规则的通用模型 —— DMN简介


背景

在业务代码的开发中,经常会遇到根据多个输入共同决定一个业务规则的结果的情况,比如一个订单的卖家服务费的费率,需要看这个订单的

  1. 出口方式,走阿里出口服务的,不收取服务费(在出口服务那边会有收),
  2. 卖家是否头部卖家,头部卖家服务费6折优惠,
  3. 是否大促优惠订单,优惠订单200美金封顶。 这些规则之间有交叉组合,并且要考虑规则之间的优先级顺序。

类似于这样的需求,在业务代码的开发中非常常见。这样的业务逻辑,可以抽象成多索引列的Lookup Table。类似下图(数据为示意,非实际费率):

对于复杂的业务逻辑,一个Lookup Table无法表达的,可以用多个Lookup Table构成级联,类似下图(示意,不对应以上例子):

(图的具体含义后面会详细说明)

实际上,已经有一个业界标准是用来描述这个Lookup Table。它就是DMN。下面我们就系统地介绍这个标准,以及其实现。

什么是DMN

DMN是OMG(Object Management Group)制定的关于决策建模的标准,全称是Decision Model and Notation(决策模型和标记)。(从名字你可能猜到它可能与BPMN和CMMN有关,事实上确实如此)。DMN标准定义了一套XML格式,并且可在决策引擎或者业务规则引擎上执行。比如开源的工作流引擎Camunda BPM(7.4以上版本)。

决策分层

DMN里面包含两个层次:

  • 输入描述层:需要哪些子的决策或者输入数据,才能够支持我做出正确的决定?
  • 决策逻辑层:有了必须的输入之后,具体怎么做出决定。这部分可以表达成一个决策表或者一个表达式。

示例:保险公司如何分配索赔任务给员工

让我们用一个保险公司的例子来进一步说明这两层的内容。这个例子很简单,但对于很多保险公司来说非常真实,要做的事情就是:决定由哪位员工来处理一个新收到的索赔。

输入描述层

先看输入描述层。我们用一个叫做DRD(Decision Requirements Diagram)的图来表示输入描述层的内容。

保险公司内部的决策业务的逻辑以及它们所需的数据可以用下面这个大图表示:

这个图有点复杂,但是里面的元素类别非常简单,只有两种——决策和输入:

我们需要输入才能做出决策,一个决策的结果可能是另一个决策的输入。在上面的例子中,我们做了一系列的决定以确定接单的员工:

  • 所需的技能/角色:我们希望通过找出硬性约束(即技能或角色)来缩小承担该案件工作的候选员工的数量。在某种程度上,这些约束条件是硬性——不允许没有所需角色或技能的员工处理索赔!输入是索赔的类型,例如第三方责任(“我弄坏了我朋友的手机”)或涉及人员的事故(“我爬山时摔下来摔断了腿”)以及客户是个人或企业的信息 - 这些信息在判断的时候需要用到。
  • 特殊情况:某些保险代理人可能会签订特殊协议,始终由特定员工提供服务。对于拥有庞大框架合同的大客户来说尤其如此。这种情况下输入是处理专员和客户 - 准确地说:专员ID和客户ID(后面会有进一步说明)。
  • 合格的候选员工:前面的决策结果是这个环节的输入:需要哪些技能以及可能已由特殊情况选择的员工。然后,我们需要一份员工及其可用性清单,以确定哪些员工可以处理案件。这里仍然是一个“硬性”决定,因为可以严格地确定一个员工到底能不能处理某项索赔。
  • 员工适当性得分:现在我们转向软约束 - 我们希望将索赔分配给最合适的员工。 “最合适的”不是一个明确的规则,并且可能经常改变。保险业的一个典型例子是暴风雨。在暴风雨灾害之后,某个地区的索赔数量可能会急剧增加。这时你可能希望放宽诸如“在他们所在地区处理索赔的工作”这样的规则,甚至可能会更多地把复杂案件分配给没有经验的员工 - 因为这比坐视所有有资格的员工都很忙,没有人处理这些案件要好得多。处理这些软约束的典型方法是评分模型 - 评估每个员工的适当性,给出相应得分。
  • 确定员工:最终决定哪一个员工处理这个索赔。这个决定很简单:得分最高的员工。如果有多个员工分数相同,则随机选择其中一个。

上面说的这些——确定决策需要哪些输入以及决策之间层次关系,就是DRD要做的事情。DRD有助于理解决策并列出所有必要的输入。在现实生活中,工作量最大的往往是收集决策所需的所有输入数据 - 因为你必须查询各种后端系统,甚至可能引入新的数据存储。在最近的一次研讨会上(业务和IT部门在一个大桌子上!)我们使用DRD来检查每个输入并讨论如何获得它。这真的很有帮助,很有成效。

但是为了理解决策的所有细节并能够实现自动化,我们必须进一步深入到第二层——决策逻辑层:

决策逻辑层

DRD中的每个决策节点都可以由决策表详细定义,该决策表也在DMN中定义。让我们看看我们给出4个表的示例:

我们看图中的table中最简单的一个:

在表格中,每一行都是一条规则。整个表由输入列、输出列、和命中策略构成:

  • 输入列:指定规则的条件。这些列通常与你之前在DRD中定义的输入相关。
  • 输出列:描述输入满足对应条件的情况下,得到的输出结果。
  • 命中策略:定义是否只能“触发”一个规则还是说允许同时“触发”多个规则。如果是触发多个规则,你可以进一步定义处理冲突的方式。以下列表简要总结了这些选项: 只能触发一个规则:  U(Unique):只有一条规则可以匹配 A(Any):多个规则可能匹配 - 必须具有相同的输出 P(Priority):选择具有最高优先级的规则(输出字段必须包含优先级) F(First):选择第一个匹配规则(表中的顺序!)  可以触发多个规则:  O(Output Order):按优先级顺序的规则列表(输出字段必须包含优先级) R(Rule Order):按表的顺序的规则列表 C(Collect):所有命中规则的无序列表,可以进一步与操作符 + (sum),< (min),> (max),#(count) 组合  
  • 完整性指标:指定规则表是否是完备的:
  • C(Complete):完备的。涵盖所有可能的输入组合。
  • I(Incomplete):不完备的。表格允许包含“空缺”。

关于员工经验的决策表是“UC”表,意思是:单一,唯一和完整。与之不同的是得分表,它是“C + I”,意思是:多重,聚合求和,不完整。为什么?

示例中的评分表基本上定义了各种情况下的得分/扣分。如果索赔没有触发任何这些规则是允许的,这种情况下它的得分为0。允许不命中的情况,所以说这个表是不完备的。一个索赔命中多条规则也是可以的,你可能会因为经验不足和已经在处理过多的索赔而扣分。把这些得分/扣分加总到一起,得到你最后的得分。得分表就是描述这些具体的规则,按照前面所说,这个表的命中策略属于“C + I”。

使用camunda BPM执行DMN

至此,我们对规则的分析已经很有价值了。在最近的研讨会上,我们发现虽然这种形式的结构化决策非常有助于达成共识,但却通常不为业务分析师所知。它没有得到广泛使用,有点让人惊讶,因为其基本方法几年前已经有人描述了,例如在决策模型中。但其实也不意外,情况与BPMN 2.0问世的时候非常相似:那时,即使我们之前已经拥有工作流引擎和业务分析方法,但还是只有在能够达成Business-IT-Alignment的标准出现后,相关方法才得到广泛采用。这些标准可以将模型图保存成XML文件并直接在引擎中执行。只有这样,业务分析的结果才不会只存在于没有人会去看的文档里面。

开源的BPM平台Camunda BPM包含了DMN支持,因此它能够执行(如上所示的)DMN决策表。重要的是,Camunda执行的是原生的DMN XML,不必进行容易出错的专有格式转换。

Camunda BPM中的DMN支持包括以下组件:

  • DMN引擎:核心决策引擎是一个小型类库,可以在无状态模式下工作。这意味着你只需将决策表作为XML文件传入,再传入一堆输入(作为Java对象),然后获得结果。不需要依赖其他的任何基础设施来使用它。
  • dmn.io:用于设计和编辑DMN决策表的Web应用程序。它是100%Java Script,这意味着你不需要任何类型的后端服务来运行它。它可以独立运行,在开发期间编辑决策表,也可以轻松嵌入到其他应用程序中。
  • BPM平台集成:通过将DMN引擎集成到BPM平台中,可以利用整个BPM平台的所有功能,这意味着你拥有可以进行版本控制的DMN规则的持久存储库。这允许你只是按名称引用决策表来做出决策。这部分采用与BPMN相同的机制:如果你构建了一个包含DMN决策表的“流程应用程序”,则这些决策表会自动部署。可以在Camunda Cockpit中检查决策定义。dmn.io可以链接到Camunda Cockpit,以便业务员或运营人员实时编辑决策表。

如果你对当前的开发状态感兴趣,可以查看GitHub上的项目:

轻松修改规则

如上所述,使用DMN的主要优势之一是Business-IT-Alignment以及业务人员可以直接更改规则的机会。等一下 - 这听起来不是有点像“零代码谎言”吗?

要回答这个问题,正确地看待这个事情很重要。就个人而言,我认为非IT人员可以编辑具体规则是完全现实的 - 这意味着编辑决策表中的行。你可以插入新行,删除一些行或只是调整行里的值。这正是保险公司想要做的:如果暴风雨发生了,你可以轻松地设定说区域约束在接下来的5天不必严格遵守。你甚至还可以放松对接单的保险专员的某些技能上的要求,这会牺牲决策的质量,但是会照顾到业务处理的速度 - 这可能是值得的(这是一个商业决策!)。

同时,如果要让这个非IT人员来修改规则表的结构,或者去关心规则与环境的绑定(如输入数据类型),是不现实的。这个还是应该由IT人员来做。

关于规则的变更何时生效,批准工作流程,变更的责任等等,可以进行更多的讨论,这里我们就不深入了。

非决策表决策

你很可能想知道上面的决策过程具体是如何执行的。你可能已经注意到我没有为每个决策节点提供决策表。有六个决策点,但是前面图里只提供了四个决策表,原因是剩下的两个决测点不适合用决策表。我们具体来看一下。

  • 确定员工:如前所述,这是一个简单的“选择得分最高的员工”决策。做这事我们不需要一个表。DMN定义了一种名为FEEL(Business "Friendly Enough Expression Language",商业“足够友好表达式语言”)的表达式语言,可以处理这种情况。但它也可以在包含它的Business Process中轻松实现(后面会说),我一般首选这种。
  • 可用的合格员工:这是一个艰难的决定。回顾DRD,你甚至可能会感觉这并不适合DRD。我就这么觉得!问题在于这个决定处理不同的多样性。到目前为止,我们只看了一个对象 - 我们的理赔申请 - 我们得到了它作为输入。现在我们想要获得合格员工的清单。这提出了两个问题:  我是否必须加载每个员工才能将其交给决策引擎? 如何处理来自此决策节点的“多实例”问题,我必须处理多个对象(一个声明+ n个员工)? 不幸的是,DMN中的决策定义(暂时?)还不能处理不同的多重性。所以严格说来,我之前提出的DRD是错误的!下图说明了这个问题:  

我们遇到了一个典型的现实问题,可以通过我称之为“决策漏斗”的方法来解决。注意我们目前正在讨论的是如何在内部实现中解决DMN的这个问题。现阶段,解决方法是把决策挂钩到整个BPMN流程中,因为流程实际上经常是需要的——我们通常必须要启动流程才能执行服务调用以收集输入数据,这并没有增加我们的负担。下面细说。

决策漏斗

如果你有指定在某些情况下选择某个员工的规则,则必须遵守某些非功能性要求。例如,加载一家大公司的所有员工只是为了“排除”99%的员工(因为他们不被允许做某项任务,比如处理索赔)可能不是一个好主意。下面这样做可能会更好:

1.确定要求/标准。
2a. 加载符合条件的所有员工(通过服务调用或简单的SQL)。
2b. 也许由DMN做出一些过滤,例如为了照顾特殊情况,或者在性能和易于改变的规则之间取得最佳平衡。
3.为所有找到的员工打分。
4.选择得分最高的员工。

使用BPMN + DMN实施决策漏斗

以下BPMN流程为理赔分配实现了该漏斗。 通过BPMN,我们可以获得订单活动和多实例语义:

上面提到的决策表可以挂钩到BPMN业务规则任务中:

执行完整的示例

为了在Camunda BPM上执行此示例,你需要做什么? 很简单:

  • 将决策表保存为DMN XML文件。
  • 添加绑定,这意味着你可以为每个列清楚地定义它如何映射到输入/输出数据对象(Java)。 这时,在Camunda BPMN中可以使用JUEL(Java统一表达式语言)。
  • 从业务规则任务调用规则引擎。

让我们看一下XML长什么样,获得对其的一些基本理解。我将一个规则标记为粗体 - 因此你可以跟踪其绑定:

<?xml version="1.0" encoding="UTF-8"?>
<Definitions xmlns="http://www.omg.org/spec/DMN/20130901"
   id="definitions" name="camunda" namespace="http://camunda.org/dmn">   
   <Decision id="decision" name="CheckOrder">
      <DecisionTable id="experienceOfEmployee" name="Experience of Employee" isComplete="true" isConsistent="true">
         <clause id="input1" name="Approval Authority">
            <inputExpression id="inputExpression1" name="Approval Authority">
               <text>employee.approvalAuthority</text>
            </inputExpression>
            <inputEntry id="inputEntry1">
               <text><![CDATA[< 1000]]></text>
            </inputEntry>
            <inputEntry id="inputEntry2">
               <text>[1000..10000]</text>
            </inputEntry>
            <inputEntry id="inputEntry3">
               <text><![CDATA[> 10000]]></text>
            </inputEntry>
         </clause>
         <clause id="output1" name="Experience" camunda:name="experience">
            <outputEntry id="outputEntry1">
               <text>low</text>
            </outputEntry>
            <outputEntry id="outputEntry2">
               <text>medium</text>
            </outputEntry>
            <outputEntry id="outputEntry3">
               <text>high</text>
            </outputEntry>
         </clause>
         <rule id="rule1">
            <condition>inputEntry1</condition>
            <conclusion>outputEntry1</conclusion>
         </rule>
         <rule id="rule2">
            <condition>inputEntry2</condition>
            <conclusion>outputEntry2</conclusion>
         </rule>
         <rule id="rule3">
            <condition>inputEntry3</condition>
            <conclusion>outputEntry3</conclusion>
         </rule>
      </DecisionTable>
   </Decision>
</Definitions>

这基本上意味着为了使rule1触发,我们计算表达式#{employee.approvalAuthority> 1000},当值为真时,我们设置一个结果experience = 低。对更多细节感兴趣的人,可以参考bernd.ruecker的GitHub代码仓库。

展望

到目前为止,DMN的第一步非常棒!我们喜欢这个方法,喜欢有可执行标准。Camunda BPM中的轻量级实现已经是非常不错的一个实现,这使得执行决策非常容易。当然 - 它仍然处于alpha阶段 - 但是有用户评论说:“即使是那个pre-alpha版本也比我们试过的其他现有规则引擎产品更好。”

参考资料

http://www.bpm-guide.de/2015/07/20/dmn-decision-model-and-notation-introduction-by-example/

https://camunda.com/dmn/

注,本文主要内容翻译自 Decision Model and Notation (DMN) – the new Business Rules Standard. An introduction by example. 。背景部分为我添加的。翻译本文的目的是因为在业务中看到一些共通性的问题,而其手段已经有了,标准都出了,但是在开发人员中还并不广为人知,存在很多重复分析轮子,发明轮子的情况。希望能给该类问题介绍一个通用的好用的方案。



View or Post Comments