65.9K
CodeProject 正在变化。 阅读更多。
Home

Logician:一个基于表格的规则引擎套件,支持 C++/.NET/JavaScript,使用 XML

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (52投票s)

2017年3月9日

GPL3

14分钟阅读

viewsIcon

106994

downloadIcon

6883

概述了一个跨平台的、基于电子表格的规则引擎,其算法实现于 C++ 和 JavaScript 中,并提供了 C#/.NET/WinRT 的封装。

介绍 

应用程序逻辑,特别是业务规则,在代码中维护起来会很混乱且耗时。如果所有的应用程序逻辑都硬编码在代码中,最终可能导致大量的 if-then-else 或 select-case 代码段,演变成巨大的噩梦。开发者有更重要的问题要解决,有更多的事情要做,而不是去维护一堆字符串比较、布尔测试或存储过程。规则引擎(也称为“推理引擎”)的尝试已经有很多,但其中许多需要编写更晦涩难懂的代码,这些代码对于非开发人员来说,如果不是不可能,也很难维护。将业务逻辑与应用程序解耦无疑可以使代码更健壮、更易于维护,并使非开发人员的主题专家能够维护数据和规则模型,前提是它们合乎逻辑且易于理解。当然,您总是可以将应用程序的某些部分链接到数据库,但这仍然需要大量的开发工作来定义应用程序中每个独特的“规则驱动”事件所需的数据模型和查询,更不用说在网络化或资源受限的环境中出现数据库性能瓶颈的可能性了。基于表格的规则引擎可以成为您应用程序逻辑和自动化需求的强大而灵活的解决方案。在 Web 环境中,Logician JavaScript 库还可以将大量服务器 CPU 转移到用户的浏览器上,并消除服务器延迟回调。

决策表

决策表,或者数学家或电子/计算机工程师可能称之为“真值表”,它只是一个电子表格,用于在给定一组输入条件的情况下定义问题的可能解决方案。例如,假设我们要混合油漆,我给您展示下面的电子表格

PaintColor1  PaintColor2  ResultColor
Red          Blue         Purple
Red          Yellow       Orange
Blue         Yellow       Green

无需编写任何代码,甚至无需提供关于问题的更多背景信息,数据的含义就很清楚了。从编程角度看,我们可以将其视为一系列从左到右、从上到下的 if-then-else 语句。

if (PaintColor1 == "Red" && PaintColor2 == "Blue")
{
  ResultColor == "Purple"
}
else if (PaintColor1 == "Red" && PaintColor2 == "Yellow")
{
  ResultColor = "Orange"
}
//....etc

或在 SQL 中

SELECT ResultColor WHERE PaintColor1 = @PaintColor1 AND PaintColor2 = @PaintColor2

这三种解决方案中的任何一种都可以完成任务,但第一种对非开发人员来说肯定更容易理解,并且是现实世界中许多实际工程和/或业务数据维护的方式。使用数据库驱动规则在一定程度上确实将逻辑与源代码解耦,但在 Web 环境中,您必须使用服务器回调或 Web 服务来检索数据,从而减慢应用程序的速度。如果逻辑需要更改,而我们开始混合三种颜色的油漆怎么办?对于数据库,您可能需要返回并更改数据库表模式和您的 Select 语句/存储过程。如果您有几百种颜色组合,然后向硬编码方法添加第三个输入参数,那么您将面临大量单调且容易出错的编码。如果您选择了决策表,您可能只需要做很少的工作,除了复制新规则 XML 文件(很可能是非开发人员/主题专家为您编辑的)以及添加的第三个输入到您的网站或应用程序。在本教程中,我将向您展示如何使用开源 Logician 套件来完成此任务。通过 Logician 套件,您将获得三个基本组件:一个决策表评估库、一个决策表编辑器以及一个动态数据/类建模器和规则引擎库。

决策表引擎背景

如前所述,表规则是按顺序读取的;对于给定的一系列输入,将确定输出。在上面的示例中,它们是从上到下、从左到右工作的。决策表评估器(EDSEngine 库)会根据您的代码提供的信息以及存储的 XML 规则数据创建一个真值表。它本身是无状态的,但很容易确定和提供必要的变量。发生的基本步骤是:

  1. 代码确定需要评估表,并询问 EDSEngine 当前应用程序“状态”需要哪些输入值。
  2. EDSEngine 加载表并返回表中的输入列表。
  3. 代码提供这些输入的相应当前值列表。
  4. EDSEngine 评估决策表并返回结果。

取决于您的数据模型设计,您应该能够在代码中自动化步骤 1-3。像这样的简单内容可能有效:

//C++
map<string, string> mAppData; //application state as attribute-value pairs
CKnowledgeBase m_TableEvaluator;

//...application stuff, you loaded the rules file, etc

string GetResultingColor()
{  
  return GetSingleSolution("ColorMixingTable", "ResultColor");
}

string GetSingleSolution(string tableToEvaluate, string nameOfOutput)
//could reuse this function for all similar aplication events
{
  vector<string> inputsNeeded = m_TableEvaluator.GetInputDependencies(tableToEvaluate);
  //from our application data, obtain the values
  for (int i = 0; i < inputsNeeded.size(); i++)
    m_TableEvaluator.SetInputValue(inputsNeeded[i], mAppData[inputsNeeded[i]]);
  
  vector<string> results = m_TableEvaluator.EvaluateTable(tableToEvaluate, nameOfOutput);
  //EDSEngine supports returning multiple true results on a sigle line,
  // but in this case we expect just a single result (the first one it finds)
  
  if (results.size() > 0)
    return results[0];
  else
    return "";
}

在此示例的代码中,可以在 ColorMixConsole 应用程序中找到。规则表存储为 XML,当由 DecisionLogic 表编辑器实用程序“编译”时,它们会链接到单个 XML 文件中。规则引擎中存储的所有值本质上都是字符串,因为它们会被序列化为 XML。为了优化性能,在可能的情况下会避免字符串比较,通过对规则表中的所有存储值和传入的任何输入值进行数值标记化。这样,大多数时候只是比较数字。所以在内存中,之前的油漆颜色表看起来更像:

PaintColor1   PaintColor2   ResultColor
0             1             3
0             2             4
1             2             5

假设我们将“蓝色”和“黄色”传递给之前的油漆表。用于 `PaintColor1` 和 `PaintColor2` 的用于测试的值也相应地被分配为 1(蓝色)和 2(黄色)。您还可以对输入值执行以下布尔运算,并且会发生反标记化:

  • >:大于,字母数字或数字
  • <:小于,字母数字或数字
  • != 或 <>:不等于
  • [x,y]:值范围,包含两端
  • (x,y):值范围,排除两端

您可以混合使用 [] 和 ()。= 不会显式使用,这是规则单元格的默认行为,不需要字符串反标记化。

在运行时,一旦您传入表的值,它就会按顺序分解为一系列布尔单元格,其中每个单元格的值为 true 或 false。您留空的任何输入单元格都始终被视为 true。所以,如果我们传入 `PaintColor1` = "蓝色" 和 `PaintColor2` = "黄色" 的值,我们之前的决策表看起来很像一个逻辑 AND 门:

PaintColor1   PaintColor2   ResultColor
F             F             F
F             T             F
T             T             T <==This is our solution, corresponding
                              to the tokenized memory value of 5, 
                              whose string value is "Green"

其他值得注意的 EDSEngine 功能

您可以在输入单元格中指定多个值,这称为“OR”,并且测试将像代码中的“or”一样检查它们与输入值的匹配程度:`if (value1 = test || value2 = test || value3 = test ) then do something...` 在单元格中缩写为 `value1|value2|value3`。如果使用表设计器工具设计规则 XML,还有一个“全局 OR”的概念。列出许多值可能会浪费大量打字时间,因此您可以定义一个值列表作为变量,并在所有项目表中重复使用该变量。在输出单元格中,“|”分隔符的作用类似于“and”(&&)。通过这种方式,您的解决方案可以返回多个值。表评估的结果始终作为数组(C++ 中的 vector)返回。还有一个表被“获取一个”或“获取全部”的概念,这意味着表设计者打算您只返回第一个 true 行的结果,或者所有 true 行的组合唯一结果。这可以在 DecisionLogic 设计器中为每个表进行选择。您当然总是可以选择在代码中覆盖它。

您可以使用 `get()` 关键字在运行时动态连接值到单元格。例如,假设我们需要文本输出以显示价格列表,并希望通过规则驱动文本。我们可能希望它显示:“您已购买 X 件价格为 P 的商品”。在表中,我们可以创建一个输出:“您已购买 `get(QtyOfItems)` 件价格为 `get(ItemPrice)` 的商品”,其中 `QtyOfItems` 和 `ItemPrice` 是我在应用程序状态中提供的值。您还可以使用 `get()` 在输入中创建更动态的测试。代替输入单元格“`>55`”,它可以是“`>get(SomeValue)`”。

输出单元格支持使用 Python(C++/C#)和 JavaScript(所有端口)进行运行时脚本编写,以便您可以执行数学计算并实现更复杂的规则。您的输出单元格将仅包含正确关键字内的 Python 或 JavaScript 代码片段,即 `js()` 或 `py()`。对于单行代码,它可能看起来像:

 

js(return (56 * 3).toString())
//Note: you can actually omit the "return"
//and ".toString()" for a single line of code:
js(56 * 3)
//Combine eqautions and variables
js(56 * get(MultValueFromCode))

如果您的代码有多行/函数,请确保它在最后显式返回一个字符串,否则您将收到类型转换错误。当与 `get()` 关键字结合使用时,这会更有用,例如:`js(get(value1) * get(value2))`。另外请注意,Python 仅在 EDSEngine 的 C++/C# 实现中受支持。基于 JavaScript 的脚本在 Web 上更具可移植性,因为它是 Web 浏览器的原生运行时脚本语言。

一个相当高级但灵活的功能是回调参数。有特殊的表评估函数,其重载支持将额外数据传递给 EDSEngine,该数据也会传递给 JavaScript 或 Python 代码(`EvaluateTableWithParameter`)。基本思想是,您可能希望将您的应用程序中的某些文本或 XML 数据发送到规则,在脚本中对其进行修改,并将修改后的数据连同常规结果一起传递回来。如果此功能可能对您有用,您可以在开发人员文档中找到更多详细信息。

关系对象模型和实现规则引擎

关系对象模型库的使用将演示如何通过自己的功能来扩展 EDSEngine,以构建一个功能齐全的规则引擎。在电子商务环境中,与其编写显式类来模拟物理产品,不如使用类似 XML 的树状对象结构来模拟产品可能更有用。例如,假设我们要模拟一辆汽车。我们可能会编写 C++ 类,如:

class CPriceableItem
{
public:
  CPriceableItem();
  string CatalogNumber;
  double Price;
  double Cost; 
};

class CEngine : public CPriceableItem
{
public:
  CEngine();
  string EngineType;
};

class CTires : public CPriceableItem
{
public:
  CTires();
  string TireType;
};
//etc, keep inheriting and adding special attributes to each class

如果我们直接将整个 Car 作为 XML 来建模和处理,最终状态可能会变成:

<Object name='Car'>
  <Object name='Engine'>
    <Attribute EngineType='V6' Price='9000' Cost='4000' CatalogNumber='V6-OCTC-GM'></Attribute> 
  </Object>
  <Object name='Tires'>
    <Attribute TiresType='17inch' Price='500' Cost='175' CatalogNumber='GY17'></Attribute> 
  </Object>
  <!--And so forth.....-->
</Object>

在本教程的其余部分,我们将利用 ROM 内置的自动化功能与 EDSEngine 结合。您可能还会发现不必为许多可能需要的算法编写代码会很方便,例如对 `Car` 对象求总价。ROM 支持将数据视为 XML 并支持 XPath 查询。您只需使用 XPath 查询即可获取 `Car` 对象的总价,甚至可以在表规则的输出单元格中使用 `eval()` 关键字来执行此操作:“总价是 eval(sum(//Attribute[@Price]))”,最终文本结果为“总价是 9500”。

使用 ROM 组件时,决策表会针对特定的“Object”节点上下文进行评估,并在当前上下文中找不到输入依赖值时向下钻取到父节点。您也可以在输入列标题中使用 XPath 查询,而不是在代码中处理输入值,或者指定多个“Object”节点之间的关系。有关更多信息,请参阅项目文档。应注意,内部数据存储机制不是 XML,因为那样会造成性能瓶颈。但是,当前状态可以随时序列化,并在进行查询时更新。

教程

在本教程中,我们将演示如何使用 Logician 工具在支持 JavaScript 的网页和 C# WinForms 应用程序中建模一个真实的“重型磨机型”液压缸产品配置器(提供技术规格 PDF 和源代码)。为了演示,我们将尝试为商业上可用的“重型磨机型”液压缸建模其属性和目录号生成。根据目录号生成公式,可以定义一个简单的图形布局或一组选择。每个控件的名称将匹配我们在决策表中使用的“属性”名称。

在任何规则评估发生之前,您必须实例化关系对象建模器。您可以将规则 XML 文件的路径也传递给它,以便在第二步加载它们,然后最后设置内置规则引擎实现并应用规则。

//C# port, Javascript is exactly the same without the namepsaces
ROMNET.ROMNode m_rootNode = new ROMNode("HydraulicCylinder");
m_rootNode.LoadRules("HydraulicCylinderRules.xml");
ROMNET.LinearEngine m_engine = 
  new ROMNET.LinearEngine(m_rootNode, "HydraulicCylinderDictionary");
m_engine.EvaluateAll();

根据产品文档,使用各种下拉列表和编辑框构建一个最小的 GUI 界面,让我们开始根据产品资料定义一些简单的规则,以便我们可以测试即将构建的应用程序事件/评估代码。打开 DecisionLogic 表编辑器并开始一个新项目。对于界面中的每个控件,我们将定义一个单独的、名称相同的决策表,该表指定在任何必要输入条件下的可用值。此外,ROM 要求我们至少创建一个“字典”表,其中包含我们正在使用的所有属性名称、它们的标题/描述等。创建一个名为“HydraulicCylinderDictionary”的新表,并用我们将用于模拟产品的属性填写它。它们应与配置器 GUI 中创建的控件名称匹配。“Name”和“Description”列不言自明。“DefaultValue”列将在应用程序启动时设置属性的值,如果“RuleTable”列中没有为它定义规则表。“AttributeType”列必须是以下类型之一:

  • `SINGLESELECT` - 用于组合框/下拉列表、单选按钮等控件。
  • `MULTISELECT` - 多选列表框。
  • `BOOLEAN` - 复选框。
  • `EDIT` - 文本编辑字段。
  • `STATIC` - 只读属性。

值可以通过“DefaultValue”列设置,或通过规则进行评估。

图 1

第一个输入是“CylinderSeries”。作为气缸配置的第一个参数,它没有输入参数,因此可以从表中删除所有“输入”列。根据产品文档,我们可以看到有三种可能性:3000 psi 液压缸、2000 psi 液压缸和 250 psi 气动缸。我们可以将它们列出在“获取全部”表的三个单独的输出单元格中,或者像前面描述的那样将所有三个放在一个输出单元格中,用“|”分隔。在这种情况下,这只是个人风格问题。这里我们将采取后者。

图 2

我们继续为“字典”中定义的每个属性填写规则表。一个具有输入条件的属性示例是“RodDiameter”。在文档中,可用的杆尺寸似乎受所选 BoreDiameter 的限制。由于每个 BoreDiameter 有多种可能性,如果我们自动设置 RodDiameter 的默认值,在用户选择缸径时可能会节省用户时间。这可以通过在值前加上“@”符号来完成。然后,我们可以创建以下规则表:

图 3

任何像样的规则引擎都应该能够识别无效条件和适当的“触发器”来强制重新验证当前选择。当您测试示例应用程序时,您会注意到,如果您将 BoreDiameter 更改为不同的值,并且当前选择的 RodDiameter 超出有效范围,该屏幕上选择的值将被清除或更改为正确值,具体取决于当前条件。对字典属性的任何更改都通过事件处理和规则引擎的“`EvaluateForAttribute`”函数进行路由,该函数将根据规则集对当前属性值进行任何必要的更改。对象建模器提供了可用的方法,以便获取或设置属性的值,或者在需要时将整个应用程序状态转储为 XML。

为了生成给定属性选择或“状态”对应的目录号,我们将定义另一组规则表并直接调用表评估:

//C# port
private void UpdateCatalog()
{
  //catalog number is the concat of all the chars returned
  //from the CatalogNumber table evaluation

  string[] allChars = m_rootNode.EvaluateTable("CatalogNumber", "Code", true);
  string Catnum = "";
  foreach (string subStr in allChars)
    Catnum += subStr;

  if (Catalog != null)
    Catalog.Text = Catnum;
}

由于将目录号字符串的所有规则放在一个表中会很混乱,我们可以创建一个“获取所有”样式的表,并使用 `eval(TableName, OuputColumnName)` 表函数评估每个目录号字符的单独表,如下所示,从一个表分支到另一个表:

图 4

图 5

图 6

在 Web 上使用这些程序包的一大优点是,您可以有效地消除运行业务逻辑和执行页面更新的服务器回调。您能够将大量应用程序逻辑和 CPU 周期转移到用户的浏览器上。所有这些组件协同工作,您就拥有了 Logician,一个强大、灵活且开源的规则引擎应用程序框架。

下载 C#、C++、JavaScript、Flash 和 WinRT 的示例应用程序代码以及规则表示例,以更好地理解应用程序逻辑。访问我们的项目网页 http://logician.sourceforge.net 获取 Windows 二进制文件和安装程序包。请注意,从源文件编译需要 Boost C++ 库。

历史 

  • 2011年6月23日:更新了示例下载。
  • 2013年4月8日:更新了 WinRT 支持和 x64 构建。
  • 2016年8月7日:弃用了 Silverlight 和 Flash 封装器。新的简单示例产品。
© . All rights reserved.