.NET 企业级架构(二)

原文:zh.annas-archive.org/md5/e2f167345d4aed05295f494546f1d634

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:探索领域驱动设计和语义

上一章以提供一种处理关键格式和设计一个适应演变的、功能正确的实体的方法结束,当在这个精确领域没有标准时。这是我们本章的主题。

为了达到这个目标,一个非常重要的先决条件是始终以功能术语进行思考。我知道你们大多数人肯定有技术背景,可能会想知道我们何时才能最终接触到代码。让你们等待这么长时间而不进行任何技术操作是有意为之的,这也是本书阅读过程中提供的教学旅程的一部分。你们必须尽可能坚持功能和业务相关的概念,因为一旦将这种知识转化为软件,它就会变得固化,并且修改起来更加困难。我保证,从下一章开始,我们将开始做一些技术决策,然后,在接下来的几章中,我们将编写一些代码,以非常具体的方式展示这一切的含义。但就目前而言,让我们只关注业务功能,并像清洁架构所教导的那样,不考虑任何与技术相关的内容,仅仅从功能角度思考。这是我们构建正确信息系统的主要保证。实际上,如果你只从这本书中记住一件事,我希望那就是这个实践:从功能角度尽可能长时间地思考你的问题,然后才开始从技术角度思考如何处理它。尽可能推迟实施;考虑数据,而不是数据库;考虑模型和业务规则,而不是属性和方法。

如果你这样做,你很快就会对业务领域中使用的词汇有所疑问。技术方法有一个巨大的缺点,那就是限制了方法。但我们应该至少认识到,它迫使我们非常精确,因为计算机就像一箱石头一样愚蠢,它们强迫我们在信息指定上明确表达。考虑语义并使用称为领域驱动设计DDD)的方法将帮助我们从功能角度进行精确思考,但不会依赖于任何可能阻碍我们未来发展的技术;这样,我们就能兼得两者之利。

一旦理解了这种方法的原则,我们再次回到我们的长期示例,并将 DDD 应用于我们的示例信息系统,以绘制其边界上下文并描述其通用语言(我们很快将解释这两个重要概念)。

最后,本章将解释所有这些如何应用于我们试图设计的清洁信息系统,以及服务与 API 概念的联系,这些联系我们在上一章中已经介绍。在那里,我们将讨论业务实体生命周期分析的重要性,并讨论信息系统架构的一些最近趋势。

在本章中,我们将涵盖以下主题:

  • 功能问题的功能方法

  • 语义的重要性

  • DDD

  • 应用到清洁信息系统架构

  • 链接到 API 和服务

功能问题的功能方法

如介绍中所述,使用功能方法来解决设计关键格式的问题至关重要。在四层 CIGREF 映射中,所有层都是上一层的后果。因此,在没有正确设计第二层(业务能力)中研究的上下文的情况下,就开始从第三层(软件)开始,这必然会导致软件出现故障。更糟糕的是,一旦变成软件,错误将在代码中修复,并且可能通过被整个信息系统或外部系统中的许多用户和机器使用的 API 共享,这几乎使得纠正设计错误变得不可能。

大多数 IT 问题都源于我们经常谈论的业务对齐不足,而现在,我们正处在问题的根源:业务实体的设计。当我们没有专门的规范可以依赖并避免复杂的推理过程,其中充满了可能导致重大后果的误解风险时,我们必须特别注意任何细节,在实践中,这意味着对业务领域专家的广泛和保证的访问。

标准通常用技术术语表达,以便非常精确和无可辩驳,但它以共享和公认的方式代表了一个功能概念。例如,RFC 7519 描述了 JSON Web Token 是什么,以及发行者、主题、过期时间以及所有其他属性的作用,但它以非常受限的方式(对之前引用的信息的isssubexp有精确的定义)。这样,我们可以说规范既存在于 CIGREF 映射的 2 层和 3 层,又将它们联系在一起。这就是为什么规范和标准如此重要的原因,因为它们是业务/IT 对齐的具体行动者。以第二个例子来说,OpenAPI 也是规范和标准如何弥合功能方法和软件方法之间潜在差距的一个很好的说明,它提供了一份所有应在特定业务域的 API 上可访问的功能列表,同时,它还给出了关于这意味着在服务器之间交换的数据流中的精确 JSON 或 YAML 技术描述。

关键格式应通过结合其功能方面和技术表示来达到相同的结果。这就是为什么无论使用什么技术手段来描述它都很重要的原因。这些技术手段可以是 XML Schema 或 DTD,如果你正在使用 SOAP Web 服务;或者 OpenAPI,如果你正在设计 API 及其组件;甚至可以是一个简单的 Excel 文件,显示你打算在系统中传输的数据消息的确切名称和结构。唯一重要的是,它必须是技术性的书写,但不能具有技术限制性。

“技术上撰写,但技术上不具限制性”这个短语可能看似矛盾,所以让我来解释一下。规范的撰写技术很重要,因为它确保了精确性(没有人希望对重要事物有一个模糊的描述)。这就是为什么新的 API 必须用 OpenAPI 合同来描述。这样,就不会有关于属性如何书写的争论,无论是用大写字母还是不用,或者只使用第一个字母;例如,在 OpenAPI JSON 或 YAML 中的计算机化文本中书写,因此不可能有讨论。然而,同时,应注意关键格式(就像任何规范一样)绝不应受到任何技术问题的限制。我们都同意,如果规范的作者使用了某些会使规范难以与其他平台一起使用的 Java 原语,那么用于表示国家等的规范就没有任何意义。这将是一种有限的、技术性的实现方式,但绝对不是真正的规范。同样的原则也应适用于你的关键格式设计,它绝不应暴露出你的技术实现中的任何内容,即使是以功能方式表达。

顺便说一句,这也是在设计软件之前始终从功能角度出发的另一个原因。如果你在这个阶段强迫自己放弃数据库选择,例如,你会减少创建与你的数据库方向绑定的关键格式的可能性。我理解这听起来可能有点不切实际,你可能想知道有人如何将数据设计绑定到数据库上。好吧,魔鬼藏在细节中,不幸的是,有许多方式——有些比其他方式更微妙——会陷入这个陷阱:

  • 人们可能会使用仅在某些数据库中可用而在其他数据库中不可用的类型来表示数据属性。例如,如果我们习惯于谈论VARCHAR(n),那么在我们的数据设计中可能会暗示属性的大小有限制,尽管从功能角度来看并不合理。每个人都见过一个应用程序在姓氏过长时截断姓氏,尽管这会创建一个错误的数据值。

  • 同样,日期格式也可能发生这种情况。参考 ISO 8601(也称为 ISO-Time)的规范在行政日期和瞬间之间做出了明确区分,但大多数数据库并没有。如果我们从 SQL 的角度思考,我们可能会错过这个基本区别。

  • 标识符可能会受到众所周知的数据库机制的影响。基于计数的 SQL 数据库自动生成标识符相当实用,但这些标识符的可扩展性非常差,这是这些数据库缺乏分布的一个原因。全局唯一标识符GUIDs)更好,并且通常被更现代的系统如 NoSQL 数据库所使用。然而,如果你需要为一个在健康信息系统中代表患者的实体分配一个唯一的标识符,这两种选择都将绝对不合适,因为在这个特定情况下,广泛认可的(有时是法律要求的)标识符是国家安全号码。

实际上,有很多其他情况,一些技术知识可能会浪费关键格式的设计,我因此养成了习惯,总是通过仅由产品所有者组成的团队来设计这些格式,甚至在一些情况下,我会检测到那些有技术背景的人并将他们排除在设计团队之外。尽管我仍然可能严重影响到这个过程,因为我有技术方法,但通常我帮助设计我在业务领域没有太多经验的临界格式,因此,扮演一个完全的初学者角色,对业务领域一无所知,然后只专注于这种理解。此外,我根据经验知道,早期技术思维可能会产生负面影响,所以我总是思考因为这一点可能会出什么问题。

有时,耦合可能非常微妙。例如,让我们以一个 URL 为例,如https://demoeditor.com/library/books/978-2409002205。它听起来像是一个很好的标识符,因为它仅基于规范(URL、书籍的 ISBN 和主机的 DNS)并且显然没有其他内容。然而,有人可能会争辩说,使用(https://)方案作为前缀已经暗示了我们将如何技术性地访问这些功能实体,在这种情况下,通过基于网络的 API。幸运的是,总有一个解决方案,在这种情况下,就是通过求助于urn:com:demoeditor:library:books:978-2409002205

在本书的这一部分,你可能会希望相信,从功能的角度来看待问题总是最佳选择,而技术方面应该在之后考虑。话虽如此,我们需要一种方法来仅从功能的角度分析问题,这就是语义可以发挥作用的地方。

语义的重要性

在上一节中,我们展示了如何使用技术支持但不耦合的技术方法来定义精确的实体格式。然而,我们还没有涉及到功能分析本身,观察我们的示例 URN,urn:com:demoeditor:library:books:978-2409002205,我们可以在字符串的不同部分中找到需要进一步分析的内容:

  • urn:这是 URI 的方案。它在这里只是为了说明这是一个统一资源名称。

  • com:demoeditor:这是demoeditor.com的逆序,即我们示例公司的域名。信息的存在是为了作为前缀,以区分具有相同名称的另一个供应商的实体,并且它被逆序是为了保持从最粗略到最细粒度的逻辑阅读顺序。

  • 978-2409002205:这是一个示例 ISBN。再次强调,一旦可能,并且这在关键格式中至关重要,我们就将其转换回现有的标准。几乎每一条信息都有规范!

  • librarybooks是 URN 中具有某些功能价值的部分,我们尚未解释它们的来源。现在我们先假设library是域(书籍和其他相关实体的管理单位)而books是用于描述DemoEditor管理的这些资源的名称。我们稍后会回到这个问题。

x24b72代替books,他们根本不会介意;而使用你信息系统中的术语引入误解,最终必然会在某个时刻造成问题。

让我给你讲一个关于这个的轶事:我在一家信息领域公司担任顾问,与他们进行的一次研讨会是关于围绕购买信息的个人设计一个关键格式。营销人员和销售人员都在场,在某个时刻,他们的声音开始升高,因为他们对使用的术语有不同的看法。他们的争论是关于潜在客户客户之间的关系。营销人员解释说,客户是最佳的潜在客户,因为他们已经了解公司,而销售人员则回答说,商业管道在事实上的确很清晰,一个冷线索变成热线索,然后变成潜在客户,最后如果购买了东西,就变成了客户,从而——根据定义——失去了潜在客户的状态。实际上,他们两个都是对的,模型中只是缺少了某些东西:即“客户”和“潜在客户”不是实体的名称,而是商业规则。如果模型中包含产品提案的概念,那么事情就会变得清晰:特定产品的客户确实是同一公司目录中另一产品的绝佳潜在客户,但他们仍然不是第二个产品的客户。

阅读这些内容,你可能会说这种情况是无害的,并且没有造成伤害,因为讨论澄清了问题。这会导致忽视两件事。首先,这种误解在营销和商业之间造成了一些真正的紧张关系,以及不完整的未来销售报告,这些问题持续了几个月,直到我有机会在公司 CTO 组织的工作坊中发现问题。其次,当只有口头误解时,这确实是好的,但真正的问题是这个错误已经被固化到信息系统(记住,你应在了解第二层的分析背景之后再开始处理第三层)。如果这只是单个公司的错误,那倒不是什么大问题,但即使是 ERP 编辑(我将不提及任何名字)也会在他们的默认数据库模型中犯同样的错误!其中一些拥有名为customerssuppliers的数据表,这可能会引起很多麻烦。

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_09_01.jpg

图 9.1 – 错误的语义

当你合作的公司不仅是你的客户,也是你的供应商时,这种情况会发生什么?这在谈判市场中非常常见,而且通常人们不会犯这个错误。然而,在这个我将不提及的 ERP 系统中,编辑显然没有理解他们想要覆盖的所有市场,并试图提出一个通用的模型,该模型不适合任何可能发生这种情况的公司。当然,当你发现这个问题时,你认为顾问们是如何处理的?答案是:他们试图使用第三层技巧来弥补第二层的问题。我记得,顾问们首先创建了一个数据库触发器,当客户更改地址或银行坐标时,会将修改后的信息复制到suppliers数据表中。然后,几个月后,当问题发生时,他们实施了相同的触发器来修改customers数据表,当供应商是修改者时,创建了一个无限循环,导致数据库崩溃!

如果数据表是设计成一个单独的actors数据表(或者如果你只处理这类演员,可以是individualsorganizations),事情将会简单得多。客户的观念将简单地由一条业务规则产生,该规则指出,如果一个记录存在于指向此演员的orders数据表中,并且日期值不超过 18 个月,则该演员是一个客户。同样的规则也适用于供应商,即如果存在指向此演员的incoming-orders数据表中的记录,或者equipment数据表中的条目有一个保证所有者指向此演员,则该演员是一个供应商

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_09_02.jpg

图 9.2 – 正确的语义

那些业务规则当然是纯粹任意的,但请注意,即使规则发生变化,实体模式也不会有任何改变。这可能是这个模型中最重要的东西。如果营销部门在某个时刻决定规则应该是客户列表只包含过去 12 个月内与我们有过业务往来的演员,而不是 18 个月,会发生什么?这标志着糟糕设计中的真正问题开始出现,因为你将不得不创建一个迁移程序来将客户从表中移除并激活存档程序。由于你可能有一些待处理的订单,风险是失去指向正确数据的指针,以及许多其他事情可能会出错。另一方面,如果有一个正确的模型设计,我们应该怎么做?嗯,简单地修改业务规则!如果它在代码中,你可以将18改为12并重新编译。如果你事先足够小心,这个业务规则可能在某个自定义属性中,你甚至不需要重新编译或部署任何东西。此外,如果你有一个生成客户列表的报表 API,那么你今天真是太幸运了:你修改了这个实现,而无需采取任何其他行动,系统的每个地方的行为都会改变!

你可能会认为这些例子太简单了,并且这种方法无法应对真实系统的复杂性;实际上,恰恰相反,因为这个方法是基于在软件模型中设计业务复杂性。例如,在前面的例子中,我们完全可以有一个不同的业务定义地址和信息系统的所有者可以决定地址不应该在客户和供应商之间共享,或者可能只在某些情况下共享。例如,一些地址将仅用于客户,如交货地址。没问题:我们会通过将地址与演员分开来调整模型,然后为它们添加“类型”信息,以便在演员是客户时,只有交货地址才能被指向。我们甚至可以添加一些授权规则来确保当演员被视为供应商时,这个地址甚至不会被读取!再次强调,良好的设计本应允许这一切顺利,但你必须获得这种清晰的设计。此外,这恰好是你架构师工作中最困难的部分之一——汇集领域专家并得出接近完美的结果。幸运的是,存在一些方法来结构化这项工作。是时候介绍 DDD 了。

DDD

DDD(请注意,最后一个 D 并非指 开发,而是指 设计)是由埃里克·埃文斯创建并记录在其基础书籍《领域驱动设计:软件核心的复杂性处理》(2003 年发布,自那时起,它就广为人知为 蓝皮书)中的一种完整的功能设计方法。尽管这本书相当难读,但它对许多软件设计师产生了影响。通过其数百页的内容,这本书提供了大量关于数据建模和功能设计的最佳实践。它面向软件,但它所说的每一件事都可以在编写第一行代码之前提供帮助,并且它是在你甚至开始考虑通过 IT 解决方案自动化业务功能之前,理解你的业务功能的宝贵建议。

话虽如此,我们在这里的目标不是过多地谈论书籍,或者展开完整的方法。如果你想充分利用这部开创性作品的全部优势,你必须自己阅读它,或者观看埃里克·埃文斯在youtu.be/lE6Hxz4yomA上的出色演讲,专家在那里解释了该方法的核心要素,如下所示:

  • 领域专家和软件专家的创造性协作

  • 探索与实验

  • 形成和重塑通用语言的模型

  • 明确的上下文边界

  • 专注于核心领域

我们现在要展示的是,如何将书中的一些工具用于帮助我们的关键格式设计,以实现良好的业务/IT 对齐。回到我们的样本公司,从一般的角度来看,我们正在做什么?有人可能会说,我们处于名为 书版 的业务领域。我们需要一个用于创作的子领域和一个用于销售的子领域。这两个可以被认为是核心领域,因为这是我们样本公司的主营业务:监督书籍的编写和销售。还有一些辅助子领域,如人力资源或会计:这些领域虽然不直接涉及公司的核心增值工作,但对于其正确运作却是绝对必要的。

这里的“版”一词指的是一般性的文学,但编辑和销售人员对于书籍的词汇并不相同:前者谈论的是 作品,而后者谈论的是 产品。尽管如此,这仍然是一个相似的实体。此外,他们也不会使用相同的属性。编辑将非常关注章节数量、写作进度以及其他类似的书籍属性,因为对于他们来说,书籍大多是一个正在进行中的作品(当他们去销售时,他们的工作基本上就完成了)。另一方面,销售人员将检查诸如书籍价格和可能甚至重量以计算运输费用等属性。再次强调,尽管如此,对于这两个角色都有兴趣的属性:页数、书籍的 ISBN、出版日期等等。

为了解决命名中的这些明显悖论以及在属性分离管理中可能遇到的潜在困难,DDD 提出了两个概念。

第一个概念是通用语言。DDD 认识到,对于同一功能实体,在不同的上下文中可以使用不同的名称,因此可以解释地方行话,同时保持信息系统所有参与者之间共享的唯一名称。在我们的例子中,这可能是“书籍”,这是一个足够重要且广泛接受的名称,可以用来指代编辑所说的“作品”和销售人员所说的“产品”。为了完全清楚,DDD 不推荐为每个概念找到一个单一的表达式并放弃所有其他表达,而是决定一个将由模型的所有参与者共享的表达式(因此有“通用”的称号)。地方行话不是被禁止的,因为它们通常在特定上下文内的快速沟通中很有用,但每次有轻微误解风险时,都应该使用标准表达式。

DDD 引入的第二个概念是边界上下文,它包含实体和业务规则的范围,在这个范围内,词汇是一致的。我们讨论了这种上下文,其中可以无障碍地使用替代词汇,前提是仅限于该上下文的参与者;这个上下文确实就是所谓的边界上下文。在完整的业务域中找到边界上下文很重要,因为它有助于定义交互在哪里,因此,在哪里对语言的完美清晰性最为重要。边界上下文可以与业务子域对齐,但这不是强制性的。正如我们将在下一章中看到的,还必须考虑实体生命周期的问题。

为了图形化地总结这一点,请参阅图 9.3中的我们版本域的边界上下文:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_09_03.jpg

图 9.3 – 版本 DDD 中的边界上下文

由于我们对样本信息系统的设计和开发遵循敏捷方法,我们现在不会进一步深入设计,只会进行这一非常初步的步骤。一旦我们将这一层次的知识应用于创建数据参照的第一版(见第十章),我们将在需要时进一步深入。实际上,试图涵盖整个业务域会花费太多时间和篇幅,而且不会增加对方法理解的帮助。在我们继续之前,让我们回顾一下数据参照的知识。数据参照是一种专门用于处理特定功能实体数据的服务,但也包括元数据、数据历史、授权、治理以及许多其他功能,作为仅处理持久性的传统数据库的补充。数据参照是良好主数据管理的基础。

应用于清洁的信息系统架构

现在我们已经清楚了我们业务模型的语义和领域分解,我们可以向前迈出一大步(尽管技术问题将在下一章介绍)并开始设想这些实体将如何被引入 IT 系统。到目前为止,我们所说的所有内容都可以应用于非基于软件的信息系统。从本节开始,我们将承认在设计中不再存在这样的信息系统,并且每个公司现在都是一个软件公司。既然我们在谈论实体,并且它们的关键格式被认为是设计的,下一步就是讨论信息系统将如何操作(因此存储)它们。

在指代应用中使用实体

关于存储和操作功能实体的第一个问题就是它们的分解。由于复杂的业务属性可能有数百个属性来界定它们,当然有必要至少对它们进行分类,如果可能的话,创建一个树状结构来分类它们。实体总是有一些基础属性,这些属性被信息系统中的每个人使用,其余的数据属性大多与一个子域相关,或者至少,每个子域都可以被选为数据质量的理想维护者。这种分解通常用于将数据指代表示为花(参见图 9.4),花的中心包含共享数据,而围绕中心的瓣片包含与子域相关的数据。由于瓣片总是附着在中心,图 9.4显示,没有识别实体的核心数据,外围数据就没有意义。它还指出,瓣片可以是独立的,而且即使缺少一些瓣片,某些用户仍然可能觉得花是有用的。最后,这个隐喻表明,如果花的中心被丢弃,瓣片也会随之而去。

将此方法应用于我们的book实体应该是相当明显的,根据我们之前所说的:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_09_04.jpg

图 9.4 – 指代的花的隐喻

虽然我们之前只讨论了两个主要的花瓣,但花周围可能还有一些其他的花瓣,例如关于书籍物理生产的那个,以及关于印刷单元存储的那个。再次强调,由于这本书是关于方法而不是关于为真正的图书编辑公司设计 IT 系统,我们不会深入这些细节;但当你真正设计一个实体花时,你绝对至少应该了解所有花瓣,即使你第一次分析中不能了解每个花瓣的所有细节。

管理实体的生命周期

此外,由于我们质疑存储的设计,因此包含时间是重要的,正如在第四章中解释的那样。一旦设计了一个实体,一个常见的错误就是认为我们需要存储和处理在此阶段出现的所有数据属性。然而,围绕实体的许多其他事物都会影响存储。时间当然是第一个,一个重要的实体通常需要在其整个生命周期中持久化所有状态,而不仅仅是最后已知的状态。在某些情况下,可能需要处理实体的版本和分支。实体的元数据(谁创建了它,它处于什么状态等)可能被视为历史的一个专用花瓣,但它通常是与实体相关联的完整数据集,对所有花瓣都可用,尽管它不是花朵的核心,因为并不总是需要这些元数据。

如果我们坚持时间,数据变化的可追溯性当然是显而易见的事情,但考虑时间不仅仅是每次修改时存储每个属性的更改:它还涉及到将实体的演变建模为业务知识的一个元素,并使其能够理解数据是如何变化的(例如,删除索引为 1 的数组元素并添加另一个实体),以及背后的功能原因是什么(例如,某人搬走并记录了他们的地址变更)。这就是所谓的实体生命周期。设计它比列出实体的属性更困难,因为它不是一个常规的设计活动,也因为引入时间的方式有很多,每一种都是相互补充的。例如,它可以用来思考实体在其生命周期中将经历的状态(创建、草案、有效等,直到达到存档状态)。然而,有时拥有一个更接近以重要实体为中心的业务流程的设计可能会更容易沟通。

图 9*.5* 展示了如何在“书籍”实体上引入时间标准,以及如果我们从书籍生命周期中的不同步骤开始,从版本域的角度来看(当然,不是从读者的角度,因为这会导致一个完全不同的信息系统)生命周期会是什么样子:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_09_05.jpg

图 9.5 – 书籍的生命周期

如您所见,图表的上半部分显示了在编辑公司中书籍在其生命周期内会发生什么。它总是从一个想法开始,即使这个阶段非常短暂,比如来自与潜在作者会议的想法。在这种情况下,流程将直接跳到第二阶段。在这里,阶段看起来相当线性,但这并不意味着它们必须如此。例如,当创建第二版时,写作和校对阶段将再次开始,但一般来说,这个图表有助于将实体视为不仅数据的总和,而且是一个信息系统中的活生生的对象。

实体随时间的变化,当然会影响其数据的许多方面,但也会影响适用于它的业务规则。在图表中,我只展示了这种影响的几个示例:

  • 与书籍相关联的标签,用于对其进行分类,最初会发生变化,但很快就会固定,之后不能再变化,因为如果一本书的主题在销售人员开始在其沙龙、社交媒体或向经销商谈论它之后变化太多,就会产生问题。

  • 在一本书的制作过程中,参与其中的角色当然会随着其生命周期而变化:市场营销将创造书籍的愿景,编辑将帮助作者创作,在内容经过审查和验证后,主要角色将变为销售人员,直到书籍达到存档阶段,此时编辑将再次对书籍进行一些工作。

  • 已经提供了一个业务规则的示例,尽管在信息系统的重要实体中总是有许多这样的规则。在这个图表中,我展示了业务规则是公开的,只要书籍没有被审阅者验证,这就是错误的,之后,在特定情况下(此处未展示),一旦公开,一本书就不能再回到私有状态,因为人们已经了解它了。

编辑会在book实体实例上更改的status属性的概念。但它也可能与一条业务规则绑定,该规则指出一旦以下任务完成,一本书就可以变为准备发布状态:

  1. 它的主要编辑或两位编辑已经对其投了票。

  2. 作者已经签署了他们的合同,特别是财务修订条款。

  3. 印刷公司已经批准了提供的文件。

业务规则也可以相互级联。例如,我们只能授权支付给作者,如果他们的银行详细信息已经验证少于三个月,这意味着验证拥有该账户的银行,这意味着反过来检查 SWIFT 号码是否正确,等等。

最后,所有这些都因某些业务规则可能在某个时刻稳定为数据而变得更加复杂。这出于性能原因,计算过程如此之长,以至于计算结果不总是最新的(这发生在读取值比重新计算其结果更频繁时)。也可能有一些功能上的原因,导致状态违反业务规则并最终固定,而没有返回的可能性(通常,当一个实体达到“存档”状态时就会发生这种情况:其内容随后从数据库中删除并放入存档,因此返回“活跃”状态是不可能的,因为数据现在只能由档案保管员访问)。在这种情况下,状态覆盖了业务规则本身(或者业务规则开始读取记录的状态,并在没有此状态覆盖的情况下继续计算)。

子域与时间的关系

这种生命周期概念同样重要,因为它有助于定义信息系统中重要的实体,以及子域。例如,一本书无疑是该领域的主要实体,因为我们已经展示过,它有一个生命周期。作者在信息系统中也有生命周期,因为他们在其中被创建时,他们的联系数据会发生变化,他们可能会写几本书,并在某个时间点,在一段不活跃时间后(在此期间,肯定存在一些监管业务规则,例如欧洲的 GDPR),将从数据库中删除。然而,标签并不是一个重要的实体,因为它们在书籍之外没有生命周期。当然,一个标签可能会消失,但这只会是这个类别中没有更多书籍的结果。决定作者地址绝对不是主要实体甚至更容易,因为它们永远不会存在于作者之外,并且它们将始终随着其父实体消失。

实体的定义本身可能会随着时间的推移而演变,这在敏捷方法中是完全自然的,因为我们知道事情会在时间中变得更加清晰,我们应该只为下一个版本中将要添加的变化做准备(同时了解足够的业务知识,以确保系统的兼容性和平稳演变)。子域的切割通常永远不会随时间演变。信息系统的拥有公司改变战略后,可能会出现额外的领域,但应该有一些非常重要的事情来证明这种低级变化是合理的。

现在我们已经看到了 DDD 的实际应用及其与正确结构化信息系统之间的关系,我们将更具体地讨论所有这些在 API 合同设计中的后果。

链接到 API 和服务

我们花了很多时间从实体的演变角度而不是数据角度来讨论实体,但这是故意的,因为我们通常花太多时间定义属性,而不是足够地思考整个业务实体,一个活生生的对象。现在这已经完成,让我们利用本章的最后部分回到我们在上一章详细说明的服务和 API 的概念。

在 API 中包含时间

将实体视为一个整体(包括其历史)思考的第一个后果之一是,相应 API 上的编写方法不应完全相同。作为读取方法,它们类似于以下内容:

  • GET on /api/entity: 这用于读取实体列表

  • GET on /api/entity/{id}: 这用于读取给定实体

然而,如果您想采取行动并能够访问历史记录,您应添加一些方法,如下所示:

  • GET on /api/entity/{id}?valuedate={date}: 这用于读取给定实体的特定日期状态

  • GET on /api/entity/{id}/history: 这用于读取给定实体的完整历史记录

应对 API 的编写部分进行修改,但有一个方法保持不变:

  • POST on /api/entity: 这始终是关于创建实体实例。(不要忘记遵循标准并发送201 HTTP 状态码,以及包含刚创建的资源 URL 的Location响应头。)

然而,当你从完整生命周期角度思考时,API 的传统调用是有限的:

  • PUT on /api/entity/{id}: 这不应被允许,因为它会破坏最终一致性以及避免锁的能力,如前所述。

  • DELETE on /api/entity/{id}: 这也应进行调整,不是在其表述上,而是在其工作方式上。大多数时候,由于资源并没有真正被删除,而是通过达到存档禁用状态使其不可用,因此可以使用等效的修改调用以相同的方式进行,并且更加明确。

此外,应使用一个现有但不太为人知的动词来对实体的状态进行操作:

  • PATCH on /api/entity/{id}: 这与请求体内容一起,遵循RFC 6902(JSON Patch),应用于以渐进的、最终一致的和无锁的方式(乐观锁和悲观锁已在第五章中解释)写入数据

  • PATCH on /api/entity/{id}?valuedate={date}: 在某些情况下也可以允许,当实体的历史记录并不严格遵循 API 服务器的订单流程时,应考虑价值日期。

我们将在下一章,主数据管理中回到这些定义,并展示它们的实现。

将 API 与子域对齐及其后果

如果你使用严格的服务架构,所有主要实体以及因此所有业务子域都应该有它们自己的流程。然而,由于我们说它们应该有自己的 API(次要实体将位于它们相关的域的主要实体之下;例如,地址将在/api/authors/{id}/addresses中),这相当于一个 API 始终应该有自己的流程。此外,如果你遵循一个流程在一个 Docker 容器中的规则,你将有一个一个 API 对应一个 Docker 服务的等效性(考虑到可伸缩性,因为服务是一组相同镜像的 Docker 容器)。

注意

Docker 是软件安装中容器化原则最知名的实现。这项技术允许部署自包含的黑盒软件实例,这些实例包含所需的所有依赖项,并保持与其他实例的隔离,同时不需要像虚拟化这样的重型机制。

如果你认为所有调用都应该通过 API 进行,因为它们是唯一业务规则管理和给定 API 的真相来源,那么这意味着除了 API 展示之外,没有人会访问应用层。在这种情况下,为什么还要费力去分离这两个层呢?在下一章中,我们将遵循这个简单的规则,并将 API 业务代码直接实现在 ASP.NET 控制器中。如果你想知道验证以及它们如何尽快完成,那么反序列化将处理所有这些中的大部分,而实现代码中的先决条件将完成剩余的部分。

当然,我们将为所有依赖项,如持久性和日志记录,保持分离。然而,在业务行为方面,所有内容都将由 API 代码本身处理,仅在一个大块中。这可能会给人一种不明显的印象,考虑到责任分离的明确原则,但这是在本书及其相关代码中故意这样做的。这并不意味着像timdeschryver.dev/blog/treat-your-net-minimal-api-endpoint-as-the-application-layer中解释的那样,将系统分层是没用的,但仅仅是因为一个健全且不断发展的信息系统的前几个版本完全可以从一个非常简单的受限 API 实现开始,将其留给未来的版本去发展到扩展的 API 内容和更复杂的实现。

API 可测试性

关于 API 及其与实体的对齐的最后一件事:没有比一个漂亮的 Postman 收集更好的方式来手动测试 API 的内容,然后使用这些请求作为一组自动化测试的基础。当然,还有其他用于特定测试目的的工具,但根据我的个人经验,我还没有找到像 Postman 一样在 API 发现和测试方面如此通用的工具。

注意

Postman 是 API 测试的参考工具。一个收集是一组可以手动测试或自动、顺序测试的 HTTP 调用。

如果你能够收集你的客户、内部团队和合作伙伴(无论是外部还是公众)如何具体使用你的 API,并将他们的代码集成到你的 质量保证 (QA) Postman 收集中,那么这绝对是确保非回归和向后兼容的最佳方式。当然,它不能取代单元测试和集成测试,但前者是开发工具,后者是 QA 工具。所有介于两者之间的内容都将完美地保持在 API 级别,这使得 API 成为你的交互级别,并且与你的模型测试接口统一,因为它与 API 对齐。无论你的互操作性水平如何,回归测试最好在 API 级别进行。

如果你完全遵循前面的原则,最终你会得到以下内容的完美对齐:

  • 一个业务子域

  • 一个主要实体

  • 一个 API 合同(以 OpenAPI 格式)

  • 一个用于实现此 API 的代码的 Git 仓库

  • 一个用于交付此代码的过程

  • 一个用于部署的 Docker 镜像

  • 一个用于协调执行 API 调用的 orchestrator 服务

  • 一个用于 API 测试的 Postman 收集

摘要

我们终于到达了这一点,我们将开始进入代码!前几章为理解创建一个具有进化能力、功能丰富的信息系统的多数约束奠定了基础。在本章中,我们看到了我们应该如何详细说明实体的数据及其生命周期,以创建一个干净且面向未来的架构。

DDD 和之前展示的基于语义的方法将有望帮助你找到在信息系统中结构化重要实体的最佳方式。正确的模式使得通过 API 暴露这些实体变得更加容易,并且功能价值更高。这种方法还允许系统以最平滑的方式进化,因为如果设计正确,技术进化与功能进化应该分离。这样,不仅信息系统在其当前形式上更好,而且它也将更容易进化。

在下一章中,我们将看到我们设计的功能实体将如何在技术层中得到实现。我们不会立即深入代码细节,而是从数据如何在逻辑服务器中组织开始,我们将讨论我们之前提到的实体生命周期如何在将要部署的软件应用中得到实现,以及为什么主数据管理和数据治理对于确保我们在这章中设计的这些功能正确的关键格式能够高效利用是重要的。

第十章:主数据管理

在上一章中,我们向您展示了一种设计信息实体的方法,使它们没有任何技术耦合,努力使包含这些实体的信息系统在业务变化时能够自由发展。如果数据模型是业务代表的纯粹反映,那么它使得跟踪业务变化(变化是唯一不变的因素)变得容易得多,因为我们不会遇到一些技术约束,这会强迫我们妥协设计质量,从而影响整个系统的性能。

在本章中,我们将开始讨论将数据模型具体化(如果可以这样说关于软件,它主要是虚拟的)的实施。只有在第十六章到第十九章,我们才会编写我们称之为本书其余部分“数据参照”的代码。现在,我们将开始一些实际的软件架构,以欢迎数据模型,持久化相关的实体,等等。数据参照有很多责任,而在信息系统中处理这些基本资源的学科被称为主数据管理MDM)。乍一看,这些责任可能看起来像是你会信任数据库的,或者甚至可以在基于资源的 API 中找到的。但本章应该让你相信,模型中还有很多其他事情,这证明了使用“数据参照”这样的新词的合理性。

除了定义数据参照的功能外,MDM 还涉及选择正确的架构,定义整个信息系统中的数据流,甚至实施数据治理,这包括确定谁对数据中的哪些行动负责,以保持系统处于良好状态。拥有干净且可用的主数据可能是系统质量的最重要因素。没有干净的数据就无法进行报告,而且大多数业务流程都依赖于数据参照的可用性。此外,一些监管原因,如会计或合规性问题,要求高质量的数据。

在展示了你可能在信息系统中遇到(或创建)的不同类型的数据参照之后,我们将以对数据可能存在的问题、使用模式、数据随时间可能的演变以及一些其他一般性主题的概述来结束本章,这些主题希望为你提供有关 MDM 架构的最新知识。

数据相关的责任

数据参照作为给定领域数据实体的唯一真实点的概念已经在全局范围内解释过了,但我们还没有正式描述其中包含的功能责任。本节将解释参照的每个主要责任和功能。查看以下子节中解释的责任,你可能会问为什么我们谈论数据参照而不是简单地使用更为人熟知的数据库表达方式,但我们在本章的第二部分将会看到,参照远不止于此。

持久性

持久性是我们谈论数据管理时首先想到的责任。毕竟,当我们信任信息系统的数据时,我们首先的要求是计算机一旦了解这些数据,就不会忘记它们。这个要求至关重要,因为即使是电力故障也不应该对其产生影响。这就是为什么发明了数据库,工程师们也走过了如此长的路来确保数据在内存和硬盘之间安全传输,双向都是如此。

持久性可能经常被简化为CRUD(代表创建、读取、更新、删除——数据上的四个主要操作),但与数据参照所包含的功能相比,这个概念过于局限。尽管它对于信息系统中低重要性数据的多数标准用途来说已经足够,但当我们谈论信息系统中的主要数据时,必须考虑持久性的其他方面。第一个方面在第四章中已经详细讨论过——即时间。当我们将时间纳入 MDM 方程时,存储所谓的“当前”数据状态(这通常只是对应业务现实的最后已知或最佳已知状态)突然变得复杂得多,这意味着至少要存储数据随时间变化的不同状态,并标明时间以追踪这些连续状态的历史。

如我们在第五章中所述,一个好的 MDM(Master Data Management)系统是一个“无所不知”的系统,它应该存储实际修改数据的命令,而不是状态,以便我们能够追溯这样一个实体的状态是如何演变的。这意味着数据库中将要写入的不是一个带有日期的状态,而是一个理想的“delta”命令,它导致从一个状态到另一个状态的变化——例如,修改我们样本信息系统中作者第一个地址的邮编。这样,我们不仅可以在业务实体的生命周期中的任何时间重新构建其实体的状态,而且还可以避免乐观/悲观锁、事务、数据协调、补偿等复杂性。

元数据也是简单 CRUD 方法的一个重要补充。实际上,在主数据的操作中,能够检索和操作与数据变化相关联的信息非常重要——例如,其作者、命令来源的机器的 IP 地址、引起这种变化的交互的标识符、交互的实际日期,也许还有如果作者已经规定的话,一个价值日期,等等。这允许进行可追溯性,这对于信息系统中的主要业务实体变得越来越重要。它还提供了对数据的强大洞察力。能够分析数据的历史将帮助您打击欺诈(例如,通过检查哪个实体经常更改其银行坐标,或者限制在一定时期内一个特定公司的代表可以更改的数量)。它还可以帮助解决一些越来越常见的监管问题,我们将在稍后讨论数据删除时看到这一点。

当谈到持久性时,我们通常会想到一个特定的实体(而且我,至少,之前只给出了这种原子操作的例子),但操纵大量数据的能力也是数据引用的一个重要责任。在大多数情况下,这转化为能够批量执行操作,但后果也涉及性能管理和处理引用范围事务的能力(这与以业务实体为中心的转换非常不同,数据引用应该帮助消除这种转换)。

标识符的问题

一旦创建了一个业务实体单元,如何识别它的问题就随之而来,因为持久性是指信息系统能够检索它所提供的数据的能力,这自然意味着必须存在一种确定性的方式来指向这个特定的实体。至少,应该存在一个系统级的标识符来做到这一点。它可以采取很多形式,但为了适用性,我们将考虑以下作为 URI,例如,https://demoeditor.com/authors/202312-007urn:com:demoeditor:library:books:978-2409002205。这种标识符应该被任何参与信息系统的模块全球理解。它有点像领域驱动设计中的通用语言,但允许指向一个特定的实体而不是定义业务概念。

当然,可能存在本地标识符。例如,由 urn:com:demo editor:library📚978-2409002205 指示的书籍可以存储在 MongoDB 数据库中,其技术 ObjectID 将是 22b2e840-27ed-4315-bb33-dff8e95f1709。这种标识符属于它所属的模块是本地的。因此,通常不是一个好主意让其他模块知道它,因为实现的变化可能会改变链接,并使他们无法检索他们指向的实体。

实体还可以有业务标识符,这些标识符本身不是本地的,但也不保证在信息系统中的任何地方都能被理解。通常通过urn:com:demoeditor:library:books:978-2409002205识别的书籍,只能通过其 13 位 ISBN 978-2409002205检索;实际上,它是唯一系统标识符的可变部分。然而,还存在其他标识符。例如,同一本书也可以通过其 10 位 ISBN 检索,即240900220X。业务标识符也可以在信息系统中为特定用途创建。在我们的样本版公司中,可以想象给一本书应用一个序列号,以便在印刷站跟踪,在那里使用批量,单个整数可能比完整的 ISBN 更容易处理,而不会引起任何混淆,因为车间只印刷样本编辑的书籍。

在信息系统中,尤其是在具有遗留软件应用的信息系统中,经常会遇到额外的技术标识符。确实,这些系统通常坚持使用自己的标识符。例如,DemoEditor的会计系统可能通过其本地标识符BK4648来识别urn:com:demoeditor: library📚978-2409002205的书籍。ERP 系统如果将这本书作为第 786 个产品录入,可能有一个技术标识符00000786`。等等。当然,理想的情况是所有软件应用都是现代的,并且能够处理外部提供的、符合 HTTP 标准的 URN。但这种情况很少见,甚至现代网络应用似乎也忘记了与其他应用互操作意味着无差别地使用它们提供的 URL。

为了提供优质的服务并考虑到信息系统这一现实,数据参照应该具备存储系统中参与的其他软件模块的业务标识符的能力。至少,这应该是一个与实体关联的标识符字典,每个值都由一个键指向,该键全局标识系统中的模块。例如,urn:com:demoeditor:accounting 可以是指向 BK4648 的键,而 urn:com:demoeditor:erp 可以指向 00000786。在定义键时,人们自然倾向于使用实现功能的特定软件的名称,这并不会太重要,因为标识符确实只针对这个软件。但仍然是一个好主意保持通用性,以备不时之需。仅举一个例子,在法国两个行政区域的合并中,这种区分证明非常有用。两个现有的财务管理软件应用在合并后竞争拥有一个独特的市场。结果,其中一个软件应用比另一个更可定制,并且可以处理外部标识符,这是将其保留为新唯一财务管理应用的一部分决策。然而,由于被废弃的软件使用的标识符以供应商标记为前缀,而保留的软件的键不是通用的而是使用了其名称,因此出现了诸如 urn:fr:region:VENDOR1=VENDOR2-KEY 这样一些奇怪的标识符关联。由于这两个品牌在法国是众所周知且相互竞争的公司,两个行政区域的合并导致了大量的团队调整和变革管理,这种额外的混淆很快变成了一个烦恼,以至于人们甚至无法确定他们应该使用哪个软件来操作财务数据。最终,切换到通用的键如 urn:fr:region:FINANCE 真的很有帮助,即使这听起来像是一个小小的技术举措。

我将以一个非常特殊的情况来结束对标识符的审查,这个情况是业务标识符的变化。标识符本质上是不变的,因为它们应该是一种确定性的方式来指向信息系统中的实体。一个全球标识符变化的文档案例是当社会保障号被指定给一个尚未出生的人时,通常是因为需要对胎儿进行手术。由于法国社会保障号的第一位数字使用 ISO 性别平等标准来指定所有者的性别,所以可能会发生这种情况:而不是使用 1(男性)或 2(女性),社会保障号以 0(未知)开头。然后,在个人出生后,标识符会改变为新标识符,因为第一个数字那时是已知的(或者在某些其他条件下可能是未知的——在这种情况下,规范指定该数字应为 9,以表示性别不确定)。这确实是一个非常特殊的情况,它引发了全局、系统范围内标识符的变化。然而,系统的架构必须能够处理任何现有的业务案例(这并不意味着不能对这些案例进行一些手动调整)才能被认为是“对齐的”。

单个实体读取责任

如果存储在某个地方的数据无法在之后被检索出来以供后续使用,那么坚持实际上什么也不是。这就是为什么读取数据是我们将要研究的数据引用的第二个责任。本节详细介绍了不同类型的读取操作,与持久化数据相比,它们在形式上实际上非常多样。

我们自然而然会想到的第一个读取行为是检索一个唯一的实体,直接使用其标识符。在 API 术语中,这可以总结为在创建实体时,在响应的Location头下发送的 URL 上调用一个GET操作。或者至少,这会发送数据的最新已知状态,因为可以通过添加参数来指定应该检索哪个时间版本的数据。这通常引发一个问题,即如何获取数据的状态,因为我们说过我们会存储变化,而不是状态。那里的响应可以是简单的或复杂的,取决于我们深入到什么程度。如果我们激进地应用唐纳德·克努特(Donald Knuth)提出的“过早优化是万恶之源”原则,那么只需指定可以通过应用它们到前一个状态来从变化中推断状态,并考虑这种递归使用数据的初始状态,即由唯一标识符指定的属性集合。

我非常清楚,大多数技术导向的人(因此至少 99%的正在阅读这本书的读者)总是会进一步思考,并思考如果每个GET操作都需要对实体进行数百次补丁迭代以找到其生命周期中某个点的状态,数据引用将不得不处理的巨大性能问题。我们至少会做的是缓存计算出的状态来改进这一点。但当你这么想的时候,绝大多数的读取操作都是请求实体的最佳已知状态,也就是最新的已知状态。因此,为了在保持良好性能的同时提高存储,缓存实体的最后已知状态是正确的选择。

当然,也有一些例外,正如这本书多次解释的那样,必须考虑业务合理的例外情况——不仅因为这是对齐的目标,而且主要是因为这些例外通常是对数据模型的大挑战,如果它能够在保持简单的同时容纳它们,这意味着这种设计是成熟的,并且有更大的正确性和稳定性机会。一个这样的例外可能是当数据经常使用日期参数值进行读取时。在这种情况下,提高性能可能意味着存储所有计算出的状态,但这会使用大量的存储,并且浪费了其中大部分,因为并非所有状态都会及时被调用。一个良好的折衷方案可能是只存储每 20、50 或 100 次更改计算出的状态。这样,我们就可以始终从一个现有的状态开始,并快速计算出指定的状态,因为我们只需要应用几个有限的补丁到数据上。根据业务约束,一些比其他更常用的状态可以作为缓存中保留的里程碑。例如,在金融应用中,通常很有趣的是保留财政年度变更前后的值。

另一个必须考虑的细节是实体生命周期中插入修改的可选可能性。我理解这听起来可能很奇怪,“重写历史”并插入可能对后续操作产生影响的更改,但有些情况下这是有意义的。例如,我见过这种情况在会计系统中发生,当出现错误并且重新应用计算规则以找到正确结果时,会在初始错误出现时插入纠正操作。再次强调,这是一个罕见的情况,它应该由严格的授权规则来限制,但为了全面性,这种情况必须被提及。

其他类型的读取责任

有时候,业务实体的全局唯一标识符是未知的或已被遗忘(这意味着未存储在其原始参照之外),在这种情况下,必须使用搜索与给定标准对应的实体的责任。这种责任通常被称为查询数据。根据请求中指定的标准,操作将返回一组结果,这可能是一个空集或包含对应数据的集合。可能会有查询属性的情况,使得结果总是包含零个或一个实体——例如,因为使用的约束是一个唯一的业务标识符。但也可以有结果特别众多的情况,这时一个额外的责任称为分页将非常有用,以减少带宽消耗。

分页可以是主动的(客户端指定他们想要的数据页)也可以是被动的(服务器限制数据量并提供客户端请求下一页数据的方式)。实现第一种方法的一个标准方式是使用$skip$top属性,如$filter属性中指定的,该属性用于指定减少查询结果的约束,这在讨论数据检索性能时已被提及。这本书不是解释这个标准丰富性的地方,这个标准遗憾的是没有被像应该的那样频繁使用。大多数 API 实现者实际上选择使用他们自己的属性名,而没有意识到他们正在重新创建(例如,分页偏移量)已经被多次完成并且完全规范化的功能。对标准的缺乏兴趣,以及许多开发者遭受的“不是这里发明的”综合症,正在将我们的整个行业拖回。但关于这一点就足够抱怨了:已经有一个完整的章节专门讨论规范和标准的重要性,所以我们将通过向您介绍 OData 标准来结束这个话题,或者在这个案例中,GraphQL 语法也是如此,因为这两种方法可以被视为竞争(尽管它们是相互补充的,并且一个优秀的 API 会公开这两种协议)。

另一种阅读责任是报告:这有时可以直接由数据参照实现,但这相当罕见,因为报告通常是通过跨多个业务领域的数据来完成的。即使只有少数报告需求需要这种外部、共享责任实现,那么最好是将所有数据用于报告给这个实体。根据你使用的科技,这可能是一个数据仓库、一个 OLAP 立方体、一个数据湖或任何其他应用。再次强调,实现方式并不重要:只要你在接口上保持清晰,你就可以随时更改它们,对系统的影响有限。

在报告的情况下,可以使用不同的基于时间的方法来请求这些接口:

  • 同步、按需调用始终是可能的,但通常由于性能原因不使用,至少在复杂的报告中是这样(这是“拉”模式)。实际上,如果报告系统需要等待所有源响应,然后在其一侧仅计算聚合,那么结果当然是尽可能新鲜的,但可能需要几分钟才能到来,这通常用户无法接受。

  • 异步、定期的数据读取是最常用的模式。在这里,数据以一定的频率(每天一次或更频繁,有时每小时一次)收集,通常由 ETL 完成,并发送到数据仓库系统,在那里进行聚合和准备报告。这样,报告可以更快地发送给用户(有时,它们甚至可以在数据检索时直接生成并可供使用)。然而,缺点是数据可能不是最新鲜的,将光标移动到更快的数据发送会增加资源消耗。可以进行优化——例如,通过仅传输新或更新的数据来减少传输——但这只能在一定程度上提高更新整个数据仓库所需的最短时间。这种方法最大的技术缺点是,即使源数据没有变化,大部分计算也会重新进行,这是一种资源浪费。

  • 在“推送”方法中进一步深入,可以使用 webhooks 将数据刷新注册到源数据变更的事件中。这样,只有在数据发生变化时才会重新进行计算,并且时间尽可能接近数据变化的事件,这意味着大部分时间报告都非常新鲜。处理大量事件是一个挑战,但将变化分组为最小包(或带有最大新鲜时间约束)可以帮助。

  • 一种非常现代但技术要求很高的方法是,通过使用包含数据变化的队列消息的系统,以及一个用于在每个消息上应用细粒度计算的专用架构,来混合这些“推送”和“按需计算”策略。这种大数据方法的具体实现包括 Kafka 架构或 Apache Spark 集群。这里的目的是不详细说明这些方法,只是解释它们将收集源数据中的所有事件,然后智能地计算聚合数据中的后果(智能之处在于它们知道后果,只计算所需的,并且可以在集群的许多机器上平衡这些计算,并在最后分组结果)。它们甚至可以进一步到在聚合数据上生成最终报告,并将其提供给最终用户,实现完整的“推送”范式。

这四种方法在以下图中以象征性的方式表示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.1_B21293.jpg

图 10.1 – 报告模式

为了详尽地说明这些额外的阅读责任,索引是另一个用于加速数据(以及一些简单的聚合)读取的功能。它并不像大数据和先前的报告方法那样深入数据转换,但它已经可以准备一些聚合(如总和、局部连接等)并通过简单的协议作为原始数据提供。SOLR 或 Elasticsearch 等索引引擎通常用于在数据检索速度上伴随数据参照。在这种情况下,数据参照本身专注于数据一致性和验证规则,然后处理索引系统中的参考数据,以便在快速读取操作中使其可用。

删除数据的复杂艺术

如果存储的是 delta 而不是状态,那么在资源上的 POSTPUTPATCH 操作之间并没有太大的区别,因为它们都转化为实体状态的改变,资源创建的特殊情况是从完全空的状态的改变。但是,就 DELETE 操作而言,我们处于不同的情境。确实,我们可以盲目地应用相同的原理,并认为 DELETE 移除了实体的所有属性并将其恢复到初始状态,但这并不完全准确,因为实体仍然保留了一个标识符(否则将无法删除它)。这意味着它并不处于不存在时的状态,而且无法回到这种情况。

处理这种情况的最佳方式通常是使用日期的一个特定属性来表示它已不再活跃。当使用 status 属性来保持实体生命周期中计算出的位置时,此属性可以使用如 archived 这样的值来实现类似操作。这就是数据参照能够存储数据已被删除的事实,而实际上并未删除数据(这与之前关于数据参照及其对历史持久性的责任的说法不符)。当然,这会在参照中增加一些复杂性,因为它必须在每个允许的操作中考虑这一点。例如,读取某些不活跃的数据应该表现得就像数据不存在一样(在 API 访问的情况下,结果是 404),除非在特殊情况中,访问参照的用户具有 archive 角色,可以读取已删除的数据。随后自然会提出其他问题,例如重新激活数据并继续其生命周期的可能性(提示:这通常是一个坏主意,因为许多业务规则并未考虑到这种非常特殊的情况)。

但让我们在这里停止这种离题,回到数据保留的最初想法,即使在发出删除命令之后。这一功能背后的主要原因是监管性的,例如可追溯性,但也禁止出于其他目的(如网络攻击后的取证)删除数据。一个有趣的事实是,一些法规还规定了数据确实应该被删除(而不是仅仅被停用)的确切时间。例如,欧洲的 GDPR 规定,个人数据不应保留超过某些法律定义的期限,具体取决于它们所关联的过程。在为营销目的收集的个人数据(当然,是在用户同意的情况下)的情况下,延迟通常是 1 年。在此之后,如果没有更新存储同意,则应从收集这些数据的信息系统中删除数据。这意味着实际上在所有可能的地方(包括备份)删除数据。

与专用链接的关系

总是如此,魔鬼藏在细节中,处理数据时链接可能会成为一个问题。想象一下,我们使用一个链接在书籍和作者实体之间。这种 RFC 链接的最简单表达如下:

{
    „isbn13": „978-2409002205",
    „title": [
        {
            „lang": „fr-FR",
            «value»: «Open Data - Consommation, traitement, analyse et visualisation de la donnée publique»
        }
    ],
    "additionalIdentifiers": [
        {
            "key": "urn:com:demoeditor:accounting",
            "value": "BK4648"
        }
    ],
    "links": [
        {
            "rel": "self",
            "href": "https://demoeditor.com/library/books/978-2409002205"
        },
        {
            "rel": "author",
            "href": "https://demoeditor.com/authors/202312-007",
            "title": "JP Gouigoux"
        }
    ]
}

链接通常是从专用链接继承而来的——在我们的案例中,是一个包含其模式中额外重要信息的专用作者链接,例如,为了可读性目的,仅提取已更改的 JSON 部分:

{
    "rel": "author",
    "href": "https://demoeditor.com/authors/202312-007",
    "title": "JP Gouigoux",
    "authorMainContactPhone": "+33 787 787 787"
}

当你知道链接中包含的信息是经常在操作链接时使用的信息时,在链接中包含额外的信息是有用的,因为它避免了额外的往返到其他 API 以查找此信息的额外步骤。当然,应该有一个适当的平衡,在这里包含电话号码是可疑的,因为它可以被认为是易变数据,不会经常改变,但在编辑数据库的大量作者中的某些特定场合会改变。结果是,所有链接都应该(在这种情况下)更新,这需要相当大的工作量。当你知道这是一些不会改变的数据(例如,作者的名字不会经常改变)或出于监管原因不应更改的数据(例如,批准的版本不应修改,即使有进一步的版本出现)时,就没有这样的问题。

在处理链接时应注意的第一个问题是。第二个问题更为微妙:由于title属性(这不是通过继承添加的扩展属性,而是存在于标准 RFC 链接定义中)已被用来存储作者的通用名称,正如 RFC 中该属性的定义所预期的那样,删除一个作者将导致他们的名字仍然通过这些链接存在于书籍的数据参照中。这可能对存档原因很有趣(即使我们不再处理这位作者,例如,即使他们已经去世,书籍仍然以他们的名字命名)。然而,在某些其他监管环境中,这可能是一个棘手的问题:如果我们回到欧洲 GDPR“被遗忘权”的例子,这意味着当作者从数据库中删除时,我们还应该检查他们所写的所有书籍,并将title内容替换为类似N/A (GDPR)的东西。这就是在特定功能情况下DELETE操作可以如何工作!

所称的次要功能

尽管我们可能认为我们已经覆盖了数据参照的所有责任,因为我们已经通过了 CRUD 缩略词的四字母,但一个好的应用程序的范围要大得多。为了彻底,我们应该讨论所有通常被称为“次要”的功能,尽管它们是关键的,而且在某些情况下,与数据本身的持久性一样重要。

这些附加功能中的第一个是安全性。关于这一点的重要性不应再有疑问,但如果需要说服任何人,让我们强调这样一个事实:在安全分类中常用的四个标准都是关于数据的:

  • 可用性:数据应可供授权人员使用,这意味着必须处理服务拒绝(以及其他情况)。尽管不可用数据是防止泄露或未经授权访问的好方法,但它仍然是首要标准,因为整个想法是以稳固的方式提供服务。可用性还意味着简单的失误不应该导致整个系统离线。

  • 诚信:数据不应被任何人篡改,其正确性应得到保证——结果是,所有支撑服务的功能都必须得到保护(数据库、网络、源代码等)。

  • 机密性:这是第一个标准的对应项,因为应禁止非授权请求者访问。它是授权管理系统的基础(关于这一点将在下一章中详细介绍)。

  • 可追溯性:这个标准是一个较新的标准,但随着对 IT 系统监管的加强,它变得越来越重要;它规定数据的修改和使用应该存储在一个无法篡改的日志中,以便能够回溯过去发生的事情。在攻击发生后,可追溯性最为重要,可以用来了解漏洞在哪里以及攻击者做了什么。

性能健壮性也是所谓的次要特性,在 MDM 中具有很高的重要性。它们与第一个标准(可用性)密切相关。确实,软件的健壮性是其能够以极大的信心及时回答请求的能力的基础,而性能是与数据的可用性相关联的质量。毕竟,如果某人在 5 分钟后收到了对数据请求的响应,他们不会认为该服务是可用的,尽管数据确实在某一点到达了……。数据的快速可用性往往推动了将现有的“手动”信息系统迁移到面向软件的方法。

处理这些特性是许多书籍的主题,所以我们现在就留在这里,因为这些确实是期望数据参照承担的责任。

元数据和特殊类型的数据

最后,数据参照不仅应该处理数据,还应该处理元数据。元数据是围绕数据实体本身的所有信息,有助于对这些实体的良好理解。这为数据提供了一些额外的丰富性,但请注意,元数据应该与数据本身有不同的生命周期。例如,存储关于数据历史的信息不是元数据,尽管它可以符合前面给出的元数据定义。正如现在多次被揭露的那样,数据参照跟踪其托管实体的每一个变化。因此,关于谁在何时更改了什么的信息是数据,而不是完整和正确数据参照的元数据。同样,更改日期、修改指标或读取频率可以直接从数据参照的操作序列中推导出来,因此它们也不是元数据。

元数据的一个好例子是与数值数据相关的单位。在实体的命名属性中有一个数字通常是不够的。当然,属性可以有一个描述其内容以及单位的名称(例如populationInMillionslengthInMillimetersnbDaysBackupRotation),但这并不使操作值变得更容易,而且,此外,这会使名称更长,当单位听起来很明显时可能会有些繁琐。在引用模式的某个地方有元数据声明说,这个实体的这个属性使用这个单位,这是一种更好的方式来传达数据的处理方式,并且还可以帮助在某些现代数据库引擎中直接计算不同单位尺度上的属性之间的公式,甚至当公式在单位定义方面不安全时提供一些警告,例如将米和秒相加。这些新服务器通常使用一个标准的单位定义,包括之前看到的kN单位与 M1K1S-2 相关联,但乘以 10³,名称为kiloNewton

地理属性是数据库中通常数据添加元数据的另一个好例子。一般来说,经度和纬度在lonlat属性中以双精度数字表示,但这并没有考虑到世界地图投影(这可能会在数字上产生一些差异)并且不会阻止像将两个数字相加这样的愚蠢计算。随着数据库或地理服务器能够理解添加到坐标数据中的元数据,现在可以计算距离,将坐标从一个投影系统转换到另一个投影系统,等等。

元数据是长久以来被遗忘的数据的表亲。除了 CMIS,即电子文档管理系统标准,其中它们享有第一公民权(支持在模式中实现的元数据组,这些模式可以应用于文档,在搜索时使用,有时甚至可以独立于支持的文档进行版本控制)之外,没有多少标准将它们正式化。这一演变完全取决于对以专业和整洁的方式完成工作感兴趣的工程师。只要在软件编程和信息系统的结构化中使用“快速且脏”的技巧,元数据将继续被忽视。当人们——希望是在阅读这本书以及一些其他在相同质量和长期方法上提供建议的书之后——决定耦合的负担太高,他们必须通过现代化他们的信息系统来解决这个问题时,元数据的使用应该自然增加,使其及时成为像其他任何实践一样标准和常见。

现在我们已经知道了数据引用应该如何定义,我们将深入探讨这是如何由软件系统提供的。

不同类型的数据引用应用

在本节中,我们不会讨论技术方面(这是下一节,即一些架构选择的作用),而是关于如何构建数据持久性的架构策略。

在上一章中,我们引入了“花朵”的隐喻来展示数据如何在实体内部组织。我们将遵循这个想法来表示如何在管理此类实体实例的数据参照中实现持久性。在我们深入主要架构之前,请记住,选择的主要标准始终应该是功能性的,在数据的情况下,这意味着您系统中的生命周期将主要驱使您做出这个或那个架构选择。同时,请记住,数据管理的人员方面与技术方面一样重要;治理、指定负责人员以及关于哪个团队拥有哪些数据的好沟通对于您组织正确使用数据是至关重要的。

集中式架构

集中式(或“唯一”)参照是其中最简单的一种(如图 图 10.2 所示),是每个人首先想到的,并且当它能够应用时,在信息系统中解决了许多问题:它包括为给定类型的实体(当然包括历史、元数据等)的每一比特数据拥有一个单一的存储机制。这样,系统中所有工作的服务都知道,当需要读取或写入某些内容时,他们必须将请求地址到一个单一的资源库服务,因为整个“花朵”都在一个众所周知的地方。

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.2_B21293.jpg

图 10.2 – 集中式数据参照架构

这种方法的优点是它简化了信息系统中每个人的工作。当然,这构成了一个单点故障(SPOF),如果实施应用程序出现故障,所有需要此参照信息的应用程序都将受到影响。但这只是一个技术问题,有众多经过实战检验的解决方案,例如数据库的主动/主动同步、应用服务器的扩展、硬件的冗余等。到目前为止,你也应该已经相信,功能方面始终比技术方面更重要。作为技术人员,我们倾向于关注低发生频率的问题,如硬件故障或锁定事务,而如今信息系统中的巨大问题则是数据的重复、输入的清洁度差以及其他需要紧急解决的问题。SPOF 可能在人员组织方面更为重要:集中式数据参照可能意味着一个团队甚至一个人负责管理这一组数据,过多的集中化总是可能带来一些缺点(如未考虑反馈、变化的相对不透明等)。

克隆架构

解决这种 SPOF 限制的一种方法是在本地复制一些重要应用程序所需的数据。在这种情况下,一些应用程序将在它们自己的持久化系统中保留“部分花”,并且它们有权选择如何管理数据的新鲜度与中央参照之间的比较,而中央参照仍然是全球唯一的真实版本。

当数据最初散布在信息系统周围时,通过遵守集中式业务规则同时保持数据存储的原样,这可以成为清理数据的第一步。其优势在于,对于遗留应用程序,没有任何变化:它们仍然在本地消耗数据,因此所有读取功能都像以前一样工作。经过一些努力,写入命令甚至可以保留在软件中——例如,通过使用数据库触发器来实现数据返回到唯一参照。然而,大多数情况下,尤其是如果应用程序是可组合的并且具有创建实体的唯一图形界面,将参照性 GUI 插入到该应用程序中而不是遗留形式会更简单。

这种方法的主要困难在于一致性:由于系统中存在多个数据副本,可能会出现差异,因此尽可能减少它们的时间和影响是很重要的。如果应用程序在功能隔间中很好地分离,这可能会变得非常简单,但如果应用程序分解的方式不佳,那么可能需要实现分布式事务,这可能会相当复杂。在这种情况下,最终一致性将是你的朋友,但它可能并不适用于所有地方。

克隆架构最有效形式如下,其中数据克隆(仅部分花朵,因为通常只有部分花瓣是有用的)基于数据参照中的事件,并且数据修改 GUI 已被来自集中式数据管理应用程序的 GUI 替换:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.3_B21293.jpg

图 10.3 – 克隆数据参照,最有效形式

在这种形式中,有一个选项是为所有数据添加同步机制,在夜间补偿白天由于网络微故障或此类低频但仍存在的意外事件而可能被跳过的数据更改消息,如果不想为这个简单的流使用完整的 消息导向中间件MOM)。

当同步连接器使用异步、通常是基于时间的机制来保持克隆数据库与参照信息相似时,这是一种对第一种形式的替代方案。在这种情况下,最佳做法是调用数据参照 API,因为它们提供最佳的信息质量:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.4_B21293.jpg

图 10.4 – 克隆数据参照,使用异步替代方案

一种常见的替代方案(但我真的不推荐)是让 ETL 执行同步,如图 图 10.5* 所示。这在那些投入大量资金用于 ETL 以保持数据与系统同步并使用此工具做一切事情的公司中很常见。当存在 API(每个好的数据参照都应该有一个)时,最好不要直接将我们与数据耦合。遗憾的是,许多公司仍然有这种类型的流,开始自己的“意大利面”信息系统,所有责任和数据流都纠缠在一起,定义不明确(有关更多解释,请参阅 第一章)。

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.5_B21293.jpg

图 10.5 – 克隆数据参照,使用 ETL(不推荐)

如前所述,某些实现无法更改,必须依赖其遗留 GUI。在这种情况下,唯一可能的方法是依赖于数据库上的特定触发器来获取创建和修改命令,并将它们作为请求发送到 MDM 应用程序:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.6_B21293.jpg

图 10.6 – 克隆数据参照,保留原有 GUI

这种方法中的困难在于,当数据引用由于某些业务规则而发生变化时,因为这种变化无法发送回 GUI。确实,大多数应用在将更改提交给服务器后,会保持数据的状态。即使是那些很少会监听其后台办公室返回数据的罕见应用,困难在于,在这次读取之前,完整的往返过程不会完成,而“更新”的数据将只是本地数据库中的最新数据,而不是随后从 webhook 回调返回的最新数据。当陷入这种困境时,最好向用户解释这是在达到集中引用架构之前的一种暂时情况,他们可以在稍后刷新他们的 GUI 以看到更改的效果。更好的是,学习如何使用新的集中引用,这始终会提供最新信息,代价是使用两个图形界面而不是一个(当这些是可以在两个浏览器标签中打开的 Web 应用时,这并不是一个很高的代价)。

重要注意事项

第八章中,我们简要地讨论了企业集成模式。它们是我们之前讨论的理想砖块,用于构建同步连接器,尤其是在信息系统重组/数据引用结构化项目期间实施消息导向的中介MOM)解决方案时。

集成和分布式架构

这种引用类型包括从一个中心视角(通常是 API)暴露数据,这些数据实际上被放置到信息系统的不同部分。通常,花朵的核心和一些花瓣位于专门用于持久化的数据引用中。但对于其他花瓣,持久化可以留在与之关联的商业应用中,因为认为它们对这些花瓣的内容了解得更深入。在这种最协作的形式中,引用暴露了信息系统中每个角色的全部数据,并共享花瓣的所有权:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.7_B21293.jpg

图 10.7 – 集成引用架构

数据引用可以通过其 API 生成并暴露整个数据花朵,但这意味着它必须从它不拥有的业务应用中消费不同的花瓣(基于新鲜度、变化率和性能,保留这些花瓣的本地缓存是实施选择之一,但这并不改变数据的所有权)。为了以新鲜内容暴露整个花朵,数据引用需要访问自己的数据库,以及业务应用数据(或者,再次强调,它可能保留的本地缓存)。

此外,一些应用程序,如 图 10.7 中的 App2,可能除了它们拥有的花瓣之外不需要任何东西(请注意,当然,根据定义,每个人都拥有花朵的核心)。一些应用程序,如 App1,可能需要一些额外的花瓣,在这种情况下,它们必须调用数据引用 API 来获取这些数据。

图 10.7 中,又做出了一项区别,以表明数据引用可能使用业务应用 API 来获取数据(最佳情况)或者可能求助于直接访问业务应用的数据库,这会导致更多的耦合,但有时这是唯一可行的方式。右侧显示的替代方案是危险的,不应应用:在这种情况下,App3 没有被提及,但这不是主要问题。真正的问题是使用 ETL 向引用数据库提供数据永远不应该做,因为这绕过了数据引用中的业务和验证规则。没有任何应用程序应该直接接触引用数据库,而只能是引用应用程序本身。实际上,这条规则非常重要,当在本地部署时,隐藏、混淆、拒绝访问或使用任何其他可能的方式防止任何人直接访问引用数据库是一种良好的实践。当这是一个“正常”数据库时,其耦合和其它不良后果已经足够糟糕;在如此重要的数据库上这样做是问题的根源。

当数据引用公开了一个实体上所有可能的数据(完整的“花朵”)时,该架构也被称为“统一”。在某些情况下,某些数据可能只对拥有它的应用程序有用,而对其他人没有任何用处。在这种情况下,“统一”这个术语并不合适,因为某些数据——自愿地——不可用,并且引用应该被视为“分布式”的。这种情况可以简化如下:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.8_B21293.jpg

图 10.8 – 分布式引用架构

分布式引用架构的主要困难在于保持性能。当然,可以进行优化,例如我们提到的缓存机制或者在没有使用缓存时对不同的业务应用的调用并行化,但所有这些技术补充都伴随着一个不应被低估的成本,尤其是当我们知道这种情况是暂时的,目标是集中式架构时。常常发生的情况是,“暂时”的情况,本应更便宜,作为通向下一个架构的垫脚石,实际上却与直接实施目标架构的成本相当。大多数时候,决策来自对目标愿景的困难有很好的了解,但对中间步骤的困难则考虑不足,主要是因为这些不稳定的情况数量众多,因此不如最终架构那样得到良好的记录。

让我通过谈论数据的分页来给你一个例子,说明设置中间分布式系统可能有多困难。当使用 $top=10 查询属性调用数据引用 API 时,如果引用是分布式的并且从两个业务应用中整合数据,它将不得不向应用发出两个请求,但关键问题是,根据 $order 属性请求的数据的顺序,可能来自一个源的数据为零而来自另一个源的数据为 10 条,或者相反,或者介于这两种极端之间的任何情况。这意味着负责合并数据的网关将不得不从一个应用中取出 10 行数据,从另一个应用中取出 10 行数据,然后对这 20 行数据重新应用排序算法,最后将前 10 行发送给请求客户端,并丢弃随后的 10 行。

不要认为使用本地缓存会更简单,因为你除了要实现刚才提到的排序算法外,还必须在上面实现查询机制。想象一下,如果这需要更多应用程序来完成!有 5 个业务应用程序,你已经缓存了 50 行数据,实际上只使用了 10 行,这造成了 80%的资源浪费。你可能想到预先查询应用程序,以了解哪些会提供过滤值中的数据,但这意味着你已经开始查询一个应用程序,然后调整计数查询到其他应用程序,也许是为了实现优化不会减少查询次数,而只会减少检索的行数。选择一个枢纽应用程序本身就可能很困难,因为结果可能很微弱,因为我们无论如何都在处理减少的数据集(这是分页请求的目标)。等等!我们还没有谈到最糟糕的部分:当分页到第 10 页数据时(如果我们保持在每页 10 行的情况下,在 90 到 100 之间),你将无法简单地从每个 5 个应用程序中调用 10 行,因为可能有一个应用程序会占据从分页开始以来几乎所有的行,而其他一些应用程序在同一范围内将提供没有任何数据。这意味着你可能会在调用第 10 页时,第一个结果只来自一个应用程序!你现在看到了,不是吗?是的,我们将不得不查询 5 个应用程序以提取对应于聚合数据 90 到 100 范围的 10 行,这意味着 98%的巨大浪费……而且,这个悲伤的蛋糕上的樱桃是,如果一个应用程序不支持动态范围,你可能需要多次查询它,以组成所需数据的完整范围。当然,在某些实现中,可能可以保持数据库查询的游标状态,但这意味着你的应用程序现在是状态化的,这将导致一些其他技术限制,例如可扩展性。好吧,唯一能救我们的是,通常,用户会在第二页或第三页数据处停止,细化他们的$filter属性以获得更快的结果。

一致性问题也存在,但只要数据的切割遵循功能逻辑顺序,它们就更容易处理。这通常是这种情况,因为数据分布是在业务应用程序中完成的,所以他们有重复数据的风险(当然,除了花蕊,花蕊总是共享的)通常非常低。

其他类型的参照架构

“虚拟”数据参照是“分布式”参照的一个特例,其中中心部分本身不持有数据,因此没有持久性,依赖于周围的业务应用程序数据库。示意图如下:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.9_B21293.jpg

图 10.9 – 虚拟参照架构

其他,更为罕见的参照架构也存在,但在这里展示它们似乎并不真正有用。对于那些好奇的人,法国政府发布的文件名为Cadre Commun d’Architecture des Référentiels(参照架构的共同框架,可在互联网和法语中免费获取)不应成为限制,因为不同的可能性主要是通过图表展示的。

现在已经展示了架构模式,我们可以谈论实现本身,包括在创建数据参照时应该做出哪些技术选择以及如何做出选择。

一些架构选择

其中之一当然是数据库。顺便说一句,我甚至应该说持久化机制,因为数据库是一个非常著名的持久化机制,但还有其他,我们将在本节末尾看到。还有一些其他技术考虑因素需要处理——特别是关于数据流。

本节也将是一个机会,对 IT 中的教条进行一番抱怨,以及它们如何延迟了信息系统工业化的长期期待。许多技术选择仍然基于可用团队的知识,而不是当前功能问题的适宜性。这并不是说不应考虑能力,但有时应强迫那些几十年来没有改变思维方式的技术人员接受培训,因为他们可能因为简单地应用了错误工具到问题上而阻碍了信息系统的发展。你可能听说过谚语“如果你只有锤子,所有问题看起来都像钉子。”如果你团队中有这样的人,管理者的工作就是通过培训打开他们的眼界,无论是内部、外部、正式还是非正式的。

表格式与 NoSQL

在实施数据参照时,必须做出的第一个决定之一是选择哪种数据库范式。应该是表格式的还是面向文档的?SQL 还是 NoSQL?考虑到 99%的商业实体自然形状是具有许多层级的文档结构,如属性树和具有不同深度的数组,如果你想要达到业务/IT 的协调一致,那么显然的选择应该是一个适应你数据形状的 NoSQL 数据库:如果你管理的是业务实体,那么是文档型 NoSQL;如果你操作的是通过许多类型关系与其他实体相连的数据实体,导致一个可以通过许多路径遍历的实体网络,那么是图型 NoSQL,等等。

如果真正实现了业务/IT 对齐,并寻找一个与他们的数据形状紧密匹配的持久化机制,那么对于自然表格形式的业务实体,应该使用 SQL 表格数据库……但这几乎从未发生过!当然,有一些情况,就像在 NoSQL 领域中的一些键值对列表一样,但它们非常罕见。实际上,看起来 SQL 仍然被大量用于数据引用的主要原因仅仅是历史。当处理遗留软件时,这是一个合理的理由……毕竟,如果它以这种方式工作了多年,你最好不去碰它。但真正的问题是,在信息系统现代化项目期间设计的新数据引用也使用了非高效的方法。

我为什么说低效?为了解释这一点,需要回顾计算机科学和数据库的历史……在数据存储的早期,当使用随机访问控制器与旋转磁盘一起使用时,数据并没有在磁盘中随机化,而是放置在一系列的块中(最好放在硬盘的最外圈,因为线性速度更高,提供更快的读取)。为了快速访问正确的块,数据库引擎会强制数据行的尺寸,以便快速跳转到下一个,提前知道每行数据的总长度。顺便说一句,这也是为什么数据库中的旧类型字符串需要固定长度。这也是为什么数据必须以表格块的形式存储,结构化数据分解成许多表,其中行通过键相互关联,因为这是计算下一个块索引的唯一方法。

这些假设虽然代价高昂:由于数据是表格形式的,存储实体属性的多个值唯一的方法是在数据库中创建另一个表并将两行数据连接起来。其结果是,需要复杂的机制来处理全局一致性,例如事务。反过来,事务使得必须创建悲观锁和乐观锁的概念,然后管理事务的隔离级别(因为唯一的完全ACID事务,即可序列化事务,对性能有显著影响),然后是死锁管理以及许多其他复杂的事情。

当你思考并意识到硬盘控制器已经提供了数十年的随机访问(而且旋转磁盘的概念在 SSD 中根本不存在),很难理解为什么这种后果在今天仍然如此普遍。其中一个原因是变更管理,因为没有人喜欢改变。但如果有工作需要适应和接受变化,那肯定应该是开发者。我也能理解为什么 SQL 仍然在人们只把它当作持久化技术的研讨会中使用。最好是用整个团队都熟悉的工具开始一项重要工作,我不会建议从没有人知道的复杂技术开始。但在这种特定情况下,不使用 NoSQL 作为业务实体数据引用,会有两个问题:

  • 首先,这是一个培训问题,因为这些技术已经存在十多年了,经验回报已经确立,有可信赖的操作员。

  • 其次,实际上很少有技术像文档型 NoSQL 那样容易。以 MongoDB 为例——将一个完整的 JSON 实体写入兼容 MongoDB 的数据库就像以下这样简单(C#示例):

    MongoDBConnection conn = new MongoDBConnection(ConnectionString);
    conn.Insert("Actors", "{ 'lastName': 'Gouigoux', 'firstName': 'Jean-Philippe', 'addresses': [ { 'city': 'Vannes', 'zipCode': '56000' } ] }");
    

    与基于 SQL 的表格型关系型数据库管理系统(RDBMS)(简称关系数据库管理系统)相对应的是以下内容:

    SQLConnection conn = new SQLConnection(ConnectionString);
    SQLTransaction transac = new SQLTransaction(conn);
    try {
        transac.Begin();
        SQLCommand comm = new SQLCommand(conn, "INSERT INTO ACTORS (lastName, firstName) VALUES (@lastName, @firstName)");
        Comm.Parameters.Add(new SQLParameter("@lastName", "Gouigoux"));
        Comm.Parameters.Add(new SQLParameter("@firstName", "Jean-Philippe"));
        string idActor = Comm.ExecuteGetId();
        comm = new SQLCommand(conn, "INSERT INTO ADRESSES (id, city, zipcode) VALUES (@id, @city, @zipcode)");
        Comm.Parameters.Add(new SQLParameter("@id", idActor);
        Comm.Parameters.Add(new SQLParameter("@city", "Vannes"));
        Comm.Parameters.Add(new SQLParameter("@zipcode", "56000"));
        Comm.Execute();
        transac.Commit();
    } catch (Exception ex) {
        transac.Rollback();
        throw new ApplicationException("Transaction was cancelled", ex);
    }
    

    我甚至没有提到创建表和列的数据定义语言(DDL)命令,这将增加很多行。MongoDB 不需要这些,因为它是无模式的,并且随着对象的添加,集合会创建为对象。

再次,有些情况下需要 SQL。报告工具非常多,使用这种语法,公开 SQL 端点以访问数据是一种好习惯,因为它简化了数据的消费。大数据工具甚至 NoSQL 数据库都有 SQL 端点。这是有价值的,因为有很多人在使用这种方式查询数据并计算复杂聚合方面很在行。然而,仅仅为了能够使用众所周知的查询语言而选择表格数据库来存储结构化数据是一个问题,因为它将导致很多不必要的复杂性。在你下一个数据引用中,请考虑使用 NoSQL,因为它会为你节省很多时间。如果你知道这类项目将是你下一个项目组合中的下一个,请开始为你的团队进行培训。只需要几天时间就能理解所有需要熟练掌握文档型 NoSQL 服务器(如 MongoDB)所需的知识,而且它们非常适合存储业务实体。

CQRS 和事件溯源

当我们谈论这个问题时,你可能还想要放弃那些由同一过程处理读写操作的老旧数据流架构。毕竟,这两组操作在频率(大多数业务线应用程序LOB)有 80%的读取和 20%的写入)、功能(读取不需要锁,写入需要一致性)和性能(独特的写入不太重要,大规模查询非常重要)上都有很大的不同,因此将它们分开似乎是合理的。

这就是命令和查询责任分离CQRS)的含义:它将接收更改或创建数据命令的存储系统与准备好回答相同数据查询的系统分开。事件源与这种架构方法密切相关,因为它存储了一系列由写入命令生成的业务事件,并允许查询以高度可扩展的方式使用此存储来获取所需的聚合结果,从而允许在大数据上实现性能。

在某种程度上,CQRS 可以被看作是分布式和克隆方法之间的一种参考架构。它不是根据数据本身的标准来分离应用程序之间的数据,而是根据将要对其执行的操作类型(主要是写入或不同类型的读取)。同时,准备好的读取服务器可以被认为是“单一版本的真实数据”的克隆。由于单一版本的真实数据主要在持久化中,它们的数量可以无限增加,因此性能总是可以调整,无论查询多么复杂,以及数据量有多大。

再次强调,这不是详细讨论这些主题的地方,但它们必须在关于数据参考和 MDM 的章节中被引用,因为它们是实现高容量解决方案无可争议的最佳方法。

超越数据库的又一步——存储在内存中

让我们回到关于表格数据库系统起源的讨论,甚至更早一些。我们为什么实际上需要数据库和存储系统?主要是因为硬盘可以存储比 RAM 更多的数据,而且数据库无法适应少量的 RAM。因此,需要一个能够快速将数据写入磁盘(以便在硬件故障的情况下保持数据安全,数据库首先写入日志文件)并擅长从磁盘检索部分数据并将其放回内存以供应用程序使用的系统(这就是 SQL 部分,特别是SELECTWHERE关键字的作用)。

当然,当计算机只有 640 千字节 RAM,数据库需要几个兆字节时,这是一个主要问题。但今天呢?当然,有巨大的数据库,但我们通常只有几个吉字节大小的数据库。至于服务器 RAM 呢?嗯,拥有数十吉字节的服务器非常普遍,而且很容易在线获得 192 GB RAM 的服务器。在这种情况下,为什么还需要在磁盘内外操作数据呢?当然,SSD 磁盘是一种内存,但它们仍然比 RAM 慢。此外,确实需要处理硬件故障下的持久性问题。但数据操作本身怎么办?将查询操作到 RAM 中不是会更快吗?

事实上,确实如此,并且存在一种很少使用且鲜为人知的技巧,称为“对象普遍性”,它充当内存数据库。我们不是在谈论存储在 RAM 磁盘或高速 SSD 上的文件,而是在应用的对象模型中直接使用数据。你可能会问,如果发生硬件故障,我们如何确保不丢失任何数据?嗯,正好像数据库一样:通过记录发送到系统的所有命令的基于磁盘的日志。那么区别在于,操作数据和提取、过滤和汇总结果的数据参考模型不是在磁盘上的某些表格上,而需要伴随索引以提高性能,而是在 RAM 中,并且是以二进制格式存在的,这是直接由你的应用程序使用的,这意味着没有什么能更快。通过这样做,SQL 中的请求被你选择的语言中的代码所取代——例如,使用 LINQ 查询的 C#。

实际上,对象普遍性从未达到更广泛的受众,但我所知道的所有使用过它的人都对其高价值深信不疑。就我个人而言,当我需要实现一个体积有限但具有以下要求的数据参考时,我总是选择这项技术:

  • 需要高性能

  • 非常复杂的查询,这些查询在 SQL 中很难编写

  • 一个经常演变的数据模型

我参与过的最好的数据参考实现之一是在一个项目中,该项目计算高级金融模拟并使用遗传算法进行优化;性能提升巨大,能够编写极其复杂的数据操作案例使得整个项目对客户来说是一个明显的胜利,客户在第一次测试中就被模拟的速度所震惊——这个取代了旧平台的新平台只需几秒钟就能给出结果,而旧平台则需要几分钟。

另一个成功的实施例子是在处理低流动数据,如国家代码。在这个特定的例子中,人们对内存方法并不感到满意,尽管数据在磁盘上的日志中是安全的(我们甚至有备份,作为第三组数据以提高可靠性)。因此,用他们可以轻松反馈到数据参照中的某些数据测试这个相当创新的方法,使得第一次尝试这项技术更加舒适。测试进行得很顺利,但客户并没有将其扩展到其他数据。遗憾的是,我不知道更多关于这项技术使用的例子,这有点令人遗憾,因为潜力是巨大的。

尽管这个例子可能不是最好的,因为这项技术并没有取得成功,但信息仍然存在:为了尊重业务/IT 的协同,这是确保长期发展的最佳方式,始终优先考虑与您的业务需求和数据形状紧密匹配的技术。

在本书的最后部分,我们将再次讨论时间以及它如何影响我们对数据参照的处理,在我们的案例中。

数据随时间演变的模式

第四章中,我们研究了在信息系统中对时间管理的重要性,以及时间处理对数据的主要影响。在 MDM 系统中处理的数据必须考虑时间因素,我们广泛地讨论了数据历史管理。但 MDM 本身的行为也应该根据时间来执行。

数据治理

数据治理是围绕数据参照管理建立功能责任的行为。谁负责哪些参照数据?谁可以操作和清理数据?谁决定模型的演变?如何通知受影响的团队和应用关于变更的信息?在操作数据时应该遵循哪些业务规则?数据应该在何时被删除或存档?这些都是治理问题,并且它们始终与时间相关。特别是,这些回应必须定期审查,就像业务流程一样,以便数据保持受控。

数据治理主要在 Cigref 地图的第二层处理,即业务能力地图,通常包含一个专门用于参照数据管理的区域。这就是您应该绘制不同的数据参照,并存储存储的实体的详细定义,以及版本以证明它们之间的兼容性或记录不兼容的变更。在这里,您至少应该找到两个主要数据治理角色的名称和联系方式:

  • 数据所有者:此人是信息系统内数据质量和可用性的最终责任人。他们定义围绕数据的所有业务规则:数据必须如何操作,谁可以访问它,在什么条件下,等等。

  • 数据管理员:在数据所有者的委托下,此人是负责数据的日常维护。他们根据数据所有者发布的数据操作规则,清理数据并确保其可用性和完整性,以及遵守授权规则。

数据治理的一个明显后果是,对于特定的数据参照有明确的职责。对于参照的共有责任是一个问题,因为可能会有竞争性的需求,这些需求在实体格式或提供的服务的不受控制的演变中发展。在最坏的情况下,IT 团队不知道该考虑谁作为决策者,并实施两个需求,使得数据参照越来越难以使用,并且不适合其目的。没有责任甚至更糟,因为实施属于 IT 团队,技术人员默认成为数据的所有者,这可能是有史以来最糟糕的举动,因为他们对与数据相关的业务风险了解得最差。当然,他们基本上知道数据是什么(毕竟,我们都在公司里知道客户或产品是什么)但同样,魔鬼在于细节,当 IT 团队负责定义数据时,没有人应该对组织只支持一个地址或产品与商品之间没有区别感到惊讶。这种错误永远不会由该领域的专家犯下,我们都知道一个糟糕的实体定义可以有多具破坏性。因此,由于没有人愿意承担责任,将这样的业务驱动决策留给 IT 团队是一个风险举动,每个人都应该对此保持警惕。

逐步实施独特的参照

在展示分布式和合并的数据参照架构时,已经指出,有时,这些走向集中参照(通常,这是最终目标)的中间步骤可能花费与直接进入目标状态一样多的时间,因为隐藏的努力或不太为人所知的缺点。相反,有时直接面对最终愿景是不可能的,而应该通过几个逐步步骤来实现这种收敛。这可能是由于信息系统耦合得太紧密,剧烈的变动可能会破坏它;大多数时候,问题在于人类接受变化的能力,而必须采取逐步的方法,以便组织本身能够调整。

在许多情况下,我作为顾问为那些需要成功管理他们的合并或收购其他公司的公司提供服务时,这种情况就发生了。为了成功管理合并或收购,他们需要将合并计划应用于两个信息系统,将它们合并为一个单一的系统。这类事情在大组织中通常需要数年(我见证的最快的是在不到 18 个月内完成的,但所有标志都是绿色的,这种情况很少发生)。正如您将在以下部分中看到的,这些计划需要许多步骤才能实现。

由于隐私原因,我将展示我为一个公共客户(法国两个地区委员会的融合)和一个由法国西部两个大型实体合并而成的农业合作社设计的两种渐进式转换的混合。他们都需要解决他们的信息系统处理(客户、代理商、潜在客户、农民、学生等)的个人和法律实体的 MDM。为了简化图表,我将考虑起点是两个实体各自都有一个综合数据参照,一些应用程序显示出克隆参照模式。这种情况通常发生在有许多需要参照数据的应用程序时:最重要的是直接连接到最高级的数据参照应用程序,而次要应用程序只是简单地克隆其主导业务应用程序中的内容。在以下方案中,我也大大减少了应用程序的数量,再次,出于简化的原因。我没有绘制它们与其他信息系统软件之间的关系,因为它们大多是具有高度互操作性的 ERP 系统。

第 1 步 – 相同的基础设施但没有链接

话虽如此,第一步可以概括如下:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.10_B21293.jpg

图 10.10 – 两个 MDM 系统的融合 – 第 1 步

这两家公司拥有完全独立的 MDM 系统,因此对于他们的“演员”,如果这是我们应该用来描述这些实体的名称,那么数据参照就是这样的。请注意,大多数应用程序在每个案例中都是不同的,除了App1,这是两家公司之间的一个共同 ERP(这并不意味着它们将是兼容的,因为版本可能不同,定制肯定会有,但这可以成为一个很好的候选,以便在某些时候将事物放在共同之处)。当然,第一步是连接两个内部网络,即使接下来将要展示的所有内容完全可以仅通过互联网通信来实现。

第 2 步 – 提供一个公共接口

第二步是为新融合实体的所有用户提供一个 API 来读取演员:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.11_B21293.jpg

图 10.11 – 两个 MDM 系统的融合 – 第 2 步

注意这个图是如何对称的:选择一个中立的中心格式至关重要,因为使用其中一家公司的专有格式将对另一家公司(它将不得不更改所有连接器和映射代码)造成明显的劣势,这还会引起人为问题,因为在公司合并期间,尤其是当它们之前是竞争对手时,紧张局势总是加剧的。因此,我们花了很多时间为用户制作了一个漂亮的中心格式,使用了来自两边的最佳数据表示。在这一步,不仅读取是唯一可用的操作,而且没有任何一家公司可以读取另一家的数据!你可能会想知道这一步有多有用,因为目标是达到两家公司都有的唯一 MDM 系统,而现在它并没有改变任何事情。事实上,没有功能性效果确实更难,但准备一个共同的中心格式是适当共享数据的基础。此外,它为在融合过程中创建的所有新软件功能以标准化的、融合就绪的方式读取参与者提供了一种方法。这意味着我们不必回到这些新应用,当你知道整个项目中要处理数百个应用时,这是一个非常受欢迎的消息。最后,它开始了中介连接器的工作(同样,这是最好在 Apache Camel 或另一种企业集成模式中实现的事情),这是一项重要的工作,最好在项目早期开始。

第 3 步 – 将次要来源的数据与主要来源合并

从现在起,我们将只从公司 A 的角度来表示流之间的差异,但情况总是相反。下一步是开始从信息系统之一获取一些数据并将其传输到另一个系统中。这同样是非常进步的:目前只针对数据的读取操作进行了操作,如图所示,数据首先在发起请求的人的系统上读取,然后仅使用“来自屏障另一侧”的数据来完成。在任何时候,原始侧的数据都会获胜,除非修改日期清楚地表明来自另一个信息系统的数据更新。

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.12_B21293.jpg

图 10.12 – 两个 MDM 系统的融合 – 第 3 步

为了使前面的步骤工作,有必要找到一种方法来寻找类似的角色,例如,使用他们的增值税号或其他商业标识符。

第 4 步 – 存储来自另一来源的标识符

由于这是一个复杂的操作来实现,一旦找到了对应关系,一方的技术标识符被存储在另一方,反之亦然,这将允许下次更快地访问。这是系统第一次在 MDM 系统中写入,但仅限于存储另一方的标识符:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.13_B21293.jpg

图 10.13 – 两个 MDM 系统的融合 – 第 4 步

然而,这开辟了共享数据的新方法,因为一旦提供了“写入”授权并且知道了“外部”标识符,每一方都能够与另一方共享信息。

第 5 步 – 向另一侧发送信息

每当一方上的行为者发生变化时,另一方都会得到通知。接收信息系统能够自由地以自己的节奏处理这些信息,也许第一次什么也不做,但随后选择哪些数据片段是有趣的并将它们存储起来,等等。在这个阶段,保持数据变更的来源是必要的,以避免启动一个信息循环,将数据变更的事件发送回初始信息系统,因为它的初始事件。为了简化,图表再次仅从 A 到 B 表示 – 如下所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.14_B21293.jpg

图 10.14 – 两个 MDM 系统的融合 – 第 5 步

现在,由于初始写入已经开始,信息系统(和人)开始更好地相互信任,下一步就是推广数据的修改。

第 6 步 – 集中数据写入并扩展边界

这意味着双方开始使用集中的 API 进行写入,该 API 的实现是在双方推送数据,以便每个信息系统都能了解最新的数据。再次强调,使用数据取决于接收端是否知道行为者(或应该记录它),但在某些情况下,数据被简单地忽略,例如,当这涉及到一个只在另一家公司使用的供应商的变化时。至于潜在客户,数据是共享的,因为商业方法开始在这两个逐渐融合的公司部分之间统一。

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.15_B21293.jpg

图 10.15 – 两个 MDM 系统的融合 – 第 6 步

在 MOM 实现中使用的企业集成模式是“重复消息”模式,将初始请求推动的数据发送到两个类似的消息中,并通过中介路由,等待两个确认消息返回,以便在其被调用的路由上发出自己的确认,从而有效地创建一个健壮的变更交付,双方都能收到。

第 7 步 – 统一访问

这段时间,旧的数据参照系统开始仅作为消息的守门人,检查它们是否与其信息系统的相关部分相关。但是,由于参与者现在主要是共享的,这并不是一个特别重要的功能,因此一些应用程序开始直接将它们的参与者消息注册到顶级数据参照中:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.16_B21293.jpg

图 10.16 – 两个 MDM 系统的融合 – 第 7 步

App1(在双方都使用的 ERP)是开始这种新方法的绝佳候选者,因为连接到它的中介连接器可以直接在两个信息系统之间共享,从而实现首次共同部署,降低“门槛”的高度。由于这种方法工作得相当好,它为其他应用程序的启动提供了动力,并且很快在另一个应用程序上出现了专门的连接器,因为共同的枢纽格式已经演变,比之前的格式更容易,也覆盖了更多的业务案例。

第 8 步 – 消除不必要的调用

情况迅速演变成如下所示的方案,因为旧的 MDM 系统基本上已经没有更多的事情要做,因为所有数据都来自新的集中式系统:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.17_B21293.jpg

图 10.17 – 两个 MDM 系统的融合 – 第 8 步

此外,一些应用程序,如App7,有足够的时间进行演变,能够直接采用一些表示参与者的 JSON,而不需要借助中介连接器。还有一些应用程序开始在两个组织之间共同使用(这一点越来越明显地表明它们正在成为一个单一的组织),App4App6的通用使用而消失。

第 9 步 – 移除不必要的中间应用程序

一些“低策略”应用程序仍然受业务应用程序(如App3)的控制,但这并不是问题,因为它们的父应用程序现在位于主数据参照之下,将为他们处理格式变化。这些应用程序没有看到系统有任何变化,这对用户来说是个好消息,因为用户根本未受到其他重大变化的影响。由此产生的信息系统开始看起来如下:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_10.18_B21293.jpg

图 10.18 – 两个 MDM 系统的融合 – 第 9 步

由于App6被所有团队使用,两个原本分离的公司之间的障碍又降低了一步,达到了一个点,即它不再成为问题,因为它只分割了一些由专门团队在非融合过程中的某些特定情况下使用的次要业务应用程序。现在有一个独特的集中式 MDM 系统,其中一些重要应用程序作为本地参考,供次要应用程序克隆部分数据。这总共花费了许多年,但目标已经达成:合并双方使用的参与者,并以这种方式逐步进行,以至于业务从未受到技术选择的影响。

关注教条和无用的复杂性

我希望在这一章(以及本书整体)中已经说服你,对那些使用起来看似明显的技术和实践保持批判性的眼光。就像用于数据参考存储的 SQL 数据库,或基于硬盘的数据操作一样,在开发过程中有许多先入为主的观念,当纯粹从业务/IT 对齐的角度思考时,这些观念并不非常适合问题。

只举一个例子,就是数据验证。在大多数编程语言中,验证器与数据实体的字段或属性相关联,例如在 C#中通过属性。在我看来,这种方法非常错误,在我的实践中已经多次证明这是一个真正的痛点,因为几乎总是可以找到一个特定的情况,这些验证属性是不正确的。在业务标识符的情况下,产品所有者有时会坚持认为没有任何实体应该在没有这种值的情况下创建,然后大约一年后,他们会意识到存在这样一个特定的情况,即标识符尚未知道,我们仍然应该将实体保留在系统中。例如,这可能是一个医疗患者数据库,产品所有者会向你保证,没有社会保障号码的实体在考虑提供药物之前是没有意义的,这是绝对必要的……在坚持为了数据质量原因在这个字段上放置严格的NOT NULL验证器之后,同一个人几个月后可能会回来,当数据库处于生产状态且重大影响变更将产生巨大成本时,告诉你他们忘记了新生儿的特定情况,这个新生儿应该接受药物,但他们还没有社会保障号码。

在这个特定的例子中,我个人的习惯是永远不会将任何实体属性描述为强制性的,因为只有其使用的上下文才使其成为强制性的或不强制性的。添加一个阻止null值的业务规则或表单行为是如此简单,以至于在实体本身上不放置它根本不是问题。另一方面,当这种强制特性已经在你的信息系统最低层实现时,整理混乱和错误的原因是如此痛苦,以至于在我看来,永远不应该将字段称为“强制性的”(除了一个技术标识符的例外,否则一旦创建就无法唯一检索实体)。

重要提示

当我阅读像jonhilton.net/why-use-blazor-edit-forms/这样的文章时,我很喜欢,作者在那里质疑技术中存在“太多的魔法”。实际上,确实如此,这样的批判性眼光是阅读给定技术的最佳方式,而不是众多仅仅解释如何使用函数而不深入探讨何时有用以及何时实际上更多的是危险而不是真正优势的博客文章。这篇文章对表单和数据定义中包含的验证确实有一个很好的观点。

顺便说一句,对于之前提到的标识符,同样适用于基数:如果你没有产品所有者绝对、明确和完全负责的承诺,即一个属性应该具有零或一基数,总是将其作为一个具有N基数的数组。最坏的情况会是什么?数组总是只填充一个项目?嗯,这其实并不重要,对吧?开发者会抱怨,在这些场合,他们必须输入deliveryAddresses[0]而不是deliveryAddress?向他们展示如何在所使用的语言中创建属性,问题就会解决。至于 GUI,我们将在没有对应处理数组中多个值的用例的情况下,简单地显示一条信息。只有当出现这个新的业务案例,我们需要处理多个数据时,我们才会调整 GUI,用列表代替单个文本区域,例如。但这种方法的好处是,这将顺利地进行,因为之前唯一的数据将简单地成为列表中的第一个,更重要的是,所有 API 的客户端都将保持兼容性,不会因为这种新的用途而损坏。他们甚至可以在不使用其他数据的情况下继续只使用列表中的第一个数据,只要他们不想使用其他数据并坚持旧的行为。由于所有客户端和服务器都可以根据自己的步伐在业务变化上前进,我们知道我们有一个低耦合。

这一点也适用于许多其他旨在帮助企业的技术方法,但最终可能会阻碍企业的发展。仅举最后一个例子,大多数关于数据鲁棒性的技术方法实际上与商业概念相悖。例如,出站模式(microservices.io/patterns/data/transactional-outbox.html),只有在最终一致性不是选项时才应使用。但是,当你知道即使是银行也一直使用最终一致性(并且肯定会继续这样做),这大大限制了这些技术的实用性。当然,深入理解业务不如使用最新的技术或模式有趣,这些技术或模式可以将交易错误率降至最低。但从长远来看,这是唯一获胜的方式。

因此,再次强调,因为这是一个如此重要的信息,首先考虑业务功能,然后找到适应它的技术。为了做到这一点,最简单的方法是想象在没有计算机参与的情况下,业务参与者之间在现实世界中会发生什么。

摘要

在本章中,MDM 的原则已被应用,实施技术已被公开,不仅从架构的角度,还包括在构建数据参照时可能有用的技术选择。这些服务器应用的主要行为已被涵盖,并通过一些示例描述了它们随时间的变化。这应该使你相当了解如何实现自己的数据参照。

我们将在第十五章中回到 MDM 的主题,我们将深入到实施的最底层,使用实际的代码行以及用 C#设计和开发两个数据参照实现的示例,分别处理作者和书籍。这将是我们最终要完成的部分,我们将结合在第八章中学习的服务管理和 API 的原则,第九章中展示的实体的领域驱动设计,以及本章中描述的架构方法。

但在我们达到这一点之前,我们将研究理想信息系统中的另外两个部分,就像我们在 MDM 部分所做的那样。下一章将介绍业务流程建模以及我们如何使用BPMN(即,业务流程建模符号)和 BPMN 引擎在我们的信息系统中实现业务流程。下一章还将介绍其他一些主题,例如中间件、无代码/低代码方法以及编排与协奏曲的比较。

第十一章:业务流程和低代码

业务流程遍布信息系统,在与 CEO 交谈时,他们通常会告知他们如何看待 IT:作为一种自动化公司业务流程的方式,提供可靠性、可重复性和——在最佳系统中——对发生的事情的可见性,无论这些事情是为内部客户还是外部客户带来价值。业务流程是公司信息系统的核心,因为每个活动通常由一个流程实例承载。当公司获得 ISO 9001 认证时,认证范围内的每个流程都得到了精确的记录,并且可以验证相关演员的实际使用情况。因此,这些流程结构化了公司的活动。

本章详细说明了从业务流程的角度来看,一个干净的架构应该如何表现,通过解释业务流程建模、业务活动监控和业务流程挖掘的概念,然后展示如何在 IT 系统中使用业务流程。本章讨论了低代码和无代码方法,以及基于 BPMN-2.0 的方法。在整个章节中,我们将提供示例,将实践与我们的演示信息系统联系起来,使 IT 中使用的流程更加具体。最后,还将详细说明通过服务编排的业务流程的另一种方法。

在本章中,我们将涵盖以下主题:

  • 业务流程和 BPMN 方法

  • 基于业务流程软件的执行

  • 其他相关实践

  • 业务流程实施的其他方法

  • 我应该在信息系统中使用 BPMN 吗?

如果记得清楚,第五章介绍了乌托邦式的理想信息系统的概念,该系统仅由三个模块组成。主数据管理MDM)是第一个模块,在上一章中已经对其进行了详细研究。现在,我们将分析第二个模块,即 BPM,究竟是什么。在下一章中,我们将通过彻底解释 BRMS 来结束这一部分。正如您将看到的,业务流程方法目前在信息系统中的应用并不广泛,当然甚至不如数据参考服务文化那样普及。规范和标准已经存在,但实际实施却很少。这是一个需要考虑的重要观点,因为本章将要展示的内容更多的是一种理想(至少目前是这样),而不是对现有信息系统进行定位的建议。只有时间才能告诉我们,IT 是否会围绕这种稳固的方法进行结构化,或者成本是否会过高,以至于在 IT 行业中难以广泛应用。

业务流程和 BPMN 方法

在本节中,我们将更详细地解释什么是业务流程,以及我们如何使用软件方法对它们进行建模,特别是使用一种称为 BPMN 的标准。在谈论 IT 世界中的流程之前,确实很有趣回到从纯粹的功能角度对流程的定义,正如我们在关于业务/IT 对齐的这本书中一直所做的那样。

什么是业务流程?

业务流程是一组协调的人力和自动化动作,旨在实现一个目标。术语“流程”通常可以替换为几乎等价的“工作流程”,这更好地表达了这样一个事实:这些动作(或“任务”)是由一个人类行为者或软件片段实际执行的工作,并且它们在一个有组织的流程(“流程”)中实现,以实现流程所追求的商业目标。

如引言中所述,业务流程在组织中无处不在,因为“企业”按定义是一群人,他们拥有实现一个目标的方法,这个目标单靠他们是无法实现的。流程是企业实现这些目标的方式。通常有一个主要战略目标,解释了为什么几个流程实际上是必要的。例如,公司的战略目标可能是成为软件书籍编辑和出版的世界领导者。其战略可能包括,例如,通过雇佣许多不同的专家作者,详细涵盖所有可能的主题。制定如何实现这一点需要几个较小的、操作性的目标。在我们的例子中,这意味着一个良好的招聘流程,因为为所有软件主题找到合适的专家需要一种有组织的做法。另一个操作流程将是写作的跟进,包括编辑、校对员、校对员等。

这种类型的流程是我们通常首先想到的,因为它直接面向目标,在这里是生产和销售书籍。尽管如此,还有两种其他类型的商业流程:

  • 支持流程是所有必要的商业工作流程,以便公司能够继续运营,而这些工作流程与公司的目标没有直接关系。在以盈利为导向的公司中,支付员工的薪水不是战略目标;绝对有必要保持公司和流程的运行,但这不是公司成立的原因。这些不是作为公司目标建立的,但对于实现这些目标来说是必要的流程,被称为支持流程。

  • 试点流程是处理其他工作流程治理和分析的流程。其中一种工作流程是分析公司的活动指标。另一个可以归类为“试点”的流程是质量管理,它涵盖所有运营流程,目标是持续提高其效率。治理或试点流程,就像支持流程一样,不是直接运营的。与试点流程的区别在于,它们位于所有其他流程之上,而支持流程是运营流程的依赖。

流程的粒度

正如我们刚才看到的,一个组织通常有一个主要的高层次战略目标,并且需要几个流程来实现达到高层次目标所需的各个较低层次的目标。当以如此大的粒度对流程进行分组时,我们通常谈论宏观流程,因为它们非常通用。它们很容易被定义为这样的,因为它们的目的是一个具体的可交付成果,而是一个公司做什么的一般想法。例如,可以谈论“商业”和“生产”作为宏观流程,因为它们的成果非常通用,分别是通过销售获得资金和生产商品或服务。很难说我们如何真正详细地实现这一点。

当谈论业务流程时,与宏观流程相比,其结果是可量化的。例如,生产汽车是一个业务流程,因为我们能计算一周内有多少辆车离开工厂。编写软件是另一个业务流程的例子,因为其结果是发布一款软件,以及如何利用它(文档、设置软件等)。构成业务流程的任务与一种类型的参与者相关联,例如“组装发动机”、“撰写书籍摘要”或“制作商业报价”。这就是它们与宏观流程的不同之处,在宏观流程中,流程的某个部分可能需要许多不同的角色,例如“产品营销”或“开票”。宏观流程内部的单元可以是业务流程。在我们的编辑示例中,“生产书籍”的宏观流程需要招聘作者的业务流程,另一个是监督他们的写作,还有一个是校对书籍。

通过将业务流程的不同项目分解成详细的步骤,可以在一个层次下观察到基于级别的分解和流程的粒度。这些步骤本身又构成了另一个流程,这次是一个细粒度的流程,通常被称为“程序”。这次,程序不仅说明了每个参与者必须完成的任务,而且还精确地说明了他们必须执行的操作来实现业务流程中的特定任务。例如,在“销售书籍”的业务流程中,可能有一个名为“发送发票”的任务。这个任务的详细程序可能由以下“程序要素”组成:

  1. 每个月列出所有订购书籍的客户。

  2. 对于每位客户,从库存数据库中收集所有已发送的书籍和数量。

  3. 核实这些包裹确实已经发出。

  4. 核实与该客户的折扣协议。

  5. 计算已发送书籍的总金额。

  6. 减去客户可能因书籍退货或保修而应得的信用额度。

  7. 将所有这些数据输入到“发票”模板中。

  8. 打印两份副本,并将其中一份存放在会计办公室。

  9. 使用客户的账单地址将另一份副本发送给客户。

在我们今天这个充满客户关系管理(CRM)和企业资源规划(ERP)系统、只销售电子书并在网上订购/开票的虚拟世界中,这个最后的例子可能显得有些过时。使用这个例子有两个原因。首先,正如之前所解释的,在业务/IT 对齐中,通过去除与 IT 相关的任何内容来考虑问题总是很有趣。这使我们能够只关注功能问题,并在考虑技术实现之前,从最复杂的细节来理解它。这样,基于软件的假设,这些假设可能导致耦合,至少在第一次分析中,被排除在范围之外。

展示这样一个过时的程序的第二原因是,为了说明软件实现业务流程是如何让我们几乎忘记它们的。有很大可能性,当阅读这个步骤列表时,你会想,“没有人再手动做这些了。”你会是对的:现在所有这些操作大多由 ERP 和专门的发票软件应用完成。但是……必须至少有一个人了解这些步骤,这个人将设计这些软件应用!由于这本书正是关于这个的,因此——再次强调——在尝试在信息系统实现它们之前,从纯和正确的业务理解开始是非常重要的。

此外,在自动化之前建立这个详细的程序将允许你从业务专家那里获得一些见解。例如,来自会计部门的人会告诉你,如果你想要在国际上销售,你必须处理多个增值税率。另一个人会补充说,如果客户欠你债务,则不应考虑信用。还有另一位同事可能会争论,在某些情况下,订单的付款人可能是一个不同于应该接收发票的法人。等等……

流程包含其他流程作为单个任务的详细说明的原理是业务流程建模方法中的重要概念之一,我们将在本章中看到如何以正式化的方式详细说明这一点。三个主要级别是宏观流程、业务流程和程序,但根据上下文,可能还会出现其他级别。

流程的限制

如果你作为一个特定任务中的参与者在一个组织中接触过业务流程,那么你很可能对它们有不好的看法。由于许多实施不当,业务流程一直遭受着坏名声。有很多方法会导致流程偏离重点,造成更多的伤害而不是好处,但让我们从有效的方法开始。如果你想通过流程管理来改善你的组织,最基本的规则是流程应该始终反映现实中的情况。

在流程设计初期,这听起来可能很显然:大多数流程设计者会首先观察组织中的情况来制定流程。然而,还有一些组织领导者认为他们比运营人员更了解工作方式,并创建了一个不符合现实的流程。这当然会导致无用的流程,这也是为什么 Gemba 是精益方法中的重要概念之一,表示价值创造的地方。在工业组织中,这意味着去工厂车间了解真正发生的事情。

另一个谬误是认为,一旦流程建立得很好,改进就会从优化其表示中流出。这是一个常见的顺序:

  1. 流程分析师观察运营团队的工作。

  2. 流程被制定出来,并且正确地反映了实际工作。

  3. 流程分析师在流程中发现了可能的优化点。

  4. 设计了流程的改进版本。

  5. 团队继续使用现有的流程工作,现实中没有观察到任何改进。

这只是一个例子,其中人们忘记了过程应该始终反映现实中的发生情况。过程分析可能会找到一些改进的地方,但实现改进的唯一方法是在运营团队考虑到这一点并自行判断——在其自身组织内部——如何改变其工作方式以避免问题。一旦这样做(而且大多数时候,团队找到的解决方案将与解决方案分析师想象的解决方案不同),过程应该更新以反映运营团队所做的修改,并继续反映它。

最糟糕的情况是,当业务分析师对运营团队拥有层级权力,并试图强迫他们遵循一个纯粹来自分析而非来自运营观察的流程时。除非纯粹随机,否则这种流程不可能产生积极效果并改善人们实际工作的方式。会发生的情况正好相反:与不适应的流程一起工作将降低运营团队的士气,并增加人们绕过流程或甚至通过在流程中找到缺陷并主动采取行动来展示流程有多糟糕的可能性。听起来疯狂,不是吗?然而,这种情况每天都在许多公司发生,仅仅是因为人们以错误的方式使用流程,认为理论上了解它们可以导致“纸上”的改进。在这种情况下,流程会获得坏名声,因为人们认为它们比行动者本身更重要或更正确。

再次强调,流程只能是对真实、具体组织中发生情况的表示。它们可能是一个很好的工具,用于揭示瓶颈、设计解决方案,甚至在某些情况下模拟它们。但唯一真实的现实总是来自工厂车间,流程永远不能超过对人们实际工作的有用表示。

业务流程建模

前一节可能会让你认为流程是一个糟糕的工具,确实,它们通常是这样的。但这并不意味着它们不能被正确使用,并且当这样做时,它们的优点是众多的。首先,它们是团队围绕协调工作进行沟通的一种很好的视觉方式。就像看板是一个共享项目进展共同视图的视觉方式一样,一个制定良好的流程是分享团队如何共同工作的一种很好的方式。当团队围绕流程描述聚集在一起时,几乎从未有过不导致有趣优化的情况,无论是通过更好地共享信息(“我不知道你是做这个任务的人;下次,我会直接通知你这种情况,这可能会影响你在流程中的步骤”)还是通过提出不同的做事方式(“如果我在你完成任务后直接将信息传递给执行者呢?由于他们不依赖于你的输出,他们可以立即开始,总周期时间将会减少”)。

“可视化”、“绘制”、“视觉方式”:所有这些术语都清楚地表明流程应该是一个图形化的现实,猜猜看?我相信你肯定在不知道的情况下已经绘制了许多流程。比如这个简单的图?

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_1.jpg

图 11.1 – 一个极其简单的流程图示例

这已经是一个流程图,即使是一个不可否认的非常简单的流程图:它包含两个任务;它们是协调的(箭头显示第二个任务应该在第一个任务完成后进行);并且它们是为了达到一个目标而完成的,即通过书籍获得报酬。

业务流程建模业务流程管理(你将发现这两个分解都用于BPM的缩写)是关于以某种方式正式化这些流程,使得任何组织流程都可以详细描述,并且流程描述可以用于不仅仅是图形表示,这意味着,例如,关于每个执行者的任务的清晰沟通,变更影响分析,或流程优化等等。当谈到正式化时,你现在应该有条件反射地想到一个规范或标准,这将有助于这一点。好消息是,这些确实存在;坏消息是,它们如此之多,以至于几乎花了近二十年的时间才达到一个单一代码完整且被广泛接受作为 BPMN 参考点的程度。

BPM 标准的历程

在软件文本化流程表示方面有如此多的方法,那些尝试性标准的演变以及它们的合作、竞争和交叉可以表示为复杂的时序图,这些图几乎不可能在一页上显示。您可以通过网络搜索轻松找到这些图,但由于所有这些都是在大约十年前发生的,所以在这里重新呈现它们毫无意义。可能还有一点有用的是,追踪这项工作中的重大里程碑:

  • 2000 年,WfMC 联盟创建了 WPDL 1.0,该设计始于 1997 年。

  • 几年后,它采用了当时的新 XML 方法,创建了 Wf-XML 1.0,随后又推出了一些其他版本。

  • WPDL 本身演变成了一种基于 XML 的语法,称为 XPDL,WfMC 也在此基础上开发了后续版本,到 2009 年达到了 2.2 版本。这导致了一个尴尬的局面,即同一个组织提出了两个标准。

  • 同时,另一个名为 BPMI 的联盟在 2000 年代初 WPDL 发布的同时创建了 BPMN。BPMN代表业务流程建模符号。这个标准本身在 2004 年达到了 1.0 版本,是关于表示任何流程的。

  • 同时,IBM 正在开发 WSFL,在微软和 BEA 的共同努力下,于 2002 年演变成 BPEL4WS。BPEL代表业务流程执行语言,与 BPMN 采取的方法略有不同,因为它强调的是流程的执行而不是其表示。BPEL4WS 将 Web 服务作为流程执行的手段。

  • OMG 是另一个以其定义统一建模语言UML)而闻名的联盟。这个联盟负责 BPMN 的演变,2006 年取代了 BPMI,并在 2008 年发布了 BPMN 1.1。BPMN 与 XPDL 交换了概念,使得后者随着时间的推移变得不那么有用。

  • OASIS 是另一个知名的联盟,它采用了相同的方法来托管 BPEL4WS 1.1 的工作,并在 2007 年监督了其转换为 WS-BPEL 2.0。OASIS 有一个更早的标准,称为 ebXML,它被整合到了 WS-BPEL 2.0 中。

  • 由于缺乏对人类活动的支持,BPEL4People 应运而生,以补充 WS-BPEL 2.0。

  • 2010 年,OMG 发布了 BPMN 2.0,有效地将现有标准中用于业务流程表示的大多数概念整合到一个基于 XML 的语法中。

BPMN 2.0 标准

虽然在 BPMN 2.0 诞生后的几年里,XPDL 继续发展,WS-BPEL 2.0 仍然在使用,但仅用于 Web 服务的流程驱动执行,但 BPMN 2.0 通常被认为是当今流程表示的首选标准。其灵活的方法使其能够模拟任何类型组织的任何人类或机器流程,从而使得在诸如视觉表示、流程优化、转换和监控等格式中应用所有操作成为可能。由于格式非常通用,执行也是可能的,这使得 BPMN 2.0 成为 WS-BPEL 2.0 等专业标准的强劲竞争对手。由于后者还与 Web 服务堆栈耦合,而 Web 服务堆栈在很大程度上被认为过时,更倾向于 REST API 方法,因此,如果需要使用基于软件的流程,学习 BPMN 2.0 标准看起来是非常必要的。

你可以在互联网上找到大量关于 BPMN 2.0 以及如何使用该标准设计流程的资源。如果你需要一个起点,有一张非常好的海报,其中包含 BPMN 2.0 的所有概念,并以单一图像的形式解释它们,包括它们之间的关系,可以在 bpmb.de/index.php/BPMNPoster 找到。没有什么比这张图形表格更清晰、更简洁了,但我仍将提供一些关于 BPMN 2.0 主要概念的简要说明,如下,以便更容易地跟随本章后面的示例。

让我们从可能的最简单的 BPMN 图开始:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_2.jpg

图 11.2 – 最简单的 BPMN 图

它包含一个开始事件、一个任务和一个结束事件。事件已用文本标记,但这不是强制性的,因为它们的表示足以区分它们。不过,任务需要一些文本,惯例是始终使用祈使句形式的动词来描述任务。

该流程的文本表示如下(由 Camundi 设计工具输出,该工具可在 demo.bpmn.io/ 上在线获取,我已用它制作了本书的大部分图表):

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions      id="Definitions_14d2537" targetNamespace="http://bpmn.io/schema/bpmn" exporter="bpmn-js (https://demo.bpmn.io)" exporterVersion="16.3.0">
  <bpmn:process id="Process_0bj1gnx" isExecutable="false">
    <bpmn:startEvent id="StartEvent_1psg9fg" name="Start">
      <bpmn:outgoing>Flow_0xih2cf</bpmn:outgoing>
    </bpmn:startEvent>
    <bpmn:task id="Activity_1tbqy2q" name="Do something">
      <bpmn:incoming>Flow_0xih2cf</bpmn:incoming>
      <bpmn:outgoing>Flow_0v43nfz</bpmn:outgoing>
    </bpmn:task>
    <bpmn:sequenceFlow id="Flow_0xih2cf" sourceRef="StartEvent_1psg9fg" targetRef="Activity_1tbqy2q" />
    <bpmn:endEvent id="Event_08ckjbl" name="End">
      <bpmn:incoming>Flow_0v43nfz</bpmn:incoming>
    </bpmn:endEvent>
    <bpmn:sequenceFlow id="Flow_0v43nfz" sourceRef="Activity_1tbqy2q" targetRef="Event_08ckjbl" />
  </bpmn:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0bj1gnx">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1psg9fg">
        <dc:Bounds x="156" y="82" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="162" y="125" width="24" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1tbqy2q_di" bpmnElement="Activity_1tbqy2q">
        <dc:Bounds x="250" y="60" width="100" height="80" />
        <bpmndi:BPMNLabel />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_08ckjbl_di" bpmnElement="Event_08ckjbl">
        <dc:Bounds x="412" y="82" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="421" y="125" width="19" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge id="Flow_0xih2cf_di" bpmnElement="Flow_0xih2cf">
        <di:waypoint x="192" y="100" />
        <di:waypoint x="250" y="100" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0v43nfz_di" bpmnElement="Flow_0v43nfz">
        <di:waypoint x="350" y="100" />
        <di:waypoint x="412" y="100" />
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn:definitions>

这可能看起来相当复杂,因为基于 XML 的 BPMN 语法相当冗长,但它是最小的文件,因为它表示上面的流程,仅包含一个任务。

注意,文件的前一部分是实际的 BPMN 标准表示,这可以通过使用 bpmn: 前缀来看到,这些前缀与标题中的 www.omg.org/spec/BPMN/20100524/MODEL 命名空间相关联。文件的其他部分,其中标签以 bpmndi 前缀开头,对应于 Camundi 的专有扩展,用于覆盖元素的图形定位。

不深入细节,可以在 XML 表示的第一部分注意到以下内容:

  • 如前所述,表示法清楚地说明了事件和任务是什么,甚至精确地说明了使用了哪种类型的事件

  • 所有实体都接收一个唯一的标识符,这使得它们可以相互链接

  • 流(在视觉图表中对应于箭头)也被表示出来

  • incoming/outcoming 属性和 sourceRef/targetRef 属性之间存在信息重复

在 BPMN 中有一个重要的概念,即活动可能是一个任务,也可能是一个由多个活动组成的子流程本身。这使我们能够实现上面提到的不同粒度级别。因此,在 BPMN 标准中,可以表示一个业务流程,其中活动以非常通用的方式描述,如下所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_3.jpg

图 11.3 – 收缩子流程的表示

如果工具支持的话,图表可以简单地扩展到内容的完整定义,提供以下表示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_4.jpg

图 11.4 – 子流程的扩展表示

在 BPMN 标准中,任务可以用一个图标来装饰,以指定它们的操作方式:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_5.jpg

图 11.5 – 不同类型的任务

事件也可以被专门化,以考虑基于时间、消息驱动或其他类型的事件:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_6.jpg

图 11.6 – 专门化事件的示例

如果一个流程需要多个演员,它们会被绘制成类似游泳池中的泳道:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_7.jpg

图 11.7 – 在流程中使用泳道

在 BPMN 中需要了解的最后一种基本概念是网关。网关可以根据条件推导出流程的流,也可以在工作流程的给定部分中复制序列。以下图表展示了两种主要的网关类型:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_8.jpg

图 11.8 – BPMN 中的两种主要网关类型

左侧展示的第一种网关是排他网关(符号 X),这意味着只能使用一条路径(在我们的例子中,稿件可以被编辑接受或拒绝)。第二种,使用 + 符号,是并行网关,用于在所有任务完成并加入流程继续之前执行多个任务(在我们的例子中,当所有营销活动都实施完毕时,流程达到其结束事件)。

关于 BPMN 还有很多其他需要了解的内容,但本书并不是让你精通使用这种标准格式的场所,所以我会只介绍这些非常基础的概念,这些概念将在之后被使用,并建议你在组织业务流程建模中需要深入了解 BPMN。如果你在某些时候怀疑 BPMN 是否能够正确地表示你的活动,请记住,经过 20 年的专家在联盟中的努力,最终创建了一个全球标准,这个标准被认为能够形式化几乎任何可能的人类和计算机化任务的组合。有些可能很难设计,但这总是由于对标准的了解不足。通过一点实践,你将能够使用 BPMN 建模任何事物,这当然会给你的信息系统设计活动带来很多价值,因为这意味着其中的所有功能活动都将被详细和形式化,这将极大地有利于 IT 的寻求对齐。

基于软件的业务流程执行

正如所解释的,使用 BPMN 标准本身就已经具有很大的价值:仅仅使用一种形式化的方式来表示你的业务流程,就会为你提供深刻的见解,并引发你可能未曾考虑但一旦信息系统建立时如果没有考虑它们可能会成为重要问题的疑问。但 BPMN 的另一个附加价值是,一旦建模,就可以借助软件执行你的流程,因为表示的形式化使得机器能够解释这些流程,甚至可以自动执行它们的实例。

在本节中,我们将解释 BPMN 背后的原则,并给出一些与我们的示例信息系统相关的示例,以更好地理解这些原则。然后,我们将解释 BPMN 图是如何根据角色分解的,以及我们可以使用什么类型的软件来建模和运行它们。最后,我将简要解释为什么 BPMN 在软件行业中没有得到更广泛的应用。

原则

业务流程执行的原则非常简单:一个名为 BPM 引擎的软件读取基于 XML 的 BPMN 合规的业务流程表示,可以启动你想要的任意多个“实例”。一旦启动,一个实例将大致具有以下行为:

  1. 实例被保存在磁盘或数据库中,实例可以在它们发展的不同步骤中被读取和修改,这些步骤对应于构成流程的任务的进展。

  2. 每个给定过程的实例与其他实例完全分离,尽管它们执行的是相同的过程定义。实例中过程的执行方式可能会根据网关的通过方式而根本不同。

  3. 一旦启动,实例将遵循其创建时刻设计的流程。如果流程设计之后有所演变,所有正在运行的实例将继续使用旧版本,以保持工作流程的一致性。

  4. 流程中的每个任务都是由引擎“执行”的。实际执行步骤取决于其类型以及引擎如何配置来处理它:

    • 当达到服务任务时,执行应该是自动的。可以调用 API 或连接到应用程序等。

    • 当达到手动任务时,引擎会提醒用户需要他们完成某些操作。这可以通过电子邮件或其他渠道的通知来完成。当用户完成任务后,他们通常会被邀请通知 BPM 引擎,以便流程实例的执行可以继续。

    • 当达到用户任务时,用户也会被告知,但由于预期的操作是填写表格或至少在机器上实现某些操作,可以提供一个指向所需表格的指针,以加快操作速度。

  5. 如果引擎遇到网关,它将根据其类型以不同的方式反应:

    • 如果这是一个并行网关,所有后续任务都将运行,并且引擎将负责等待所有路径完成后再运行流程的其余部分。

    • 如果这是一个排他网关,决策引擎(我们将在下一章更详细地讨论这一点)将被激活,业务规则的执行将定义应该采取哪个分支。

  6. 在一些高级引擎中,可以设计通知,以便过时的流程实例向功能管理员发出警告,以便他们完成任务。

  7. 当达到结束事件时,流程实例被视为完成并归档。在其中无法执行任何操作,但它被保留用于统计或可追溯性原因。

将应用于我们的示例信息系统

理解技术的最佳方式是通过示例,这就是为什么我们从本书的开头就遵循了一个信息系统设计的示例。让我们回到DemoEditor,看看可以设计哪些业务流程——也许甚至可以自动化。

第一个示例将展示流程在其执行过程中如何积累数据。毕竟,IT 系统中的流程大多数时候都是关于创建或收集数据。一本书是一份数据,销售也是数据,即使它们的主要目标是为公司带来金钱,等等。流程可以被视为一系列创建(或不创建)数据的任务。到流程结束时,已经创建了足够的数据或检索到了数据,以实现流程的目标,至少在其实例中是这样。在下面的示例中,我们从一个编辑那里请求信息,并要求作者完成这些信息,因为流程的目标是发布关于新作者的完整信息:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_9.jpg

图 11.9 – 为作者注册的 BPMN 示例

流程应该是自我解释的,所以我们不会提供任何细节。DemoEditor 的第二个业务流程示例如下:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_10.jpg

图 11.10 – 为合同签署的 BPMN 示例

这次,数据的收集可能不像之前的过程图那样明显,但我们仍然可以这样思考流程:

  • 第一个任务收集一些数据,即所选作者的标识

  • 第二个流程会创建一些数据,因为合同草案将是一份文档,因此构成了电子数据

  • 第三个任务可能不会产生功能性数据,但作者下载合同草案以供签署这一简单事实本身就是一个信号,并在信息系统中产生数据(即使非常简单,如日志)。

这些流程在示例软件中的执行方式将是 第十七章 的一部分。在本章中,我们只展示它们作为使用 BPMN 可以做什么的示例,以及为什么 BPM 引擎是本章中描述的乌托邦信息系统架构的三个部分之一。它们也将在本章的其余部分用来说明关于流程执行的一些观点。

链接到用户

现在是回到一个之前有点过于迅速覆盖的概念的好时机。当谈到如何将包含多个参与者的复杂图切割成流程所代表的“池”中的“泳道”时,引入了“参与者”这个概念来解释每个泳道都必须与一个“参与者”相关联(并且按照惯例,以该“参与者”命名)。这听起来像是在谈论用户,但参与者比这更通用,应该与一组用户相关联(有些人可能会称之为配置文件,即使这个词通常用来指代授权的紧密集合,如在基于角色的访问控制范式中的“角色”语义)。

在前两个 BPMN 图表示例(图 11.9图 11.10)中,参与者是 EditorAuthor。在用户目录中,通常可以找到具有等效名称的组。一个好的 BPMN 引擎总是包括支持用户组的用户目录 – 或者甚至更好,可以通过例如标准化的 LDAP 协议连接到一个中央企业目录。这允许在谈论功能流程时进行一定程度的间接引用,即我们指的是哪个特定的作者或编辑并不重要。在第一个示例中,某个编辑将收到一份注册协议,然后向某个作者发送信息请求。

正确理解这个概念很重要,不要过多地将它与用户的目录耦合。将 BPMN 参与者/泳道与一个组关联是最合理的做法,但这种耦合不应过于紧密。例如,如果某个时候决定只有少数高级编辑可以启动合同,流程引擎应该能够实现这一点,而无需依赖于用户目录为这些特权编辑创建一个新的组。当然,如果这种情况更加优雅,但这再次说明,技术前提条件永远不应该阻碍功能请求。

从参与者的选择角度来看,一个流程实例化的时刻同样重要。在“泳道”中,哪个具体用户将对应于一个通用定义的参与者,通常是在 BPM 引擎中实例化流程时实现的。大多数引擎都会显示一个对话框,询问用户目录中谁将与此或彼参与者/泳道在流程池中关联。有些甚至允许定义默认用户分配或根据其他上下文元素选择用户的规则。例如,我们可以有一个DemoEditor流程,当作者发送其稿件进行审阅时,它会自动选择与该作者关联的编辑。

可用于 BPMN 编辑和执行的软件

在这样一本书中,试图保持通用性并推动标准而不是实现,我们不太可能找到供应商的例子,但 BPM 引擎在行业中的应用如此稀少,我认为引用一些例子可能是有用的。当然,我只会列出那些遵守规则并努力支持 BPMN 2.0 标准的供应商,而不是通过提供甜蜜但专有的功能来试图将客户锁定在供应商身上。以下是一些 BPMN 引擎(其中一些包括图形编辑器):

市场总是在变化,新的版本和功能不断出现;这就是为什么我不会推荐或比较这些解决方案。当然,还有许多其他我未曾接触到的解决方案。这份列表仅仅作为一个起点。

为什么在行业中 BPMN 的使用如此之少?

正如我们所见,规范和标准已经存在了很长时间,BPMN 引擎也已准备好,可供全球使用。然而,BPMN 在行业中的整体使用非常有限。除了少数非常大的公司,并且仅限于特定的区域,基于 BPMN 的自动化流程的使用非常稀少,尽管在规范的价格或复杂性方面没有问题。事实上,许多实现都是免费的,规范也相当容易学习,即使是对于非技术人员来说也是如此。实际上,它可能是面向商业人士最容易理解的标准之一,因为它代表了他们的日常工作(至少当正确使用时)。那么,为什么这么少的人使用它呢?

可能的原因之一是,在许多情况下,自动化一个流程并不带来太多的价值。确实,设置一个 BPM 引擎是一项相当复杂的任务,只有在以下情况下才值得投资:要么是流程的复杂性很高(由业务专家绘制的 BPMN 流程图,由软件“盲目”执行),要么是流程定义频繁变化(这使得将执行委托给通用引擎变得有趣,因为这将允许软件的其他部分保持稳定)。当你这么想的时候,许多业务流程并不是那么频繁地变动。发送发票的过程不会每个月都改变,所有与法规相关的流程都倾向于相当稳定。甚至运营流程的变化频率也不会比应用程序的新版本发布频率高多少。

另一个原因是,在 BPMN 文件中设计流程往往会使其变得僵化。在许多组织中,流程并不那么清晰,很大程度上依赖于人类如何执行它们,有时并不遵循共同的做法,而大多数时候是找到一种创造性的方法来实现流程目标,而这最终是唯一真正重要的事情。当然,这可能会让一些喜欢流程带来的控制感的经理感到不满。但这也是本章先前解释的流程的缺点之一:当它们倾向于取代人类选择时,它们不仅会降低团队士气,而且在试图提高生产力的同时也会降低生产力。

或者,BPM 引擎的低使用率可能仅仅是因为 BPMN 仍然有点复杂。当然,BPMN 2.0 标准的基礎非常容易理解和应用(每个任务一个框,每个参与者一个通道,任务之间的箭头显示活动流),但规范的其他部分可能需要更多的智力投入。

总的来说,BPMN 2.0 并没有像它应该的那样被广泛使用,因为它如果被更好地了解和普及,确实可以为 IT 行业带来更多价值。这就是为什么我们需要尝试其他解决方案,这也是本章最后一节要展示一些可能用作 BPM 轻量级替代品的替代方案的原因,希望它能给 BPM 带来新的推动力,尽管它不是基于 BPMN 2.0 标准。但在那之前,我们将稍微偏离一下,讨论一下可以使用 BPM 执行的其他操作——流程执行无疑是其中最先进的,但不是唯一能带来运营价值的操作。

其他相关实践

流程的自动执行是理想状态,但 BPM 还带来了大量其他优势,其中一些更容易获得。本节概述了其中一些可能性。

业务活动监控

业务活动监控BAM)是使用 BPMN 流程表示法从业务流程实例中提取关于序列流的统计数据,当然,也可以从中获得对流程所表示活动的洞察。以下是一些基于此类统计问题的例子:

  • 在流程中,哪些任务耗时最长?

  • 流程平均需要多少时间?

  • 这个平均时间是否会以规律的时间相关模式变化?是否存在某些季节使得这个过程更快/更慢?

  • 流程中的循环时间和提前期是多少?

  • 部署流程新版本与执行时间的变化有什么关联?

  • 某项任务的自动化是否确实提高了整个流程的生产力?

除了自动执行的承诺(这带来了可重复性和一致性)之外,BAM 是 BPMN 中最受欢迎的功能,它吸引了管理者。背后的原因是管理者需要指标来了解他们的业务行为。他们还能有什么比直接由他们的业务流程产生的指标更好的指标呢?

如果您已经使用监控系统,BAM 的实施实际上相当简单。在这种情况下,只需在任务的每个输入和输出上添加日志,然后使用您的聚合机制来推导出所需的统计数据。此外,在流程的图形表示上显示这些值是必须的,以便使它们易于理解。基于时间的统计数据可能很有趣,但有时,只知道某个特定任务执行了多少次,就能为流程带来知识。

例如,假设我们给DemoEditor业务流程的第二个例子添加计数器:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_11.jpg

图 11.11 – BAM 的示例

计数器显示在任务的底部,应按以下方式阅读:

  • 100 个流程实例已被启动

  • 98 个已通过第一个任务,2 个仍处于作者尚未被选中的状态

  • 88份已通过第二项任务,10份仍处于合同起草状态(占上一任务的98份)

  • 88份合同已发送给作者,所有消息都已确认

  • 75份合同已被作者下载;13位作者已收到消息但未处理

  • 在这75份中,15份已拒绝合同,60份已签署并发送了批准消息

基于时间的统计信息位于任务的最上方。在这个例子中,它们只针对任务,而不是转换。格式首先显示平均时间,然后是括号内的最小 -> 最大范围。这种统计信息可能允许我们计算在时间间隔内发送了多少合同;签了多少并已返回;平均起草合同所需时间与作者签署它所需时间相比;是否应该使用提醒来加快流程,或者作者快速签署合同会导致时间浪费;等等。

BAM 对于在流程中找到瓶颈也很有帮助。有时,流程停止的地方很容易找到,但在角色分散的大型组织中,授权可以委派,涉及许多步骤,有些步骤在不同的服务中,涉及其他经理,并且可能存在政治因素,所以如果你不使用 BAM,找到整个流程突然下降的原因可能会很令人沮丧。这就像在没有良好的监控系统的情况下,在分布式云应用程序中找到错误几乎是不可能的。

最后,BAM 可以用来找出是否确实遵守了推荐流程。当流程实施(当然,与使用它的团队一起),可能会发生新来的人不知道它,并试图跟随他们领域资深人士的领导。他们可能会错过流程的一些步骤,有效的监控可以帮助他们或流程所有者发现这些失误。补救措施只是简单地指导人员如何执行任务,但可能有趣的是采取更深入的方法,并开始对流程本身进行持续改进:为什么人员没有正确了解流程?应该有什么可以防止这项任务被遗忘?既然显然可以不这样做,那么这项任务是否应该完全自动化,以确保将来没有人忘记它?所有这些问题都将改善流程的工作方式,但在这个例子中,触发因素是 BAM。

业务流程模拟和优化

BPM 的另一个用途,虽然不太为人所知,但在某些情况下非常有价值,是模拟流程的可能执行,以找到材料资源、工具和人员之间的良好平衡,并优化整体。例如,想象以下业务流程:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_12.jpg

图 11.12 – 业务流程优化的基础

如果您需要处理大量的 incoming letters 并需要找到一种平衡执行五个任务的人员的方法,那么拥有这张图可能会特别有用。让我们从一个过于简化的假设开始,即您的团队中有十个人,每个人都能执行任何一项任务,并且每项任务所需的时间与其他任务相同。您可能会认为合理的人员分配是每个任务两人……但再想想!发送回复信件这项任务必然比前两个任务多调用两次。您也不知道有多少比例的邮件会发送到更新联系表单这项任务:如果没有任何邮件发送到那里,那么您将节省一些时间,将最初受此阶段影响的两个人分配到两个其他子团队。

在保持上述假设的情况下,一旦您有了邮件类型的统计分配,您可能能够计算出您应该如何在不同任务上分配人员,以优化整个流程,只需一个简单的计算器即可。如果您开始引入更现实的行为,比如不是所有任务都需要相同的时间,那么您肯定需要电子表格或至少一些脑力。

现在,假设 incoming mail 的份额会季节性变化(例如,地址变更在年初和九月更为频繁);所有任务都有一个给定的平均时间,但有些任务的波动范围要大得多(这意味着它们可以显著地围绕平均时间变化);某些人可能能够处理某些任务而无法处理其他任务,这取决于他们的能力;您必须考虑到有人生病的可能性,或者人们休假的事实,尽管由于团队内部规则,并不是每个人都同时休假……所有这些都使得系统如此复杂,以至于您无法确定不同任务的最佳人员分配。幸运的是,BPMN 表示法可以帮助您通过模拟不同的团队组织和数千个流程实例,然后根据您的标准确定总持续时间并选择最佳配置,从而简单地获得最佳结果。

基于大人群的优化应用已经存在(例如,使用类似遗传算法或蒙特卡洛方法),但它们都需要某种快速模拟系统如何响应的东西:这就是一个好的 BPMN 引擎可以帮助的地方,因为它可以执行纯自动化的任务,这些任务可以随机模拟所需的时间。因此,优化引擎将能够模拟大量情况,将它们发送到 BPMN 引擎进行虚拟执行,收集结果,并最终收敛到最佳解决方案。

业务流程挖掘

最后,即使业务流程挖掘不是 BPMN 的使用,而是一种可以成为业务流程来源的活动,我们也应该快速解释业务流程挖掘。业务流程挖掘(不缩写以避免与业务流程管理混淆)是通过分析通常来自软件的其他数据(通常是日志)来确定业务流程。

例如,一个流程挖掘系统可以使用出现在网站上的日志,以及发票和库存/配送指标的历史表,以便确定与电子商务商店上“正常”购买行为相对应的标准流程。

BPMN 有许多其他应用,但我们不应该偏离我们的目标太远,我们的目标是展示业务流程管理如何帮助实现业务/IT 对齐。我们已经看到流程自动化是 BPMN 带来最大价值的使用之一,但遗憾的是,投资可能相当高,因此这种方法在工业应用中并不常见。一些替代方法可能有助于在保持所需投资低的情况下变得更加流程导向,甚至在某些情况下不需要比通常的商业应用更多的额外投资。

业务流程实现的其它方法

在本节中,我们将考虑所有提供替代 BPMN 引擎执行业务流程的方法。我们将发现哪些是更常见/更现代的,并将比较它们的效率。最后,一切取决于上下文,但了解具体细节应该有助于你了解何时应用这种方法或那种方法。

图形用户界面中的流程

你可能没有意识到,如果你曾经创建过软件图形用户界面(GUI),那么你很可能在不经意间实现了一个流程。例如,所有向导都是流程,因为它们通过链接屏幕来提供按顺序添加数据的方式。最复杂的那些允许选择,这在 BPMN 中是网关的完美等价物。它们也有开始和结束,就像任何流程一样。向导与流程非常相似,但当我们简单地将业务流程定义为一系列旨在达到目标的人力和自动化任务时,那么任何 GUI 实际上就是一个流程。

每个图形用户界面(GUI)都允许人类交互,这通常是简单过程的开始。这个过程将通过表单收集数据,然后通过调用后端来执行一些“服务”任务,以执行一些命令。就像向导一样,GUI 的行为将根据在表单中指定的业务规则或值(再次,就像网关一样)而改变,并且过程的结束通常由一个吐司通知、一个对话框,或者简单地由 GUI 等待另一个交互来表示。

你可能会争辩说,GUI 中的过程实际上是一个人类过程,其中用户在软件中遵循一个过程。然而,一个好的 GUI 会引导用户以特定的方式使用它,这往往倾向于模仿业务流程。这在向导的情况下很明显,但在设计有 UX 能力的 GUI 中也会发生。当然,在像命令行这样的最简单界面中,过程执行的大部分——如果不是全部——都在用户手中。但简单的事实是,参数的命名是根据 BPMN 过程收集的数据来进行的,这已经在尊重所表示的工作流程方面提供了一些帮助。

高级 API 中的过程

当我们考虑解决编排问题的简单方案时,拥有实现一系列对其他更简单 API 调用的有序序列的专用 API 也是一种已记录的方法。实际上,这是一种众所周知的 API 结构,将它们组织成三层,每一层都建立在下一层之上:

  • 第一层是 CRUD API,用于操作和读取单个业务实体。这是我们之前章节中在解释 MDM 概念并展示如何使用 REST API 实现时讨论的那种 API。

  • 第二层是关于那些将多个第一级 API 调用组合起来以在系统中实现复杂操作的 API。例如,这样的 API 可以公开为/api/contract,其POST动词实现可以调用/api/authors以验证作者是否已经注册,然后在这种情况下对该 API 进行POST调用。之后,代码会调用专门用于提出合同金额的服务,然后最终到达/api/pdf-fusion服务以检索创建并发送到电子文档管理系统(当然,使用 CMIS 标准)的文档地址。在任何发生失败的地方,这种实现都会有规则知道它应该做什么,在最坏的情况下,会向人类提供通知以清理计算机难以解决的过于复杂的情况。

  • 第三层用于调整 API 以适应其调用者。这一次,它们不一定组合多个调用,而是向请求添加一些参数,调整和过滤一些响应内容,等等。这些三级 API 通常用于提供“前端的后端”,例如,调整移动应用程序检索的默认分页和属性集。

当使用 API 网关包括诸如身份验证、授权、速率限制和为发票计数等通用功能时,所使用的服务器可以被视为另一个级别,但它并不位于上述三个级别之上。由于它可以用于暴露任何级别,更好的表示方法是将它显示在面向业务 API 的三个级别旁边,为它们提供技术覆盖:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_13.jpg

图 11.13 – API 的三个级别

如您从该方案中选择的示例中可以看到,不同级别的 API 不一定使用不同的表述。例如,可以决定暴露作者及其过去书籍的 API 使用/api/authors路由,就像仅暴露作者本身的 CRUD API 表述一样。这样做的一个理由是尊重开放数据协议标准,特别是适用于此情况的$expand语法。尽管如此,该 API 仍将是第二级 API。

这种方法在 API 内部实现业务流程的一个优点是它极大地尊重了单一责任原则。一个缺点是实施将要么在代码中,这更难进化,要么使用 BPMN 引擎,但在这种情况下,这会创建一些与技术的耦合。当然,在选择 BPMN 引擎时会有一些耦合,但在许多 API 实现中使用它无疑会增加对它的这种依赖程度。

现在我们已经展示了两种“简单”的软件中实现业务流程的方法,让我们分析一些更复杂的方法。我们将从专门的中间件服务器开始,我们之前在面向服务的架构的背景下讨论过这些服务器。

MOM/ESB 中的流程

第八章中,引入了中间件的概念,并介绍了一些著名的实现,包括面向消息的中间件MOM)和企业服务总线ESB)。当然,这些可以用来实现流程并编排不同消息或服务调用,从而实际实现特定业务流程的任务。尽管 MOM 和 ESB 并不理解 BPMN,但使用企业集成模式EIPs)足以使业务流程具体化,然后在中间件中执行和监控。

你可能会看到这句话,正如我建议在中间件中引入业务功能一样,而我在第十章中提到,所有业务规则都应该始终位于与承载它的实体关联的 MDM 服务中。这里是否存在可能的悖论?实际上,如果你利用单一责任原则,就不会存在悖论。当谈论某个服务负责的业务功能时,重要的是服务应该明确对每个功能负责。例如,如果一个会计服务需要一本书的净价,而图书服务只包含不含增值税的原始价格,那么在中间件中简单地应用增值税率以节省时间并避免更改和部署新的图书 MDM 服务版本,这并不是一条简单的道路。图书及其所有属性显然属于图书参考服务的责任,必须将另一个属性添加到它所公开的列表中(这不会损害向后兼容性,因此如果客户端编写正确,不应有任何影响)。

另一方面,假设编辑决定一本书应该停止出版,那么命令应该从 CRM 和商业网站上删除该书的引用,同时也应该将该书在图书参考服务中的状态设置为存档。显然,该操作首先会发送到图书参考服务,但谁应该决定其他操作呢?我们可以要求数据参考服务向 CRM 和网站发送消息,但这会与这些应用产生明确的耦合。CRM 和网站都不应该控制这种交互,因为它们都不应该对图书实体级别的发生的事情负责。这个命令组的责任不是唯一的,因此很难将其分配给单个服务。

中间件解决这个问题的方法是通过提供另一个负责这些“编排”任务的应用程序。它位于消息之上,只负责处理和路由消息到需要它们的任何服务。请注意,中间件应用程序对消息的内容一无所知;它只是确保对/api/books上的DELETE操作应该发送到图书 MDM 服务、CRM 和网站。它们如何处理这些消息不是它的业务。当然,有一些细节需要解决。例如,如果其中一个服务发送错误,中间件应该如何反应?这是它的责任取消交易并要求其他服务回滚它们所做的一切吗?这些问题将在本节稍后部分进行讨论,但在此期间,请记住古老的谚语“哑管道,智能端点”:中间件永远不应该嵌入任何除了纯粹编排之外的业务规则,即简单的消息分发,不再做其他事情。

流程的低代码/无代码方法

你最近很可能已经听说业界关于低代码/无代码运动的讨论。这些方法背后的理念是,专门的平台可以使非开发者通过移除大部分或所有代码,并提议使用可视化编辑器来创建表单、工作流、数据结构等,从而能够创建业务线(Line-Of-BusinessLOB)软件应用。从某种意义上说,它们包含了创建完整系统所需的一切,就像我们之前讨论的理想化系统一样。区别在于,它们通过图形编辑器来实现,用户根本不需要输入任何基于文本的代码(或者在低代码方法中几乎不需要,与无代码方法不同,无代码方法要求不输入任何代码)。

这些方法和它们相关的平台周围有很多争议,有些人把它们呈现为一场革命,允许“公民开发者”的出现,而其他人则解释说代码逻辑和算法仍然存在,只是以非文本的形式存在。对他们来说,这些平台不过是一个老承诺的新化身,这个承诺已经超越了代码生成、第四代框架、之前的图形集成开发平台方法……以及当然,所有使用最灵活的工具(Excel)创建的应用程序。作为一个架构师,我试图远离这些观点,专注于这些工具可能带来的价值。

尤其是更好的工具可能是对上述行业中对 BPMN 引擎有限使用的困难的一个答案。就像 MOMs 一样,BPMN 引擎是相当复杂的系统,需要一些设置、维护和专业知识。由于其专注于非开发人员(我差点写“非技术人员”,但这过于牵强,因为使用它们肯定需要技术导向的思维)的易用性,也许低代码/无代码工具可以提供一种易于使用的编排方式,从而使业务流程执行方法得到更广泛的应用?

这可能以两种不同的方式发生,具体取决于一个人使用的工具类型。第一种工具族是用于流程自动化的最容易使用的:即数据驱动的平台。由于理想的信息系统明确区分了主数据管理(MDM)和业务流程管理(以及业务规则管理),这听起来可能有点奇怪,但具体化的概念将有助于打破这种分离。单词“具体化”意味着将某物转化为一个具体实体,通常是两个实体之间的关系。在流程的情况下,它们可以被看作是一系列有助于获取数据的任务,但我们可以应用具体化,并考虑流程本身的一个实例也是数据。这就是数据驱动的无代码系统,例如 Airtable,如何处理业务流程:它们只是在其数据结构中存储有关流程的数据,每一行对应于流程的一个实例。此外,由于大多数简单的流程主要针对一个业务实体,这意味着流程和相关的实体可以简单地转化为同一个实体,由 Airtable 等实际的主数据管理(MDM)来管理。

例如,让我们以人力资源的入职流程为例。目标实体是员工,这与入职流程有关。因此,我们将简单地为这些入职员工创建一个数据结构,并用更面向流程的数据来完善它,通常是入职日期(流程的开始),完全融入日期(入职流程的结束),新员工第一天上班时拍摄的照片的 URL,可能是指向他们需要批准的 IT 图表签署文件的指针,等等。正如你所看到的,流程数据和关于员工本身的数据有时界限模糊。例如,加入公司的日期是入职流程的开始,但这个日期在入职流程结束后对员工来说仍然是非常重要的数据,因为人力资源部门用它来计算这个人将获得多少额外的假期(取决于他们在公司工作的时间长短)。

还有一类工具可以被归类为“无代码”,因为它们只允许图形化操作:这些是轻量级编排工具,如 Zapier、IFTTT 以及许多类似的使用方式。这些平台允许我们通过将事件(例如,当 GMail 账户收到带有附件的电子邮件)绑定到动作(例如,将文件存储在 OneDrive 账户的指定目录“图片”下)来创建简单的交互。创建这些交互的 GUI 可以更进一步,例如,允许一个中间任务根据文件扩展名过滤文件,如果检测到的附件不以 .png.jpg 结尾,则停止。但这通常将是您能拥有的最复杂的使用方式。这种限制通过提供大量连接器到第三方平台得到补偿。我在例子中提到了 Google Mail 和 Microsoft OneDrive,但还有数百或数千个编辑器已经通过这些工具使他们的应用程序可访问。公开 API 通常是这样做的先决条件,我们很快就会看到 webhooks 在这里也非常有用。

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_14.jpg

图 11.14 – Zapier 示例

一些平台如 Microsoft Power Apps 在保持将业务事件与动作关联的相同方法的同时更加复杂。简单来说,它们使得添加中间过滤器、复制消息等操作变得更加容易。它们可以被视为在功能方法中实现 EIPs,但由于它们不遵守模式名称,因此不符合这一条件。尽管如此,它们的一个优点是,EIPs 的实现是用 Java 或技术领域特定语言DSL)编写的,这两种都是代码,需要真正的开发者参与。我们不要认为用视觉图表编辑器替换文本会彻底改变实现“流程”(有时这样称呼)所需的技能:需要开发者导向的思维来正确设置 Microsoft Power Apps 工作流。这样,这类工具实际上是低代码而不是无代码,因为其中一部分,如复杂的属性映射函数,涉及编程语言。

为了结束本节,只需知道低代码/无代码工具可以是非常好的工具来实现 MDM,也可以是 BPM 和 BRMS。使用它们很容易创建一个信息系统,但请注意:与平台的技术耦合可能非常高。如果你的目标是设计一个工业级、长期演进的信息系统,最重要的方面始终是业务/IT 的协同,技术耦合可能会让你错过一些重要的事情,并降低你系统的性能。尽管如此,它们可以是非常好的工具来原型化编排或实体的定义。而且,如果你在合同优先的 API 背后保持服务之间的清晰分离,这些低仪式的工具可以作为一个“哑管道”的实现,而 API 实现则是“智能端点”。

舞台编排而非编排

到目前为止,我们只讨论了在软件方法中执行业务流程时使用编排:在每一个公开的实现中,某个东西(一个中间件、一个 BPMN 引擎或一个低代码平台)处于游戏中心,从一侧接收消息并查看事件,从另一侧向服务发送命令。当这个中心路由器失败时,这种情况会发生什么?由于它是一个单点故障SPOF),整个系统都会停止,这当然是一个问题。有些人会争辩说 ESBs 有分布式代理,并且网络故障可以通过广播方法处理,即使在技术事件发生的情况下也能传递消息。但功能逻辑仍然是集中的,如果路由设计不当,可能会影响整个系统。

为了说明这一点,想象你有一个以下要自动化的流程(我们只表示 CIGREF 地图的前三层,因为硬件层在这里不会改变任何东西):

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_15.jpg

图 11.15 – 自动化流程示例

在分析由 SimpleOCR OCR 化的来函内容后,二进制文档存储在 EDM 中(例如,Alfresco 的社区版本)。如果需要签名,将调用签名簿(可能由 iXParapheur 软件实现),最后,签名的文档也发送到 Alfresco 存储,与未签名的版本一起。

如果我们使用编排方法,将一个 BPMN 引擎,例如 Kogito,添加到软件层(及其在 BCM 中的功能)中,并且与业务流程对应的文件也在同一层,因为这是一个软件工件。然后,当流程实例运行时,Kogito 将调用所有需要的函数:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_16.jpg

图 11.16 – 通过编排自动化

很容易看出,如果proc1.bpmn文件存在问题,整个流程将会崩溃。这就是单点故障(SPOF)对一个组织造成的危害。但从演变和 SPOF 的角度来看,情况可能会更糟:想象一下,如果我们不是为 BCM 中的不同功能选择最佳应用,而是选择了“完全集成”的方法,使用 SharePoint 来存储文件(以下图中标记为Docs),其 OCR 功能,以及用于工作流(以下图中标记为WFW)的功能。结果将是以下高度耦合的:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_17.jpg

图 11.17 – 集成编排的更高耦合

在这种情况下,如果 SharePoint 出现故障,不仅流程会中断,而且该服务器实现的所有功能也会受到影响。由于它们很可能被组织中的许多其他业务流程使用,因此 SPOF 现在所承担的风险比以往任何时候都要大。当然,Microsoft 对 SharePoint 365 有一个非常健壮的实现,但你可能会失去互联网访问。如果你认为运行 SharePoint 本地会更好,那么再想想,因为你永远无法达到 Microsoft 为其自身解决方案提供的鲁棒性水平,无论你的管理员多么有才能。那么我们如何才能消除流程执行中的这个 SPOF 问题呢?

对于这个问题的一个激进答案是简单地消除所有集中式权威,只保留消息在服务之间流动所绝对必要的部分,即网络连接。这听起来可能相当严厉,但毕竟,如果我们真的想要“愚蠢”的管道,它们还能比简单的 TCP/IP 数据包更愚蠢吗?HTTP 以及特别是 HTTPS,将添加一些受欢迎的低级功能,如流加密和收据确认,但它们将完全从业务角度保持中立,这正是我们所说的“愚蠢”。

通过设置所谓的“编排”来消除任何集中的编排。在编排方法中,就像在音乐乐团中一样,有一个领导者从物理集中的位置指导所有乐器的节奏和音调。在编排中,一群舞者不跟随一个单一的领导者,而是通过观察他们的邻居来调整,就像鸟群或鱼群一样。例如,当向左移动时,舞者会盯着他们左边邻居的脚;然后,当向右移动时,他们会与另一边的舞者同步。在这些群体中,没有“主要舞者”,而只有一群习惯于共同工作的舞者。顺便说一句,这意味着仍然存在某种类型的领导者:当学习他们的编排时,编舞者会向舞者群体解释预期的动作,展示他们如何同步,等等。练习之后,一旦舞蹈“投入生产”,团队就不再需要编舞者。这在 IT 编排中也是一样的:你需要一个架构师告诉每个服务它们应该监听什么信号以及它们的反应应该是什么。但一旦设置完成,系统就会自行运行,架构师只需简单地监控一切是否按预期进行。

在我们的例子中,为我们的过程实现这种编排方法将如下进行:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_11_18.jpg

图 11.18 – 低耦合编排

简单来说,将不会有任何额外的软件,因为每个应用程序都会关注其他应用程序的事件。因此,将不会有任何可能的单点故障。为了实施此过程,“注册”将如下所示:

  • EDM 会等待来自 OCR 的信号,表明新文档已被分析。收到此信号后,EDM 会存储该文档。

  • 签署簿会等待一个信号,表明新文档已存储,并会使用业务规则来筛选需要签署的文档。这可以通过两种方式完成:

    • 签署簿可以获取所有文档,并根据自己的元数据自行决定文档是否需要签署。

    • 更好的是,如果 EDM 应用程序支持此功能,它可以通过注册一个 EDM 可以应用的“过滤器表达式”来告诉此应用程序仅通知特定文档。这将减少带宽并提高性能,因为只有实际需要签署的文档才会被通知给签署簿进行处理。

  • EDM 还会等待来自签署簿软件的信号(就像它会等待来自 OCR 平台的信号一样),并在发生此类事件时存储签署的文档。

没有单点故障(SPOF)的伟大之处在于,所有不受故障影响的部件将继续在系统中工作。例如,如果由于某种原因,签名簿软件不可用(比如说它是一个 SaaS 应用,你的互联网连接中断了),其余的过程将正常工作:不需要签名的文件将直接发送到 EDM 并存储;问题只在于需要签名的文件将不会被展示出来进行签名(但它们仍然以原始形式存储在 EDM 中)。在本节稍后,我们将看到我们甚至可以建立一个“安全网”,以确保事件不会丢失,并且待签名的文件最终会在系统恢复在线时到达签名簿。

实现编舞

实现这种编舞方法最逻辑且耦合度最低的方式之一是使用 webhooks。我们已经在第八章中讨论了 webhooks,并看到它们是反转服务顺序的绝佳方式。在编舞的背景下,webhooks 是消除所有集中式编排的绝佳方式,因为责任只由两个组件分担:发射器和接收器。发射器可以存储对某些事件的回调请求,并在其服务中发生业务事件时,负责将这些消息发送到这些回调 URL。另一方面,接收器需要注册它想要了解的发射器事件,提供一个回调 URL,并监听它,一旦消息到达,就要处理它。几乎所有的事情(我们稍后会看到一些事情确实是缺失的)都由这两个参与者处理。

那么,为什么像 Zapier 或 IFTTT 这样的平台存在呢?你可能会问。简单来说,是因为 webhooks 和业务事件还没有标准化。OpenAPI 3.0.2 支持 webhooks 定义,但尚未完成,目前很少有编辑器支持它。而且,定义技术事件的标准需要很长时间,更不用说以业务为导向的事件了。Zapier 和类似的产品为游戏带来的是成百上千的 LOB 应用的专有连接器的集中市场,这就是它们仍然在游戏中的原因。它们的价值也可能在于简单的流程,这些流程只需要将 webhook 插入 API 即可实现,因为在中间使用它们可以为你提供监控、错误检测、自动重试以及持久性故障的通知等。

但在纯粹的理论上,通过在信息系统中的所有服务上注册 webhooks,告诉它们调用其他服务公开的 API,就可以实现你的事件驱动架构(EDA)(这是定义这种通用方法的公认术语)。然而,这要求所有消息都必须标准化,并且存在一个全球性的约定来定义所有可用的业务事件。

总的来说,魔鬼藏在细节中,在这样的理想 EDA 系统中,一切都会运行得很好,直到某个数据包被愚蠢的管道丢失,或者网卡出现故障。由于一切都在同步和内存中,这样的技术事故会导致功能损失,可能对业务产生低到灾难性的影响,具体取决于丢失了什么。这当然是在工业级系统中无法容忍的事情,也是为什么对于重要的数据流来说,保留某种中间件,比如消息队列系统,是一个好主意。然而,原则是保持管道的愚蠢,这很困难,因为一旦设置了分布式代理,就很难限制其仅用于编排。这样做可能有很好的理由,因为这种方法比基于编排的方法更能适应其他环境。

队列系统将允许消息的稳健交付。如果你还需要回溯时间并执行某些事件源操作(例如,为了实现 CQRS,进行大数据复杂计算,或者甚至为了简化最终一致性),那么你可能需要部署专门的分布式系统,如 Apache Kafka。再次强调,这是一套相当复杂的工具,所以请特别注意在偶尔丢失消息(我们真正谈论的是不频繁的事件,因为现代网络比它们的祖先更加健壮)和支付中间件额外成本之间的平衡(作为一个经验法则,你可以认为一个平均大小的中间件将花费你一个全职员工)。特别记住,即使是电子商务的巨头也接受一致性的损失,并实施许多策略来减轻后果(限制保留购物篮的时间,库存管理,只有在有足够库存的情况下才接受预订,如果产品锁定失败,提供改进的赔偿等)。

再次提醒,功能一致性至关重要。在思考错过一个事件有多危险时,不要采取会立即让你启动大炮的技术方法;只从功能角度思考,想象一个没有任何软件的信息系统。你该如何补偿这一点?也许有一种方法是在你下一次收到订单事件时,与发射者核实自指定日期和时间以来所有已通过的订单;通常情况下,应该只有一个触发事件的订单,但如果你错过了之前的订单,你现在就会了解到这一点。实现一致性的另一种方法可能是使用双重的 webhook 方法,通过一个自动作业每五分钟查询所有新订单,并验证它们是否确实按照应有的方式处理。如果你真的想把它提升到下一个层次,你甚至可以设置一个自我修复系统,该系统从其相邻服务中克隆所有重要数据,并且只对基于时间和基于交互的事件做出反应以执行其任务,同时始终在其操作中保持和传达一个价值日期。

当我与软件架构师讨论这种以功能优先的方法时,他们通常倾向于回答说,一个好的事务性系统会处理一致性,让我们摆脱这些功能复杂性。或者 Apache Kafka 最适合这类问题,将是解决方案——有时甚至没有对这些解决方案成本的文档估计。尽管有时这可能是可以接受的(再次强调,这完全取决于上下文),但尽可能深入地理解业务总是会给软件架构师带来价值。同时,也应该记住,尽管技术方法似乎完美地覆盖了困难,但总有失败的可能,这一点应该被考虑进去(当这些技术是 SPOFs 时,这种危险很高)。另一方面,当你清楚地了解你的系统在功能上必须保持多少一致性时,失败就包含在讨论中,因此不会再造成惊喜。功能方法是解决整体问题的唯一途径。

如果你想要更深入地了解这一点,一个很好的起点是理解传奇(简单来说,传奇是在你将持久性分离成几个数据库时,重新创建事务的一种方式,正如 MDM 和 SRP 所建议的,以减少耦合)。在microservices.io/patterns/data/saga.html上的优秀文章展示了如何通过编排和协奏来实施它们。请注意,尽管如此,在两种情况下都需要一个消息中间件(MOM),所以我们仍然得出相同的结论:由于事务是一种技术解决方案,它们不能完全覆盖功能性问题;如果你真的需要一个全局解决方案,你必须提供一个完全功能性的解决方案。在这种情况下,这涉及到为最终一致性找到业务规则并实施它们。正如你现在可能已经习惯的那样,找到这种功能性解决方案的最好方法是想象一个没有任何电脑的办公室:你将如何确保在复杂的工作流程中的一致性,例如,一个需要两个人按顺序工作的业务案例?最简单的方法是交替同步调用和异步回调:“这是文件,当你完成时叫我。”当回调发生时,这将触发将业务案例传递给下一个人的过程,带有相同的请求。当第二个人告诉发起者他们已经完成了这项工作的这一部分,整个过程就可以被认为是完成了。如果在任何位置出现停滞,发起者可以请求工作的状态。如果请求在某个地方丢失了,它可以再次发送。当一个执行代理表示他们已经完成了他们的工作单元时,发起者可能会用另一个工作单元发送给他们,这可以很好地避免过载代理和建立缓冲区,这对扩展规模时的性能是不利的。

我应该在信息系统中使用 BPMN 吗?

本章的长度可能表明我真正热衷于使用流程来实现软件信息系统集成。为了完全透明,我长期以来一直反对使用流程,因为我主要接触到它们的负面方面:将人们限制在远离实际工作的人所决定的工作方式中,工作流程的僵化往往阻碍任何创新,因为“它一直都是这样工作的”,等等。通过跟随法国数字大学(French Digital University)提供的两个名为 CARTOPRO(业务流程映射)和 PILOPRO(使用业务流程来引导组织)的大规模开放在线课程MOOCs),我完全改变了我的想法,因为这些课程通过展示正确使用 BPM(业务流程管理)时的力量,让我认识到,这意味着流程是由使用它的团队设计的(BPMN 专家只是帮助他们使用 BPMN 标准并提出正确的问题),而持续改进是整个流程策略的基础。实际上,我甚至继续参加了这两个 MOOCs,并从法国让·穆兰大学(Lyon 3)获得了额外的数字文凭,我的论文工作集中在敏捷方法流程表示(这是一个挑战,因为敏捷宣言的第一条建议是“人胜于流程”)。

为什么我要分享这些个人信息?只是为了强调这样一个事实:即使这可能会让你非常惊讶,我通常不推荐在信息系统架构中使用 BPMN 引擎。我知道,在我所说的和展示的所有内容之后,这可能会听起来很奇怪,尤其是考虑到 BPM 是这本书从开始就试图展示如何达到的理想信息系统的一部分。但经过多次在生产中使用这种方法尝试后,我现在可以真诚地说,在大多数组织中,工具和——更普遍地说——对 BPM 的理解还没有发展到足以使真正的 BPM 方法值得。

请仔细听我说:我并不是说这种方法完全没有价值。如果你有一个复杂或经常变化的具体业务流程,投资可能是值得的。但这是一个非常特殊的情况,当你必须满足以下标准时:

  • 这个流程对你的业务至关重要,你知道它将持续多年,甚至可能如此重要,以至于它将和公司一样长存。

  • 你知道工具并不完全稳定,并且你准备在必要时在项目中间更改 BPMN 引擎的实施,考虑到所有成本和后果。

  • 你拥有 BPM 建模和 BPMN 引擎维护的正确专业知识,并且你意识到你几乎不可能将这项工作集中在一个人身上。

  • 管理层理解,将团队的工作流程转变为以流程为导向的方法将需要为每个人提供培训,并且将需要长期而复杂的变革管理过程。

如果你勾选了所有这些选项,你面前将有一大堆工作要做,但在这个项目的最后,你将得到一份大礼:一旦投资完成,回报将是惊人的:当公司需要调整策略时,通过更改几个文件来修改流程的实施,这真的是一个信息系统的完美匹配的顶点。你需要经历解耦、适当的职责分离、遗留系统的长期演变,以及所有上述的 BPM 障碍,但一旦你到达那里,信息系统不仅将成为你组织的脊柱……它将成为其主要资产。

摘要

在本章中,我们在讨论了 MDM 之后,已经涵盖了乌托邦式信息系统的第二部分,即业务流程管理。尽管 BPMN 2.0 标准在标准 LOB 系统中并不常用,但它绝对是一个成熟的规范,而且工具对于编辑和运行时相当完整。遗憾的是,BPMN 2.0 的使用仍然没有飙升,这真是一件遗憾的事,因为它确实可以帮助适应信息系统的平滑演变。也许低代码/无代码方法将更好地实现使功能变更更容易、更少依赖 IT 人员的成果;只有时间才能告诉我们。

在下一章中,我们将涵盖乌托邦式信息系统的第三部分和最后一部分,即业务规则管理。我们将展示这个表达式的涵盖范围,它如何与另外两个职责集成,基于我们的演示信息系统场景提供示例,当然,我们还将讨论如何使用哪些软件应用来实现这样的功能,并遵循哪些一般性建议。

第十二章:业务规则的显式化

在详细介绍了理想信息系统中的主数据管理和业务流程管理部分的前两个章节之后,本章将以这样一个理想系统的第三部分和最后一部分结束,即业务规则管理系统BRMS)。我们已经在之前的章节中简要讨论了业务规则,因为数据参照可能包含与特定业务实体相关的一些验证规则,并且业务流程也可能嵌入一些业务规则以指导工作流程并决定根据上下文应该执行流程的哪个分支。但在我们构想的完美理想系统中,一个集中的系统应该负责所有业务规则,这就是本章的主题。

我们将首先详细解释什么是 BRMS,以及实施此类解决方案在业务规则管理、部署和系统数据流架构方面所需的内容。然后,我们将展示使用称为DMN(即决策模型和符号)的标准在业务流程中使用的第一个业务规则管理的例子。

与前两个不同,我们将在这个章节(以及关于理想信息系统不同部分的三个章节系列)结束时,不提供我们样本信息系统的应用示例。这样做的原因是,授权管理是业务规则管理的最佳例子之一,但这个主题非常复杂,需要一整章来理解。

业务规则管理系统

业务规则管理系统(以下简称BRMS)是一套软件,用于处理可以应用于数据的计算和决策,以便输出具有更高商业价值的成果。这个定义中包含了许多概念,我们将逐一进行解释。

BRMS 如何处理业务规则?

例如,一条业务规则可以计算订单行的总价,使用商品的不含税价格、商品数量和适用的税率。另一个应用示例可能是决定在开票过程中创建的文件是否应该给予电子签名。在这种情况下,业务规则输出一个布尔值,表示结果为真或假。业务规则可以相互调用。在前一个例子中,我们可能需要决定如何向某人展示文件以供签名,如果初始签署人在多次通知后被视为缺席,谁将签署,将发送多少此类通知以及通过哪些渠道,等等——所有这些都是业务规则。

如其名称所示,BRMS 管理业务规则。但其所涉及的内容并不那么明显。一方面,你可以认为 BRMS 是业务规则的 MDM(主数据管理),它可以存储它们,包括它们的旧版本。它可以允许一些人阅读它们,一些人编写它们,或者拒绝那些对某些业务规则没有任何授权的人。它可以对业务规则进行分组和分类,以便指定其研究。所有这些都是在 MDM 对其引用的业务实体上完成的,但 BRMS 还有一个 MDM 没有的责任,那就是执行业务规则。确实,业务规则通过输入来计算输出,而 BRMS 的主要责任就是这样做。

然而,责任并不意味着 BRMS 执行一切。大多数时候,它将委托规则的执行,因为它不拥有业务规则所针对的数据或以规则输出定义的方式执行业务动作的服务。这可能听起来有些反直觉,但 BRMS 甚至可以委托规则执行的职责(即从其输入计算规则的输出)。例如,当 MDM 服务使用来自 BRMS 的规则验证其传入数据时,这种情况就会发生。由于它不会引入太多的耦合,因此规则表达式的本地缓存也是可能的。尽管如此,验证规则的责任仍然在 BRMS,因为如果有人在 BRMS 编辑器中更改了规则,那么它将(可能是在缓存因性能原因未立即失效后)应用于使用此规则的所有服务器,其中包括我们示例中的 MDM。

总结来说,BRMS(业务规则管理系统)的主要职责是存储、暴露和执行业务规则。它负责规则的正确执行,因此要么内部执行这些规则,要么信任其他应用程序执行它提供的规则。这种情况很常见,因为外部应用程序是那些能够访问执行规则所需输入数据的应用程序。而且,它们通常也是那些根据规则输出调整其行为的应用程序。

BRMS 的附加特性

我们经常谈论服务的“次要职责”,这些职责对于绝对必要的功能来说不是必需的,但仍然很重要。在 BRMS 的情况下,有几个这样的职责。

首先,一个 BRMS(业务规则管理系统)应该具有高性能,无论是在执行时间上还是在承受高请求量方面的能力。确实,规则执行是少数几个难以应用缓存的情况之一。当你从一个 URL 检索图像时,有很大可能性它不会在几秒钟后从一个调用变为另一个调用而改变;因此,保留缓存是非常有价值的,因为这将避免网络往返和服务器请求处理,并极大地提高性能。对于业务规则来说,情况并非如此,因为它们的主要功能是从变化的输入中进行计算,并提供依赖于它们的输出。

当然,规则表达式可以被缓存(并且规则可能不会频繁更改),但是当你这样做时,调用者必须能够从其文本表达式本身执行规则,这可能过于复杂,需要规则执行引擎。如果规则在许多服务之间共享,那么许多引擎实例将需要与 BRMS 保持同步,这并不高效。因此,我们回到引擎只在一个地方,即 BRMS 服务器本身。

在这种情况下,发动机的移动部件可能被缓存,或者至少保持在 RAM 中,这将提供快速执行。然而,根据输入缓存结果,大多数情况下并不高效,因为可能存在如此多的可能值。以我们之前的例子来说,缓存发票行总价计算的结果完全没有必要,因为几乎不可能有另一个调用会很快返回相同的商品,相同的数量和税率。如果你考虑到一些规则可能基于不断变化的数据(例如股市价值),那么安排某种缓存方式可能变得完全不可能。因此,我们基本上退回到需要一个能够尽可能快地输出值的引擎的需求。当然,在高负载情况下,这一需求应该得到支持。因为与报告数据相比,业务数据往往更具波动性,所以业务规则会被频繁调用。

一个好的 BRMS 的另一个“次要”特性是健壮性。当它们在行业中使用时(这并不常见,因为它们是复杂的应用程序),是因为它们是业务流程的重要组成部分。例如,BRMS 被保险公司用来计算风险,或者被移动通信公司用来根据通话数据(通话时长、拨打的号码、一天中的时间等)和合同(对某些号码的折扣、每月预付费、月消费等)来计算应支付的费用。由于 BRMS 的成本,它们通常用于核心业务功能,其中重要的决策(在我们的例子中,接受合同和向客户发送正确的发票)是基于它们的输出做出的。因此,计算的健壮性是一个重要的方面,因为没有人愿意与偶尔会出错计算的系统合作。

由于同样的原因,可追溯性通常是 BRMS 的一个重要特性。当然,它可以委托给调用服务,因为 BRMS 主要是为其他服务工作的。但即使责任是共享的,也应该有记录,记录是否将规则应用于某些上下文数据,提供澄清为什么制定某个规则的输出。即使日志更适合调用应用程序,将 BRMS 规则集的版本保存在某处,并且规则引擎版本不可变,也是一个好主意。这允许你在必要时回到过去,在 BRMS 引擎当时使用的版本上重新执行业务规则计算,并了解为什么输出值是错误的。

最后,如前所述,BRMS 通常与其他服务一起使用,单独使用是无用的。其低级特性使其集成和与其他服务的良好互操作性至关重要。实现通常应至少提供一些 API,如果可能的话,为尽可能多的语言提供 SDK,使其易于与所有可能的软件应用程序交互。

BRMS 的实际使用

如前所述,BRMS 在实际应用中的使用非常低。实施成本如此之高,以至于只有少数非常特定的业务规则执行案例才真正值得部署专用服务器。此外,正如我们所见,规则的外部化会带来很高的性能损耗,因为要么知道数据的应用程序必须将其发送给 BRMS 并等待输出以继续其流程,要么它必须动态执行 BRMS 发送的业务规则表达式,也许内部缓存。在这种情况下,执行速度仍然低于将规则编译到应用程序中时的速度。当然,应用程序和规则之间的耦合度是最大的,没有规则的集中共享,许多用途可能会分化。然而,性能问题可能非常严重,这些原因并不那么重要。

此外,我们也不应低估习惯性因素——由于开发者的大部分职业生涯都是将业务规则从用例中提取出来,并将其转换为应用中嵌入的代码,因此改变这种思维方式,提取业务规则,并将其放置在其他地方是一项努力。而结果如何?性能大幅下降,代码的可读性和可维护性更差。这意味着业务规则应该放在 CommonBusinessValues 类的 public static readonly 成员中,这样一切都会顺利,并准备好更新。

这意味着,确实,在 99.99% 的情况下,业务规则将通过代码具体实现,如下面的 C# 示例所示:

public decimal GetPrice(decimal unitPrice, int quantity, decimal taxRate)
{
    return (unitPrice * quantity) * (1 + taxRate);
}

此外,许多其他业务规则将散布在代码的各个角落:

if (Document.Type == DocumentTypes.INVOICE)
{
    SendForDigitalSignature(Document);
}

作为旁注,对于这种类型的值,最好使用字符串值或甚至专门的代码结构,而不是枚举,因为这有助于进化。

事实上,代码库中到处都是业务规则,很难全部找到。但这并不是最重要的。真正的挑战是架构师/产品所有者/开发者要知道,在创建应用程序时,哪些应该外部化,哪些应该集中化,哪些应该简单地留在代码中,即使有重复,因为它们永远不会改变。但请注意,有些被认为永远不会改变的事物有时会随着时间的推移而演变!例如,你可以这样说,关于净价的规则总是稳定的;净价总是免税价格乘以(1+税率)。嗯,是的,直到政府决定应用不同的税率,这些税率适用于产品的不同部分。例如,如果你的产品信息管理软件中有一些由硬件部分和安装服务组成的文章,你可能会遇到第一部分被征收 5.5% 的税,而第二部分被征收 20% 的税。如果计算已经写入集中化的函数中,这并不是那么糟糕。但如果它在代码中的数百个地方被重复(这可能是每个人都认为恒定且不可变的企业规则),你将面临一些困难,不仅因为改变需要花费很长时间才能实现,而且因为你忘记的那个实例很可能是你的最重要客户使用的。

简而言之,将业务规则外部化到一个专门的 BRMS 中,99.99% 的时间都是过度设计,且成本无法得到合理证明。但仅仅通过将业务规则放入一个函数中,你就能走得很远。而且,大多数情况下,唯一的困难可能就是意识到你正在实现一个业务规则!

BRMS 的示例

假设你确实处于这种非常特别的 0.01%的情况中,即你实际上可以从实施一个专门的 BRMS 中获得商业价值。因此,你需要一款软件来为你完成这项工作,因为正如你可以从所需的其他功能中想象到的,这种服务器是一段相当复杂的代码。在撰写本文时,只有两个严肃的 BRMS 服务器竞争者——Drools(开源)和操作决策管理器ODM)。

最常用的开源 BRMS 是 Drools(www.drools.org/)。它包含一个核心引擎来计算规则(包括一些如规则链的功能),有时被称为推理引擎,因为它可以从数据上下文和一组规则中推断出结果。它还包含一个用于创建和操作规则的应用程序(带有网页编辑器)。Drools 是用 Java 编写的,可以与其他平台互操作,但不是原生支持。

IBM 的 ODM 是一个专有决策管理系统,旨在从遗留的 COBOL 代码中提取重要的业务规则,以努力使 z/OS 平台上的信息系统现代化。尽管它可以操作规则,但它主要围绕事件决策的概念组织。

如你所见,这个领域的复杂性远不如其他 IT 领域——例如大数据,一本书甚至无法描述所有可用的软件应用、平台和服务器,它们大多在做同样的事情,同时假装与竞争对手有根本的不同。这有其清晰性的优点——如果你需要在你的信息系统中实施 BRMS 并希望降低成本,Drools 将是你的首选。

当然,还有一些不太为人所知的替代方案。许多 BPMN 引擎实现了自己的工作流决策语言。Windows Workflow Foundation 就是这样做的,但现在不再受支持。PowerApps 有一些表达式能力,可以用于业务规则执行,但它只能共享,因此它不是一个真正的 BRMS 系统。另一个解决方案,尽管它涉及额外的工作,就是实现你自己的 BRMS。如果你不需要高级功能,如果你使用现有的表达式执行引擎,你可以相当快速地构建一个。有很多脚本语言可用,你甚至可以在 C#中使用 C#,利用表达式树、动态代码生成和其他先进但仍然易于访问的功能。

简而言之,你有软件的选择,即使它不像某些其他 IT 领域那样丰富。然而,正如你现在可能已经习惯的那样,业务/IT 协同是关于减少耦合的,因此软件实现的选取通常不是一个如此重要的话题(从它可能损害应用演变的角度来看),只要存在一个标准规范、广泛接受的规范,或者甚至只是一个组织范围内的关键格式,它可以在功能依赖和技术实现之间提供一种间接层次。而且好消息是,存在一个关于业务规则的标准,即决策建模符号DMN)1.0。这将是本章下一节讨论的主题。

DMN 标准

DMN 是一个定义决策树和决策表的标准,这是关于业务规则实现的两个主要概念。在接下来的章节中,我们将展示它是如何工作的以及它有多么有用。

DMN 的起源和原理

DMN 标准目前是 1.0 版本,由OMG(即对象管理组)在 2015 年 9 月发布。在撰写本文时,最新的验证版本编号为 1.3,并于 2021 年 2 月发布。1.5 版本自 2023 年 6 月存在,但目前被视为一个测试版本。因此,我们将只讨论 1.3 版本。

注意,OMG 也是 BPMN 2.0 标准的联盟,它与 DMN 标准协同工作。正如 OMG 在第一版发布时所述(www.omg.org/spec/DMN/1.0/About-DMN):“DMN 符号设计为可以与标准 BPMN 业务流程符号一起使用。”在 BPMN 中存在一种与业务规则直接相关的任务类型:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_1.jpg

图 12.1 – 业务规则任务

这种任务背后的想法是存在复杂的业务规则,这些规则决定了 BPMN 业务流程应该如何行为(主要是,在网关中应该选择哪条路径)以及应该有一种处理此类决策的方法。确实,想象一下以下(并不那么)复杂的过程:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_2.jpg

图 12.2 – 具有多个规则的流程示例

目前来说,情况还不错,因为只有三种类型的合同。但这通常是那种有很多机会扩展的场景(永远不要低估销售人员和市场营销人员的创造力)。如果未来有十种类型的合同,也许还需要考虑第三个标准呢?流程将变得越来越复杂,很快就会变得难以辨认,这将会是一个大问题,因为业务流程应该始终是团队的有用工具,而不是让他们的工作变得更复杂的东西。

DMN 提出了一种解决方案,即将决策规则外部化到一个专门的地方,以便释放流程本身的设计。在先前的例子中,我们会这样外部化决策表:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_3.jpg

图 12.3 – 决策表

这将使我们能够以更简单的方式绘制流程,如下所示(注意第二个任务中的图标,它对应于Business rule类型):

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_4.jpg

图 12.4 – 简化的 BPMN 流程

注意

很遗憾,由于 BPMN 流程中不同任务收集数据的方式没有标准化,因此调用 DMN 模型的方式也无法标准化。但是,值得了解www.omg.org/dmn/上的规范更新,因为这在未来的某个时候肯定会发生变化。

最好的部分是,现在这个逻辑已经从业务流程本身解耦,我们可以进化到一个更复杂的合同类型定义,如下所示,而无需对流程本身进行任何更改:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_5.jpg

图 12.5 – 扩展的决策表

这次,我们还考虑了作者的年龄,发布了一些必须由作者父母签署的特殊合同。这是通过这里的一个非常简单的表达式实现的(FEEL表达式语言允许使用更复杂的表达式,如果您想深入了解这个主题,kiegroup.github.io/dmn-feel-handbook/#dmn-feel-handbook是一个很好的起点)。您也可能已经注意到,AdditionalEdition,因为只要这本书是现有版本的全新版,结果对任何作者都是通用的。

拥有这些表来外部化可能复杂的规则已经是一个很大的优势,但 DMN 还提供了一种图形化的方式来表示决策过程本身:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_6.jpg

图 12.6 – 决策图的示例

在我们的例子中,图表非常简单,因为我们只使用了两个输入(作者和书籍信息)来创建一个决策(合同类型),可能使用“知识源”,即我们的合同参考,尽管我们在先前的简单示例中没有涉及此类使用。然而,这些图表可以更加复杂,并在必要时显示分层决策。我们可以想象,所决定的合同类型随后本身被用来决定定制合同的內容,这取决于作品的传播区域,并且销售统计数据被用来决定合同提议的金额:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_7.jpg

图 12.7 – 扩展的决策图

为了给出 DML 文件 XML 结构的概念,以下是上述第一个示例的(缩短的)内容,其中你将很容易识别出以 <decision> 开始的决定规则的第一部分和以 <dmndi:DMNDI> 开始的对应于图表的第二部分:

<?xml version="1.0" encoding="UTF-8"?>
<definitions      id="definitions_065qkmh" name="definitions" namespace="http://camunda.org/schema/1.0/dmn" exporter="dmn-js (https://demo.bpmn.io/dmn)" exporterVersion="15.0.0">
  <decision id="decision_1u2xbtg" name="Type of contract">
    <informationRequirement id="InformationRequirement_1i0e44v">
      <requiredInput href="#InputData_0wi3jz6" />
    </informationRequirement>
    <informationRequirement id="InformationRequirement_0g0syf3">
      <requiredInput href="#InputData_1jz546j" />
    </informationRequirement>
    <decisionTable id="decisionTable_0cwlzw4" biodi:annotationsWidth="400">
      <input id="input1" label="Author">
        <inputExpression id="inputExpression1" typeRef="string">
          <text></text>
        </inputExpression>
      </input>
      <input id="InputClause_1hfsajf" label="Book">
        <inputExpression id="LiteralExpression_00wz5lk" typeRef="string">
          <text></text>
        </inputExpression>
      </input>
      <output id="output1" label="Contract" name="" typeRef="string" />
      <rule id="DecisionRule_05kn45x">
        <inputEntry id="UnaryTests_19ou6i4">
          <text>"New"</text>
        </inputEntry>
        <inputEntry id="UnaryTests_0l88vr8">
          <text>"New"</text>
        </inputEntry>
        <outputEntry id="LiteralExpression_05irfs8">
          <text>"New"</text>
        </outputEntry>
      </rule>
      <!-- Some rules removed -->
      <rule id="DecisionRule_1sg8k57">
        <inputEntry id="UnaryTests_1yjyvpp">
          <text>"Existing"</text>
        </inputEntry>
        <inputEntry id="UnaryTests_0qo517n">
          <text>"AddEdition"</text>
        </inputEntry>
        <outputEntry id="LiteralExpression_0ymlni0">
          <text>"AdditionalEdition"</text>
        </outputEntry>
      </rule>
    </decisionTable>
  </decision>
  <inputData id="InputData_0wi3jz6" name="Author history" />
  <inputData id="InputData_1jz546j" name="Books history for the author" />
  <dmndi:DMNDI>
    <dmndi:DMNDiagram id="DMNDiagram_1r90cap">
      <dmndi:DMNShape id="DMNShape_15dfipm" dmnElementRef="decision_1u2xbtg">
        <dc:Bounds height="80" width="180" x="330" y="200" />
      </dmndi:DMNShape>
      <dmndi:DMNShape id="DMNShape_14d6htu" dmnElementRef="InputData_0wi3jz6">
        <dc:Bounds height="45" width="125" x="257" y="337" />
      </dmndi:DMNShape>
      <dmndi:DMNEdge id="DMNEdge_1a3apwq" dmnElementRef="InformationRequirement_1i0e44v">
        <di:waypoint x=»320» y=»337» />
        <di:waypoint x=»390» y=»300» />
        <di:waypoint x=»390» y=»280» />
      </dmndi:DMNEdge>
      <dmndi:DMNShape id=»DMNShape_0s4bzo1» dmnElementRef=»InputData_1jz546j»>
        <dc:Bounds height="45" width="125" x="457" y="337" />
      </dmndi:DMNShape>
      <dmndi:DMNEdge id="DMNEdge_0ng7t96" dmnElementRef="InformationRequirement_0g0syf3">
        <di:waypoint x="520" y="337" />
        <di:waypoint x="450" y="300" />
        <di:waypoint x="450" y="280" />
      </dmndi:DMNEdge>
    </dmndi:DMNDiagram>
  </dmndi:DMNDI>
</definitions>

所展示的所有图表都是使用 Camunda 提供的强大工具在 demo.bpmn.io/dmn 上设计的。现在你已经对 DMN 的基本概念有了初步的了解,让我们看看如何将标准应用到实践中。

实现

业务规则执行系统(Business Rules Execution System,简称 BRMS)的领域相当小。DMN 的首选实现一直是,并且仍然是名为 Drools 的 Java 开源项目。Drools 是一个支持其自身规则语言的 BRMS,同时也支持 DMN,由于 DMN 是一个标准,因此所有使用 Drools 的服务器都基于它。你可以在你的 Java 应用程序中直接使用 Drools,甚至通过一些桥梁连接到其他平台。特别是,已经有一个 Drools .NET 实现,一些项目如 github.com/adamecr/Common.DMN.Engine 可以帮助实现这一点,但这些项目的维护情况值得怀疑,我更愿意展示另一种——在我看来——更适合我们试图实现的目标的方法,即一个对齐且可适应的信息系统。

为了做到这一点,我们将通过使用一个暴露业务规则运行时通过 REST API 的 BRMS 服务器来更接近服务导向架构。当然,性能可能不会像使用嵌入式库那样强大,但请记住,首先,“过早优化是万恶之源”,其次,大多数业务规则的调用并不频繁(如果需要,我们将在本章末尾展示如何适应)。Kogito 在上一章中已经被提及,但我们没有展示与它结合的完整 BPMN 示例,因为正如解释的那样,这对大多数情况来说都是过度设计,特别是我们的示例 DemoEditor 信息系统。有趣的是,Kogito 也支持 DMN,这就是我们在这里使用它的原因——或者更准确地说,使用 JBPM,这是 Kogito 所基于的产品。

事实上,Kogito 是 JBPM 的云原生衍生产品,一个在 JBoss 旗下维护的产品。由于我们不会在云中部署,而是保持基于 Docker 的应用程序部署以满足 SaaS 或本地化条件,因此我们将在以下示例中简单地使用 JBPM。然而,请记住,对于您的需求来说,Kogito 可能是一个更好的选择,尤其是因为它提供了一些可以与轻量级 MDM 相比的功能,通过动态生成的 REST API 直接暴露实体。如果您想朝这个方向前进并查看一个完全集成的面向云的方法如何适合您,您可以从 Kogito 的 Docker 镜像开始,这些镜像可在github.com/kiegroup/kogito-images找到。

我们将要利用的 JBoss JBPM 服务器是一个一站式应用程序,提供前端和后端来设计和操作带有基于 DMN 业务规则的 BPMN 工作流。它与包含一些 Java 代码用于单元测试和可能用于实体展示的 Maven 项目一起工作,但也可以使用简单的标准 DMN 文件在我们的示例中运行。

在下一节中,我们将解释如何在 JBPM 7.74 中操作一个示例业务规则引擎,使用 Drools 引擎和两个带有多个参数的业务决策的 DMN 定义。有关此工作精确方式的更多信息,请访问docs.jboss.org/drools/release/7.74.1.Final/drools-docs/html_single/。我们使用 JBoss 提供的示例的原因是,从头开始设计一个关于DemoEditor主题的示例将占用整整一章。此外,这将是一个完全人为的练习,因为 DMN 规则引擎,就像前一章中的 BPMN 引擎一样,对我们的功能需求来说将是过度设计。我必须尊重我从本书开始就反复强调的主要规则,即技术方面应由功能需求完全定义。尽管我——就像大多数对技术充满热情的人一样——希望在我们的示例信息系统中集成一个完整的 Kogito 服务器,但事实是它并不适合我们的需求。业务工作流和大多数业务规则的实施将简单地使用专门的.NET 服务。只有特定类型的业务规则将使用一个专门的外部服务来处理,该服务强烈类似于 BRMS,即授权规则。但我正在期待本章的最后一部分,现在,我们将展示如何在业务/IT 对齐的背景下利用基于 DMN 的 BRMS,使用 JBPM。

DMN 使用的示例

以下简单的练习正是 Docker 突出表现的地方,因为它将节省我们安装 Java 和 Maven、获取正确依赖项、更新版本等麻烦。只要你在机器上安装了 Docker(如果还没有,你真的应该安装,因为这个工具现在已经成为你的基本工具集的一部分,就像网页浏览器和文本编辑器一样),你就可以简单地输入以下命令:

docker run -d --name jbpm-console -p 8080:8080 quay.io/kiegroup/business-central-workbench-showcase:latest
docker run -d --name jbpm-bre -p 8180:8080 --link jbpm-console:kie-wb quay.io/kiegroup/kie-server-showcase:latest

注意,截至写作时,latest 标签是 7.74.1.Final 版本。通常建议尽可能多地使用 latest 标签,但如果您在重新播放示例时遇到功能问题,请尝试使用这个精确版本,即使它不再是最新版本。第一个 Docker 命令将启动一个基于包含设计、构建、测试和部署项目所需一切内容的镜像的容器,包括 BPMN 和 DMN 资产。这就是我们将操作 DMN 模型的地方。如果您想获取有关此镜像的更多信息,参考页面是 quay.io/repository/kiegroup/business-central-workbench-showcase。第二个 Docker 命令在一个容器上运行项目,该容器将作为单独的业务规则执行引擎,或者如果您更喜欢这样想,它将作为一个简单的运行时。此第二个镜像的参考页面是 quay.io/repository/kiegroup/kie-server-showcase

一切启动完成后(您应该允许一些时间——最多一分钟——以完成启动程序),您可以通过导航到 http://localhost:8080/business-central 访问控制台,在那里您可以使用默认凭据 admin/admin(之前引用的文档为具有不同授权配置文件的用户提供了其他凭据,以及如何设置生产就绪的授权)进行连接。

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_8.jpg

图 12.8 – JPBM 登录页面

一旦连接,您将看到欢迎页面界面,您可以通过点击 业务中心 或屏幕左上角的首页图标随时返回。

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_9.jpg

图 12.9 – JPBM 欢迎页面

设计 部分点击 项目。这将带您到一个界面,您可以在此管理您的 JBPM 项目:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_10.jpg

图 12.10 – JPBM 空间的列表

空间用于组织工作和将一组项目与其他项目分开。在这个技术的简单试用中,只需选择现有的 MySpace 空间。

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_11.jpg

图 12.11 – 没有任何项目的 JPBM 空间

刚创建的空间现在当然是空的。我们将使用其中一个嵌入式示例来说明 JBPMN 的工作原理以及我们目前特别感兴趣的内容,即 DMN 规则引擎。为此,请点击尝试示例,这将带您进入以下界面:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_12.jpg

图 12.12 – 选择示例项目

在那里,选择Traffic_Violation示例项目并点击确定。你应该会收到一条消息,说明项目已正确导入,并且你会进入一个显示示例项目包含的资产的页面:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_13.jpg

图 12.13 – 交通违规 JBPM 示例的资产

当然,我们最感兴趣的资产是 DMN 模型。点击Traffic_Violation资产进行分析,你将被引导到以下界面,该界面显示了 DMN 模型的主体部分,即决策图:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_14.jpg

图 12.14 – 一个示例 DMN 决策图

如果你拥有驾照,这个示例的理解应该是显而易见的——超速违规提供了计算相关罚款的数据。然后,根据罚款和驾驶员的额外背景信息,将采取另一个决定,关于是否应该吊销驾驶员的执照。

如果你现在点击左侧菜单中的Fine部分的Decision Table条目,你会看到以下表格,该表格描述了决策应用的条件:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_141.jpg

图 12.15 – 一个示例 DMN 决策表

现在,通过顶部导航的面包屑菜单返回项目,然后点击顶部菜单中出现的部署

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_15.jpg

图 12.16 – JBPM 构建和部署菜单

经过一段时间后,你应该会看到一条消息,说明构建成功,然后是第二条消息,如图所示,解释说现在一切准备就绪,可以利用决策引擎:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_16.jpg

图 12.17 – JBPM 部署成功通知

如果你想要查看部署结果,可以激活菜单/执行服务器命令,并观察服务器是如何配置的以及部署单元是如何在它们上面组织的。然后,你可以从这个控制台启动和停止执行服务器,甚至可以删除部署:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_18.0.jpg

图 12.18 – JBPM 服务器管理界面

由于一切现在都已设置和部署,我们能够利用业务规则。

调用业务规则运行时

检查引擎的有效性,只需调用一个为我们提供动态暴露的 REST API 即可。为了做到这一点,由于引擎(逻辑上)通过POST动词暴露,我们需要一个比简单网页浏览器更先进的工具,例如 Postman。要访问 API,您将必须使用与我们运行的第二个 Docker 容器关联的端口号 – 在我们的例子中,8180。URL 的其余部分如下所示:

  • /kie-server对应于规则执行引擎的应用服务器(或BRE代表业务规则执行

  • /services/rest表示我们将访问 REST API

  • /server/containers与 BRE 服务器通过容器暴露的事实相关联,每个部署单元与其他部署单元分开

  • /traffic-violation_1.0.0-SNAPSHOT是我们选择在本单元中部署的项目标识

  • /dmn对应于我们在这个项目中感兴趣的资源,即决策管理系统

请求体的内容应调整为raw/json,并包含以下数据:

{
    "model-namespace": "https://github.com/kiegroup/drools/kie-dmn/_60B01F4D-E407-43F7-848E-258723B5FAC8",
    "dmn-context": {
        "Driver": {
            "Points": 15
        },
        "Violation": {
            "Type": "speed",
            "Actual Speed": 135,
            "Speed Limit": 100
        }
    }
}

model-namespace对应于项目的唯一标识符,而dmn-context表示应该提供给规则引擎以执行其值的值。界面应如下所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_19.0.jpg

图 12.19 – 一个示例 Postman 调用

为了使这可行,您需要以kieserver作为用户名和kieserver1!作为密码登录到kieserver(这些是默认值,当然,在生产环境中这些值会改变):

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_20.0.jpg

图 12.20 – Postman 认证设置

最后,在向服务器发送消息后,完整的响应如下:

{
  "type" : "SUCCESS",
  "msg" : "OK from container 'traffic-violation_1.0.0-SNAPSHOT'",
  "result" : {
    "dmn-evaluation-result" : {
      "messages" : [ ],
      "model-namespace" : "https://github.com/kiegroup/drools/kie-dmn/_A4BCA8B8-CF08-433F-93B2-A2598F19ECFF",
      "model-name" : "Traffic Violation",
      "decision-name" : [ ],
      "dmn-context" : {
        "Violation" : {
          "Type" : "speed",
          "Speed Limit" : 100,
          "Actual Speed" : 115
        },
        "Driver" : {
          "Points" : 15
        },
        "Fine" : {
          "Points" : 3,
          "Amount" : 500
        },
        "Should the driver be suspended?" : "No"
      },
      "decision-results" : {
        "_4055D956-1C47-479C-B3F4-BAEB61F1C929" : {
          "messages" : [ ],
          "decision-id" : "_4055D956-1C47-479C-B3F4-BAEB61F1C929",
          "decision-name" : "Fine",
          "result" : {
            "Points" : 3,
            "Amount" : 500
          },
          "status" : "SUCCEEDED"
        },
        "_8A408366-D8E9-4626-ABF3-5F69AA01F880" : {
          "messages" : [ ],
          "decision-id" : "_8A408366-D8E9-4626-ABF3-5F69AA01F880",
          "decision-name" : "Should the driver be suspended?",
          "result" : "No",
          "status" : "SUCCEEDED"
        }
      }
    }
  }
}

我们特别感兴趣的是dmn-context是如何完成的,以及决策的结果。在我们的案例中,罚款将是 3 分和 500 单位货币,而驾驶执照吊销决策的结果将是负面的。但将请求体中的Actual Speed更改为135,再次发送,并观察对结果的影响:

        "Fine" : {
          "Points" : 7,
          "Amount" : 1000
        },
        "Should the driver be suspended?" : "Yes"

因此,该引擎已准备好在任何可以处理 REST API 的信息系统中使用(这是地球上除少数非常罕见的例外情况之外的所有平台)。注意,在 JBPM 平台内部也有所有进行测试所需的一切,以测试已构建的决策。由于 Postman 更接近另一个应用程序如何利用 BRE,因此更喜欢使用 Postman 进行测试,但如果你点击违规场景资产,你将被带到这个很棒的界面,在那里你可以执行初步测试,以确保在部署之前一切正常:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/B21293_12_21.jpg

图 12.21 – 一个集成了 JBPM 的自动测试界面

此外,如果你想更好地了解如何创建自己的项目(这超出了本书的范围,本书只关注如何使用现有项目来正确构建信息系统),最佳起点是之前使用的示例代码源,可以在 github.com/kiegroup/kie-wb-playground/tree/main/traffic-violation 找到。你还可以按照在 docs.jboss.org/drools/release/7.51.0.Final/drools-docs/html_single/#dmn-gs-new-project-creating-proc_getting-started-decision-services 中解释的详细说明,从控制台构建此项目。

最后,我们将通过一个非常简单的(多亏了 Docker)清理程序来完成此示例(请注意,这将删除与练习相关的所有数据):

docker rm -fv jbpm-console
docker rm -fv jbpm-bre

所有的内容都应该恢复到创建此示例之前测试机器的状态,这使我们得出本章的结论。

摘要

在本章中,我们展示了业务规则管理系统的作用,它在信息系统中的有用性,以及我们如何实现它,从功能示例开始,然后展示与授权相关的另一个示例,授权是软件应用中最常用的业务规则集之一。

就像 BPMN 引擎一样,BRMS 引擎并不经常使用。实际上,在绝大多数情况下,业务规则都是通过代码表达式实现的,或者编译到应用程序中。这是绝对正常的,因为 BRMS 代表着重要的投资,而实现如此复杂的应用程序确实需要强大的业务案例,其中业务规则变化非常频繁,它们与高监管或营销约束相关(例如,需要跟踪所有业务规则及其变化),有模拟业务规则集新版本影响的能力,等等。因此,很明显,这种方法目前仅限于非常罕见的情况。当然,随着信息系统设计工业化的期待,未来事情可能会发生变化,但目前,BPMNs 和 BRMSs 几乎总是过度设计。

由于理想系统的三个部分中有两个对大多数组织来说不值得使用,这意味着这个系统仍然是乌托邦式的。此外,即使是集中式的 MDM 方法也很复杂。MDM 实践本身适用于每个业务领域,所以数据参照没有问题——它们并不复杂,正如我们将在第 16 章 和接下来的章节中看到的那样,并且它们带来了大量的商业价值和优势。然而,理想系统旨在实现通用的 MDM,能够动态地适应应用业务环境中的每个实体。这种额外的复杂性目前还不适用,尽管数据参照的静态代码生成正在成为一种可行的选择,这将在第 19 章 的结尾展示。

此外,我们已经表明,理想信息系统的三个责任最终是相当相互关联的:

  • MDM 在其数据验证中使用业务规则

  • BRMS 需要从 MDM 获取数据来应用业务规则并决定它们的输出值

  • BPMN 主要作为数据收集器,为 MDM 提供数据,同时也从 MDM 消费数据

  • BPMN 还使用业务规则来确定在不同网关中的走向(有时在执行给定任务期间计算一些额外的数据)

所有这些都证明,从技术上讲,这个为 MDM、BPM 和 BRMS 设计的三个通用服务器组合并不可行,也没有实现完美的解耦。那么,为什么我们在 第五章 和最后三章中讨论这样一个理想系统呢?答案再次在于业务/IT 对齐。理想系统并不是今天的信息系统中可以实现的(当然至少在未来几十年内也不可能实现),但它有一个巨大的优势,那就是迫使架构师从三个通用、始终适用的功能责任的角度来思考。即使你使用一个独特的软件应用,了解如何分离数据管理、业务规则管理和业务流程执行,这也能为解耦你的信息系统迈出重要的一步(例如,n-层架构根本无法实现这一点)。正如你将在接下来的章节中看到的,本着这些原则构建信息系统将帮助我们实现一个非常复杂的目标,即能够非常容易地修改重要的功能规则和行为,在大多数情况下不会对实施产生任何重大影响。

在下一章中,正如引言中所述,我们将展示一个特定的用例——一个非常重要的业务规则管理应用——即使用规则在软件应用程序中确定和执行授权。尽管我们已经在本章中展示了几个例子,但如何使用 BRMS 的完整描述将在下一章中发生,我们将通过应用专门的授权管理策略到我们熟悉的示例信息系统中来实现。

第十三章:授权的外部化

上一章是关于业务规则管理的一般性内容。在本章中,我们将分析授权管理的特定案例,因为用户的权利和特权是许多应用程序中可以找到的常见业务规则使用之一。由于存在两个授权管理标准(如已在第八章中探讨),我们将快速解释第一个更完整的标准,即XACML(代表可扩展访问控制标记语言),因为它有助于理解它与单一责任原则SRP)的关系;然后,我们将使用新的、更轻的标准OPA(代表开放 策略代理)创建一个更完整的示例。

我们将以此结束本章(以及关于理想信息系统不同部分的四个章节系列),通过反思如何在实践中实施这种授权,这将开启对伴随我们至今的DemoEditor信息系统的分析和实施之路,通过示例说明所研究的概念,当然,这还将作为我们在上一章所学内容的实际应用示例。

在本章中,我们将涵盖以下主题:

  • BRMS 和授权管理

  • 将授权应用于我们的同一信息系统中

BRMS 和授权管理

如我在上一章中简要提到的,在DemoEditor示例信息系统中存在一个功能域,在这个域中,外部化的业务规则引擎会很有趣,而这个域就是授权。在解释澄清“权利”业务域的语义需求之前,先考察在软件应用程序中实现授权的主要范例,并解释与该功能相关的一个标准,该标准很好地分解了它所涉及的不同责任。

身份和授权管理的语义

第九章中所述,语义是架构中所有事物的基石,我们将明确界定我们用于某些概念的术语,以避免错误地定义业务域模型。因此,明确定义身份和授权管理IAM)的不同子域以及我们在其中如何命名事物是很重要的。让我们从与识别(你是谁)和认证(你如何证明你的身份)相关的概念开始:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.1_B21293.jpg

图 13.1 – 识别和认证语义

一个首要的——并且非常重要的——观点是,授权应仅依赖于你的身份(当然,还有一些上下文元素,但我们会稍后讨论)以及你如何证明你的身份,而不是证明身份的方式。至少,这是我们目前将在信息系统上采取的做法。当然,我们可能在将来需要考虑某些身份验证方法比其他方法更安全,以及某些应用程序可能要求进行强多因素身份验证才能打开某些功能。但这个用例将通过向身份识别添加属性来处理,以考虑这一点。毕竟,即使在这种情况下,应用程序也不需要确切知道你进行了什么身份验证,而是知道它对提供的身份可以有多少信任。

在与 OAuth 关联的标准身份配置文件中已经存在一些类似的情况;例如,除了email属性外,联系配置文件还可以提供一个email_validated属性,该属性指定身份提供者已验证该标识用户确实控制了某个电子邮件地址。这是一种在不让身份消费者了解电子邮件是如何被验证的情况下增强对身份识别信任的方法。我们不会深入探讨身份验证,因为这个领域非常复杂,而我们想要精确建模的是授权领域。现在,我们只需记住,一个特定的用户可以通过不同的账户/方式来证明其身份进行身份验证。

接下来的重要方面是,用户可以属于不同的组,这最终将使他们在权利管理方面具有一些共同点。这些组可以形成一个层次树,以简化复杂的管理。请记住,我们仍然处于身份识别领域,因此属于一个组并不直接赋予你某些权利。组只是你身份的一部分,就像lastnamefirstname等其他任何属性一样,例如从 OpenID Connect/JWT/OAuth 标准中举例。

现在我们来讨论 IAM 的另一半,即授权管理。主要语义如下:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.2_B21293.jpg

图 13.2 – 授权语义

前面的图表当然只是一个例子,你可能有你自己的词汇来描述其中的术语。但这正是这种语义分析的目的;我知道有些人用“profiles”这个词来描述身份域中的人群组,有些人用“group”来讨论授权组,还有一些人用“profile”来代替“role”。但也有人使用其他词汇,重要的是不是谁是对的;只要没有建立标准,每个人都是。重要的是能够明确理解我们谈论的内容。在这本书中,一个组将是一个组织用户集的实体,这些用户在身份上相似,而一个角色将是一组经常一起使用的授权。

让我们更详细地解释一下权限的概念,它通过指向资源和一个操作(或多个,如果这在你的模型中更容易)来定义。例如,从数据引用服务中删除书籍可能是只有一些编辑有权利做的事情;然后我们会通过指向book资源和DELETE动词来设计相应的权限。使用基于 REST 的词汇当然是故意的——首先,它使解释我们想要表达的意思更加精确;其次,它允许在软件中精确地对齐将要发生的事情。在这种情况下,这个权限将与向/api/books API 发送DELETE动词的可能性相关联,因此它在书籍数据引用服务中得到了无任何混淆的实现。

当然,一些权限是相互关联的——高级编辑不仅能够删除书籍,还能创建、修改和阅读它们。这就是角色发挥作用的地方——将许多有意义的权限组合在一起。这也是语义也很重要的地方。命名编辑角色是一个困难的选择,因为我们倾向于将“编辑”这个词用于两件本质上不同的事情:当“编辑”用于识别组时,所有用户都属于这个组,而对于角色则使用像book-editor这样的名称。

语义在另一个领域也很重要——由于信息系统中有多个应用程序,并且每个应用程序(至少是数据引用服务)都处理特定的资源,因此在角色的名称中指定主要资源是很重要的;否则,它们会相互混淆。顺便说一下,这就是我们将如何将前两个模式分组的方式,展示“权利管理目标”的多样性相对于识别关注点的唯一性:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.3_B21293.jpg

图 13.3 – 每个应用程序负责其授权

在我们更详细地讨论授权框中包含的内容之前,让我们对许多应用程序中处理 IAM 的方式以及如何使用它来实现业务/IT 的整洁对齐进行一次有用的偏离。

IAM 实施的偏离

在大多数现有的信息系统中,身份验证仍然由许多应用程序直接处理,导致这里所表示的众所周知的反模式:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.4_B21293.jpg

图 13.4 – IAM 在许多应用程序中的反模式

这些独特功能的多次实现是现有信息系统中观察到的最常见的不对齐模式之一。这不仅导致账户的重复,使得管理访问权限更加困难,还导致不同密码的重复,这对用户来说是一个痛点,并迅速引发安全问题,因为许多用户将在他们的业务应用程序中跨业务使用相似的密码,这使得密码泄露突然更具影响,因为攻击面增加了。

公司常用的一种方法来弥补这种困难是自动化“新来者”流程,并在信息系统的每个应用程序中实施某种工具,以自动创建账户。除非你只有遗留应用程序,并且没有现代化系统的意图(例如,因为活动将在几年内关闭),否则这始终是可能采取的最糟糕的行动,因为它往往会固化问题——既然你已经向系统中添加了另一个(可能成本高昂)的组件,你将更不愿意再次更改它。以下图表显示了这种方法的第二个反模式:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.5_B21293.jpg

图 13.5 – 新来者耦合版本的过程

此图表显示了所有额外的问题:

  • 该流程设计在上层功能层,但业务人员无法修改它,因为它的执行基于由ETL应用程序执行的任务,因此只能由技术人员修改,这造成了一些时间耦合(法规的变化将在业务需要时不会应用,而是在 IT 部门能够在其众多项目中抽出时间时应用)。

  • 谈到 IT 需要做的许多事情,你是否注意到 BPMN 中的唯一参与者是IT?这是合理的,因为所有任务都设计为自动化,IT 被认为负责管理软件内的用户,仅仅因为他们是安装它或知道如何访问 API 的人。这是一个非常普遍的问题;而不是让功能管理员对其应用程序承担全部责任,他们完全依赖 IT 来做这件事。虽然这可以被认为是技术任务的正常情况,但在这个案例中,这是一个问题,因为信任 IT 添加用户并确定他们的默认权限可能会成为监管灾难的配方。毕竟,你怎么能责怪一个处理了会计紧急工单的实习生,他通过创建一个默认密码的用户来解决问题,却不知道在这个遗留应用程序中,用户默认拥有全部权限,这允许新来的用户在公司上班的第一天就访问公司的银行账户并将它们清空?

  • 该流程直接在 ETL 应用程序内部实现,这是最大的不匹配反模式。如果你继续沿着这个方向前进,很快,公司的所有业务流程都将依赖于一个软件,而这个软件不仅是你的 IT 系统中的单点故障。如果它被停止使用怎么办?如果编辑突然提高价格怎么办?如果发生一般性故障怎么办?

  • 在某些情况下,实施人员可能足够幸运,能够调用一个良好、向后兼容且文档齐全的 API,例如在Application A上,这允许进行某种解耦,甚至有可能在 BCM 中公开此 API。但在Application B中,API 直接与应用程序的库进行通信,这使得这种互操作性对版本变更非常脆弱。在Application C中,情况甚至更糟,因为找到的自动化创建账户的唯一方法是将行直接插入数据库。在下一个版本中,行为可能会变得完全不可预测,或者甚至在生产中推出时就会发生,因为你忘记在脚本中持久化的重要部分,等等。

之前的方法倾向于将这种反模式嵌入到系统中,其中每个应用程序都负责自己的标识甚至身份验证,而它应该只处理授权(这个反模式必须保留,因为应用程序处理资源,权限适用于这些资源)。相反,正确的做法是逐步采用以下正确的模式:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.6_B21293.jpg

图 13.6 – IAM 责任的正确映射

在这种情况下,身份验证和认证责任由专门的软件实现(在我们的例子中,是一个 Apache Keycloak IAM 服务器,连接到 Microsoft AD 用户目录),而所有应用程序仍然负责它们各自管理的资源的授权,但它们指向这个唯一的身份验证特征,以应用正确的权限(再次强调,无需了解任何关于认证过程的信息)。当然,这不会在一天内完成;您需要逐步用支持外部化认证/识别的应用程序装备您的信息系统。如今,几乎所有现代企业级应用程序都这样做,如果它们是基于浏览器的,在某些情况下甚至可以使用前端来保护这些责任。而且由于您很可能会保留一些遗留应用程序,您最终会拥有一个“中途”的信息系统,如下所示,这已经好得多,也更易于处理:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.7_B21293.jpg

图 13.7 – 完美对齐版本的新手流程

不要因为图中增加的复杂性而感到沮丧;这仅仅是因为我增加了更多的细节——特别是硬件层,之前并未展示过。在这个信息系统部分,可以在图的右侧看到许多优点,但我们将现在更详细地讨论它们:

  • 现在可以将流程的实施专门化到任何工具上,并且它将不会与技术栈有任何耦合(除了调用 Apache Keycloak API 添加全局用户,但这极为罕见,因为这可以基于 LDIF 标准,并且软件更改对流程用户是不可见的)。

  • 如果需要修改流程——例如,为在第一版中遗忘的遗留应用程序添加另一个步骤——这可以由决策者独立完成。在新版本中,这个额外任务将像现有的遗留会计系统任务一样工作——当基于用户的任务完成时,会向应用程序的功能管理员发送一封电子邮件,附带添加所需用户的流程链接。完成操作后,这个人会点击收到的电子邮件中的链接,以表示任务已完成,这也会关闭流程。

  • 专门分配给 IT 部门的第一项任务仍然是手动的,因为需要填写一个表格(Apache Keycloak 的表格或——如这里所示——由 BPMN 引擎提供的表格,该表格会调用与 BCM 的“创建用户”功能关联的 Keycloak API)。如果 Keycloak 的 API 遵循 LDIF 标准,它可以被认为是与信息系统中的功能相关联的标准化唯一点,这使得在需要时替换 Keycloak 为其他软件变得更容易。

  • 此外,Keycloak 充当实际用户目录的间接层。如果这需要更改为另一个目录,或者甚至使用身份联合和多个目录,这对与“创建用户”功能关联的 API 的任何用户来说都是透明的。

当然,遗留应用程序的问题不会完全消失,但至少,在这个配置中,遗留的影响会逐渐减少,并且正确的功能已经准备好以应有的方式为新和更现代的应用程序工作。此外,遗留应用程序被隔离到一个孤岛中,将来丢弃它将更容易。在这个例子中,我们可以从移除流程中的任务开始,然后抑制带有其本地耦合的识别和认证功能的旧应用程序。最后,我们必须验证那个使用不受支持或异类、难以维护的操作系统旧行服务器是否在系统中扮演任何其他软件角色。

基于角色的访问控制和基于属性的访问控制模型

在对 IAM 实现进行了相当长——但希望是有用的——的离题之后,我们将回到之前的地方,即在一个好的信息系统中,识别和认证功能对所有应用程序都是唯一的,但授权功能对每个资源都是重复的。确实,只有处理资源的应用程序才知道如何处理其上的权限。在我们的书籍数据参考服务示例中,我们看到一个名为book-edition的角色是有意义的。但在一个存档系统中呢?我们可能会在那里找到像archivistreadonly-verifier这样的角色,但book-edition就没有意义了。

这并不是说我们找不到应用程序之间的共同角色名称;相反——相似的名字应该仔细考虑,因为它们并不意味着相同的事情。这就是为什么即使它经常发生,将角色命名为“管理员”是如此危险。当然,每个人都知道这意味着什么——拥有这个角色的用户可以在软件中执行每个操作。但是,具体来说,“一切”的定义可能因软件而异。如果你在你的用户目录中添加一个名为“管理员”的组,这个组本应意味着这个组中的用户应该在每个应用程序中拥有完全权限,那么混淆就会增加。

我个人建议将这种情况限制在domain-administrator上,并安排您的 IT 部门永远不要成为应用程序的功能管理员,而只是它们安装的机器的管理员(这并不阻止他们间接地查看或操作数据,但这又是另一个应该通过合同标准和行政行为的完全可追溯性来解决的问题)。

为了解释这一点,前述图表的更好表示方式如下:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.8_B21293.jpg

图 13.8 – 在权限上影响授权而不是资源

上一张图中的左侧并不那么详细,但这正是我们想要的。既然我们说授权应该基于身份,那么在实践中我们该如何做呢?最容易和最常用的方法之一如下:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.9_B21293.jpg

图 13.9 – 纯基于角色的访问控制方法

当角色与组或直接与用户关联(或称为“映射”)时,这种基于权利管理的范式被称为基于角色的访问控制RBAC)。这种方法的优点在于它非常容易实现。由于管理权利的人只看到角色,因此从他们的角度来看,图表甚至可以表示如下:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.10_B21293.jpg

图 13.10 – 记录的 RBAC 方法

这也简化了开发者的工作,因为他们只要尊重与角色关联的基于文本的权利定义,就可以选择他们偏好的任何角色实现方法,甚至可以混合使用:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.11_B21293.jpg

图 13.11 – RBAC 中的其他可能的角色实现

角色的文本定义可能会引起一些麻烦,因为文本的不精确性和知识随时间过时的可能性,它容易产生近似,尤其是如果编辑角色有高的人员流动率且/或没有清楚地记录其软件功能。

由于纯 RBAC 相当限制性,应用程序通常允许直接将细粒度权限映射到用户或组,如下所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.12_B21293.jpg

图 13.12 – 作为 RBAC 改进的细粒度权限

这扩展了可能性,但同时也使得功能管理员在情况超过仅仅例外时,跟踪赋予不同用户的权利变得更加困难。随着用户数量的增加,使用组和角色变得越来越重要。将一些权利管理责任委托出去的诱惑也随之增加,但必须用严格的规则来实施,并仔细培训人员,因为事情可能会迅速变得混乱,拥有相同职位名称的用户最终会拥有不同的权利,这取决于谁赋予了他们这些权利。更糟糕的是,一些用户最终获得了对软件的完全权限,因为新的功能管理员并不完全理解权限管理系统是如何工作的。这又是另一个原因,不要将这项责任交给 IT 部门,尽管这可能很有诱惑力,因为他们将控制应用程序的技术部分。

另一种扩展 RBAC 功能更复杂的方法是转向所谓的基于属性的访问控制ABAC)。在这个权利管理范式中,会设置一些规则,将标识符的属性与资源的属性相链接:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.13_B21293.jpg

图 13.13 – ABAC 方法

这使我们能够,例如,克服在样本DemoEditor信息系统中使用 RBAC 时可能遇到的限制,如果作者只是添加了一个book-edition角色。实际上,这个角色要么会给他们阅读和编写书籍的权利,books资源但不限于特定的书籍。

这是 ABAC 的工作,它将使用的属性如下:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.14_B21293.jpg

图 13.14 – 带有 BRMS 的 ABAC 实现

你会注意到权限仍然被表示出来——我们也可以包括角色——因为 ABAC 并不是排斥 RBAC,而是在其未来发展中与之相辅相成。

在这种情况下,技术上会发生以下情况:

  • 应用程序会在/api/books/978-2409002205上调用GET动词。

  • 这个请求会伴随着基于令牌的认证头。

  • JWT 令牌将包括提供作者内部标识符的自定义属性(或者另一种方法是将作者关联基于电子邮件或另一个标准属性)。

  • 在接收到这个请求后,图书参考服务应用程序会调用授权中心 API,并向它提供它所知道的所有关于请求的信息——传入的 JWT 生成的身份,请求的图书属性等等。

  • 授权应用程序会找到适用于该情况的规则——在这种情况下,对书籍的GET操作。

  • 它首先会检查传入的用户是否有author_id,并且这个 ID 与给定书籍的作者之一相关联(查看book_mainauthor_id属性,如果需要,还可以查看book_secondaryauthors_ids属性数组)。

  • 然后,它会检查最初的请求到书籍参考服务不包含像$expand=release-information这样的内容,因为作者将看不到这些数据。

  • 它会意识到需要检查作者没有被阻止,并调用一个GET请求到/api/authors/x24b72。这将使用具有完全读权限的特权账户进行,因为我们认为 BRMS 由于其系统中的功能,有正当的“知情权”。

  • 作为这种方法的替代方案,书籍参考服务可以提供书籍的扩展视图,就像调用/api/books/978-2409002205?$expand=authors一样。

  • 对于大多数高级授权系统,这三个检查会并行进行以节省时间。

  • 如果一切正常,BRMS 将向书籍参考服务的调用发送200 OK HTTP 响应。

  • 然后,书籍参考服务会授予请求的访问权限。

当然,如果这些步骤中发生任何错误,请求将被拒绝,并返回403 Forbidden状态码。这可能发生在规则不被遵守的情况下,也可能发生在 BRMS 系统没有及时响应的情况下。这种行为是预期的,因为所谓的“优雅降级”意味着,出于安全原因,系统不会冒任何风险来披露数据或允许任何操作,如果它不确定这是否被允许。这意味着授权是系统中的另一个 SPOF(单点故障),应该按照这个请求的服务级别进行操作。

我犹豫是否要讨论ReBAC(基于关系的访问控制),它看起来是 RBAC 和 ABAC 范式的良好补充,但在撰写本文时,它尚未达到足够成熟的阶段。简而言之,ReBAC 的原则是基于实体之间的链接来管理授权;因此,它与 DDD 有很强的联系。例如,这种方法允许你轻松地给某位作者在其书籍上赋予写权限,同时保持其他作者的书籍只有只读权限。当然,这也可以用 ABAC 来实现,但 ReBAC 通过基于关系而不是简单地基于属性来运作,使其变得稍微简单一些。要了解更多关于 ReBAC 的信息,你可以从en.wikipedia.org/wiki/Relationship-based_access_control开始,然后查看 OSO 关于这种模式的观点www.osohq.com/academy/relationship-based-access-control-rebac

OpenFGA([openfga.dev/](https://openfga.dev/))也是一个值得关注的开源项目,如果你需要一个干净的外部授权管理系统,并且支持 ReBAC。尽管它还处于起步阶段,但该项目已经被引用为云原生计算基金会项目。如果你想了解它能为你的授权需求做什么,最好的开始方式之一是调整沙盒中提供的示例(https://play.fga.dev/sandbox)。

XACML 方法

现在我们已经讨论了不同组织形式的权利管理,我们将开始讨论更多关于实现的内容,到现在,你肯定已经开始思考我们有哪些规范和标准可供选择。由于我们已经讨论了实现 ABAC 的步骤,研究最完整的规范之一并解释它如何适应这些 ABAC 步骤将很有趣。

XACML可扩展访问控制标记语言)指定了如何执行和管理访问控制。这是处理授权的最先进方法之一,它建立了五个不同的责任来实现这一点:

  • 策略管理点是定义规则的地方

  • 策略检索点是它们存储的地方

  • 策略决策点是决定应该采取哪个决策的引擎

  • 策略信息点是收集用于规则评估的必要附加属性的地方

  • 策略执行点是应用决策结果的地方

这五个责任如何在单个或多个应用程序中分布,定义了系统的复杂程度。在最简单的方法中,所有五个责任都可以在最终必须应用执行点的数据引用服务中实现(因为数据引用服务是拥有数据的一方,所以不能外部化)。在这种模式下,数据引用服务不仅存储数据,还存储规则,执行它们,并根据结果决定应该做什么。唯一可能仍然被视为外部责任的情况是,如果引用服务需要一些外部数据,但它也可以很好地存储这些数据。在这种情况下,责任会受到以下影响:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.15_B21293.jpg

图 13.15 – 所有授权责任集成到应用程序中

相比之下,这是我们之前讨论的责任组织形式中如何分配责任的方式:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.16_B21293.jpg

图 13.16 – 授权责任完全分散到专用服务中

在这种非常干净(但当然,设置成本更高)的方法中,每个责任都是完全分离的,BRMS 和数据参考服务一起工作,以协调它们:

  1. 在任何第一次交互之前,一个功能性的用户连接到 PAP 并设计规则(就像之前在 DMN 使用示例中所做的那样)。

  2. 这些规则存储在相关的数据库中,即 PRP。

  3. 参考书籍的服务接收初始请求。它不能自行做出决定,并将 PDP 委托出去。

  4. 它将调用部署的 BRE 的调用上下文,以便从中获取决定。

  5. PDP 需要检索规则以便处理。它可以调用 PRP,但幸运的是,在我们的情况下,它有一个本地副本,我们假设使用了 JBPM 服务器,控制台部署了一个用于规则执行的独立运行时容器。

  6. PDP 可能还需要一些额外的信息,它可以通过 PIP 收集,以检索作者的blocked状态。

  7. PDP 将其规则决策引擎的结果发送回参考书籍的服务。

  8. 与 PEP 一样,参考书籍的服务使用 PDP 发送的决定来允许(或不允许)访问其数据,并可能响应所呈现的 HTTP 响应。

在我们向您展示如何设置此配置的实际示例之前,让我再进行一次小插曲,这次是关于服务应该如何分离。

关于微服务粒度的小插曲

首先,让我们为一种不太复杂且更常见的情况绘制一个图表,其中每个数据参考服务除了 PEP 外还包含自己的 PRP 和 PDP。在这种情况下,PAP 通常是最低限度的,因为规则集成到代码中,不允许轻松管理,这意味着 PRP 仅仅是代码库本身。

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.17_B21293.jpg

图 13.17 – 数据克隆时的授权管理问题

你能发现潜在的问题吗?参考书籍的服务不持有作者的 PDP/PRP,这是合乎逻辑的,因为它对此不负责。然而,它仍然存储了作者数据的副本,以便快速响应如/api/books/978-2409002205?$expand=authors之类的 API 调用。这意味着,由于它不知道如何过滤这类数据,如果不小心处理,可能会造成机密数据的泄露。在四层图中,这个问题可以从一个奇怪的错位中看出,如下所示:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.18_B21293.jpg

图 13.18 – 四层图中授权反模式的表示

这种不匹配源于数据存储应用程序信任了授权。这种方式,由于数据被重复,实际上存在两种可能不同的方式来对相同的数据应用授权!这种情况也可能发生在我们在 BRMS 中外部化数据时,因为运行时和 PAP 可能没有同步,但在这个情况下,优势远比实际缺点更重要。事实上,JBPM 控制台和 BRE 运行时容器之间的解耦带来了很多附加价值——控制台是一个复杂的服务器,而运行时容器非常轻量;将它们分开是更好的选择,因为错误更容易在第一个中发生,而第二个应该有出色的服务水平。当控制台用于部署独立服务器时,它可能会崩溃,但这不会成为问题。相反,运行时可以变得极其健壮,因为它去除了几乎所有不是立即执行函数所必需的代码。控制台部署规则集版本的事实使得可以根据性能需求创建所需数量的运行时服务器(因此,你也避免了单点故障问题,因为这项服务被许多其他服务所需要,并且应该非常稳定),而不会存在任何一致性问题,这会是一个大问题(想象一下向你的客户解释他们的租户数据访问权在每次新请求中都会变化)。

然而,这并不意味着应该尽可能地将所有责任添加到尽可能多的服务和不同的流程中。当然,这可能是有用的,但正如在信息架构中经常发生的那样,最重要的是找到正确的平衡点。互联网上关于微服务和单体应用哪种更好的无意义讨论已经太多了,你几乎可以通过查看文章标题来推测文章的质量。当然,对“什么才是最好的”的唯一正确回答是,“这取决于”,任何合格的软件架构师都知道这并不是一个“一刀切”的情况。

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.19_B21293.jpg

图 13.19 – 服务粒度优缺点

我意识到我每隔一章就会说这句话,但重复一遍是有价值的——在业务功能的粒度中,什么应该是优先考虑的?如果你知道授权规则很少改变,并且等待新版本发布不是问题,那么可以直接在相关参考服务的代码中实现业务功能粒度中应该优先考虑的部分;这将带来最佳性能,并且如果你有克隆数据,你只需处理数据安全的问题。如果有问题,考虑在有任何疑问时调用其他参考服务;这也会是刷新你部分克隆数据的一种方式。相反,如果你可以预见授权规则将频繁更改或存在外部情况,例如法规,那么考虑逐步从你的数据参考服务中提取责任。预见这类事情确实是在过度预测和采用过多 DRY(Don’t Repeat Yourself)方法之间的一条细线,但这就是判断、长期的专业知识和从许多先前经验中吸取教训发挥作用的地方。

将授权应用于我们的示例信息系统

XACML 之前已经解释过了,但它是实施起来相当复杂的机制。此外,虽然存在几个产品,如 WSO²、Balana、Axiomatics 或 AT&T 的产品,但没有协议的参考实现。尽管这些产品在银行或保险等大型信息系统中都有自己的位置,但它们对于我们决定在示例中模拟的小型信息系统来说可能过大,因此我们将使用更轻量级且更接近主要互联网协议的方案。

Open Policy Agent 的替代方案

Open Policy Agent 是一个由云原生计算基金会支持的项目,它提出了描述策略语法的良好解耦。简而言之,OPA 对于 XACML 就像 REST 对于 SOAP 一样——一个轻量级的替代方案,以 20%的复杂性完成 80%的工作。为了展示如何将授权责任外部化,我们不会安装完整的 XACML 服务器,而是将使用 Docker 来定制一个授权引擎。

OPA 使用名为Rego的声明性语言来描述应用于数据以做出决策的策略。然后它可以执行这些策略,提供 JSON 结果,这些结果可以在其他服务或如果你使用 OPA 实现作为组件的情况下被利用。

技术上,将会发送如下请求到 OPA,并且它会响应请求的访问是否应该被授权:

{
    "input": {
        "user": "jpgou",
        "operation": "all",
        "resource": "books.content",
        "book": "978-2409002205"
    }
}

在这个例子中,用户jpgou请求对书籍的content花瓣的完全访问权限,该书籍在系统中的 ISBN 号为978-2409002205。如果 OPA 服务器批准了这个请求,它将响应如下:

{
    "result": {
        "allow": true
    }
}

在再次深入研究技术之前,我们需要明确从功能角度我们想要实现什么。

DemoEditor 的功能需求

让我们回到我们的 DemoEditor 示例,并描述从授权的角度应该做什么。当然,在一家出版社,作者有权限提供书籍内容并根据需要调整,但他们绝不应该能够阅读另一位作者书籍的内容,以避免剽窃甚至知识产权盗窃。由于有编辑负责作者,因此他们至少可以阅读他们管理的作者的书的内容,这是合乎逻辑的。另一方面,销售人员没有任何编辑责任,所以他们可能只知道一些关于书籍的信息来准备销售和订单,但没有理由了解任何关于编辑过程的信息。

在对 DemoEditor 权限管理中涉及的风险的简要描述中,很明显,纯 RBAC 将不足以满足需求,我们必须求助于 ABAC 来补充 RBAC,因为存在基于书籍属性的规则,即谁是作者,甚至其他信息,如作者与其编辑之间的联系。RBAC 本身是不够的,因为作者对自己书籍的权利比对其他作者书籍的权利更大,尽管他们都将从 author 授权配置文件中受益。

如下文将更详细地解释,我们还将添加一些规则,例如销售人员只能看到书籍达到一定状态后才能看到,或者另一个规则允许我们阻止不遵守编辑合同的作者的权利,并应拒绝其权限。为此,我们将使用我们在 第九章 中使用的相同隐喻,即为书籍的不同类别数据指定,就像将它们放在花朵的花瓣中,其中最核心的部分包含最重要的、定义实体的数据,如书籍的 ISBN 号码和标题。虽然根据授权规则定义这些花瓣可能很有吸引力,但重要的是要记住它们必须从功能约束中提取,而授权管理就是其中之一,但仍然只是其中之一。

创建授权策略

从定义授权策略开始,将允许我们同时做两件事:

  • 解释我们打算实施哪种授权行为以及书籍的数据引用服务应该如何工作

  • 探索一些 Rego 语法以及使用这种机制外部化授权时涉及的内容

当编写用于书籍数据引用服务授权管理的policy.rego文件(只是一个任意的名称)时,我们需要从包名开始,这允许我们在同一引擎中执行不同组的规则时将规则分开。文件的开始部分还包含一些导入特定关键字和函数(OPA 支持插件和语法扩展以简化其使用)或准备数据的说明(我们将在本章后面进一步讨论):

package app.abac
import future.keywords
import data.org_chart

文件的主体部分通常从一个模式开始,其中主要授权属性,我们将称之为allow,被分解为几个更细粒度的决策策略。我们想要实现的是一个授权引擎,当暴露于某种访问类型时,将发送一个结果,表明是否应该授予这种访问。我们将在演示如何应用规则引擎时回到这部分,但现在,让我们继续政策定义文件,并展示我们讨论的行为将如何实现:

default allow := false
allow if {
    permission_associated_to_role
    no_author_blocking
    no_status_blocking
    authors_on_books_they_write
    editors_on_books_from_authors_they_manage
}

为了实施安全最佳实践,默认情况下禁止访问。这允许所谓的“优雅降级”——如果授权子系统出现问题,它将默认到最安全的情况。在我们的情况下,最安全的做法是不允许访问,因为当然,缺乏可用性比向那些本不应该看到这些数据的人披露数据的问题要小得多,这种事件可能带来的所有商业后果也是如此。这正是前面代码的第一行所涉及的内容——将allow属性的默认值设置为false

第二个操作说明,为了使allow变为true,我们需要通过五个不同的决策,每个决策都需要评估为true。当然,这些决策的命名方式使得它们易于理解和调试(正确设置授权是一项挑战,但几乎永远不会在第一次尝试时就达到 100%正确)。文件的其他部分基本上将详细说明这五个主要决策,但在我们声明它们的工作方式之前,我们需要准备一些数据。确实,正如我们将在下一节中解释的,我们需要一些引用服务数据,以便引擎做出决策。例如,既然我们声明编辑应该能够访问他们指导的作者的书,那么引擎了解作者和编辑之间的链接将是一个明显的需求。其他一些信息,如书籍的状态属性,也将是有用的。所有这些数据将主要来自数据引用服务到引擎,但将是基本数据,我们可能在实际使用整个数据集来推断决策之前从中获取一些信息。

其中一条信息是当前用户拥有的角色。如前所述,我们将需要一些 RBAC(基于角色的访问控制)的片段,即使它不足以满足所有需求。这意味着用户将与一些角色相关联,其中一些是直接关联的,而另一些则是通过属于识别组的用户间接关联的。以下语法精确地表达了这一点:

user_groups contains group if {
    some group in data.directory[input.user].groups
}
user_group_roles contains role if {
    some group in user_groups
    some role in data.group_mappings[group].roles
}
user_direct_roles contains role if {
    some role in data.user_mappings[input.user].roles
}
user_roles := user_group_roles | user_direct_roles

组可以在一个名为 directory 的数据块中找到,通过查看由名为 user 的输入变量值指定的列表中的条目。一旦在这个列表中找到该用户,groups 属性将提供识别组的列表。然后,这些组将被用来检索与组关联的角色,利用一个名为 group_mappings 的集合。相同的逻辑也将应用于直接应用于用户的角色的集合,并且这两个角色列表将通过前面代码中显示的最后一个操作简单地合并。

我们还需要有关可能与用户关联的作者的信息。这对我来说还没有完全解释清楚,只是简要地提到,即使作者实际上不是组织的一部分,或者至少不是其员工,他们也会使用 DemoEditor 访问信息系统。这意味着,首先,必须为他们提供访问权限(我们将在实现相关函数时回到如何做到这一点)。这也意味着,在信息系统中应该有某种方式将这两个实体关联起来。一种相当常见的方法是使用经过验证的 email 属性将它们联系起来。目前,我们只需考虑用户信息包含在 author 实体中。检索关联的规则相当容易编写——它只是遍历作者列表,如果与作者关联的 user 是请求访问相关的用户,那么该作者就是我们正在寻找的:

user_author contains author if {
    some author in data.authors
    author.user == input.user
}

实际上,我们应该提到作者而不是仅仅一个作者,因为我们知道在功能上可能只有一个,但在技术上,我们甚至将使用一个列表,即使变量的名称仍然是单数形式,user_author

同样的情况也适用于请求中提到的书籍,因为我们需要从数据列表中检索其 ID,以便能够根据书籍属性上的规则做出一些决策:

book contains b if {
    some b in data.books[input.book]
}

在作者的情况下,我们还需要检索他们作为作者所写的书籍列表,因为一些规则也适用于这一点:

author_books contains book if {
    some author in user_author
    some b in data.books
    b.editing.author == author.id
}

现在所有必要的数据都已收集,我们可以开始讨论规则本身,分别考虑五个规则单元,并将它们进一步分解。首先适用的规则是权限应由与请求关联的用户拥有。仅授予访问权限是不够的,但这仍然是一个必要的约束。为了知道用户是否应该被允许访问他们请求的资源,应该研究角色提供的所有访问。如果其中之一对应于请求的资源类型和操作,那么它就是一个匹配项,权限将被应用:

permission_associated_to_role if {
    some access in user_accesses_by_roles
    access.type == input.resource
    access.operation == input.operation
}

以下规则使得以下情况发生:如果某人获得了books.contentbooks.salesbooks.editing(对应数据引用服务的花朵的一个花瓣),那么他们自动获得花朵核心的权利,这是合乎逻辑的,因为如果能够访问某些数据,但不能将其与特定实体关联,那么这不会非常有用:

permission_associated_to_role if {
    some access in user_accesses_by_roles
    "books" == input.resource
    access.operation == input.operation
}

由于我们有两个具有相同名称(permission_associated_to_role)的规则,而不是在同一组内具有不同名称的两个规则,因此在处理上存在很大差异,这意味着规则被认为是通过“或”运算符分开的(结果为真不需要所有条件都为真,例如之前为allow设置的),我们甚至还将添加第三个情况,即当访问提供的内容包含all作为操作时,这部分策略应该被授予。在这种情况下,无论请求的操作是readwrite还是任何其他值,都将被授予(至少基于这个标准):

permission_associated_to_role if {
    some access in user_accesses_by_roles
    access.type == input.resource
    access.operation == "all"
}

现在,问题应该是,user_accesses_by_roles是如何计算的?这次,它稍微复杂一些,有一个子决策会遍历一些树状层次结构,包括在提供的数据的roles实体中包含的配置文件及其相关访问。我们将在下一节中返回定义,但就目前而言,重要的是要知道我们将使用一个层次结构来设置经理在顶部,然后是销售人员和平面编辑,以及作者在其编辑之下。这种方法中有趣的部分将是如何使上面的角色在树中接收下面角色的所有权限。毕竟,如果销售人员有权利编写销售值,那么他们的经理至少应该有相同的权利。语法更难阅读,但不要担心这一点,因为 OPA 文档写得很好,而且有很多示例,即使是复杂的规则也有:

user_accesses_by_roles contains access if {
    some role in user_roles
    some access in permissions[role]
}
roles_graph[entity_name] := edges {
    data.roles[entity_name]
    edges := {neighbor | data.roles[neighbor].parent == entity_name}
}
permissions[entity_name] := access {
    data.roles[entity_name]
    reachable := graph.reachable(roles_graph, {entity_name})
    access := {item | reachable[k]; item := data.roles[k].access[_]}
}

当处理规定作者只能对其所写的书有权利的规则时,我们需要应用一个小技巧。像往常一样,我们首先将访问设置为false,以遵守安全最佳实践。如果我们可以在书籍作者的情况下跟随作者链接,或者简单地在这种情况下用户不是书籍作者,我们将将其设置为true。这听起来可能过于宽松,但请记住,这并不是完整的成果。在这种情况下,这仅仅是关于作者和他们的书籍之间链接的决策部分;如果我们处理的是编辑,这个规则根本不适用,但其他一些规则将适用,并且所有这些规则都需要一致,以便最终的总结性决策是积极的。结果是以下内容:

default authors_on_books_they_write := false
authors_on_books_they_write if {
    some role in user_roles
    role != "book-writer"
}
authors_on_books_they_write if {
    some role in user_roles
    role == "book-writer"
    some author in user_author
    some b in data.books
    b.editing.author == author.id
    b.id == input.book
}

到现在为止,你应该开始更熟悉Rego语法,但全球授权方案的第三部分仍然需要一些思考,因为它需要你遍历整个组织结构以检索编辑和“他们”的作者之间的链接,因为我们需要应用规则,即编辑只能对其管理的作者的书有权利:

default editors_on_books_from_authors_they_manage := false
editors_on_books_from_authors_they_manage if {
    some role in user_roles
    role != "book-edition"
}
book_author contains b.editing.author if {
    some b in data.books
    b.id == input.book
}
editors_on_books_from_authors_they_manage if {
    some role in user_roles
    role == "book-edition"
    some author in book_author
    some b in data.books
    b.editing.author == author
    b.id == input.book
    user_hierarchy_ok
}
foundpath = path {
    [path, _] := walk(org_chart)
    some author in book_author
    path[_] == author
}
user_hierarchy_ok if {
    some user in foundpath
    user == input.user
}

全球决策的第四部分更简单——它认为销售人员如果一本书不在已发布或存档状态,就不能看到这本书。同样,为了考虑到“或”方法,我们需要计算两次readable_for_sales属性,最初出于安全原因设置为false,分别对应允许销售人员访问的状态值:

default no_status_blocking := false
no_status_blocking if {
    some role in user_roles
    role != "book-sales"
}
default readable_for_sales := false
readable_for_sales if {
    book.status == "published"
}
readable_for_sales if {
    book.status == "archived"
}
no_status_blocking if {
    some role in user_roles
    role == "book-sales"
    readable_for_sales
}

决策的第五和最后一部分甚至更简单,我们不会解释代码,只解释规则——如果一个作者已被阻止,他们不能访问任何书籍:

default no_author_blocking := false
no_author_blocking if {
    some role in user_roles
    role != "book-writer"
}
no_author_blocking if {
    some role in user_roles
    role == "book-writer"
    some user in user_author
    user.restriction == "none"
}

语法完成之后,我们需要第二种类型的信息来做出决策。这就是决策数据的内容。

添加一些数据以便做出决策

在上一节中,我们已暗示了数据应提供(甚至从其他数据中推断)以便规则引擎能够做出决策的事实。接下来,我们将展示我们应该为我们的示例设置哪些类型的信息。首先,我们需要角色的定义(记住,角色是一组权利,每个权利由资源类型和操作类型组成):

"roles": {
    "book-direction": { "access": []},
    "book-sales": { "parent": "book-direction", "access": [{ "operation": "all", "type": "books.sales" }]},
    "book-edition": { "parent": "book-direction", "access": [{ "operation": "all", "type": "books.editing" }]},
    "book-writer": { "parent": "book-edition", "access": [{ "operation": "read", "type": "books.editing" }, { "operation": "all", "type": "books.content" }]}
}

前面的 JSON 内容还定义了parent的概念,它创建了一个角色树,例如,book-salesbook-edition被放置在book-direction下,这意味着导演将自动获得默认授予销售人员的所有权限,以及授予编辑的权限,当然还包括直接在角色本身上描述的权限。

将有关书籍的一些数据发送,以便应用需要这些数据的特定规则。在以下示例中,我展示了一个列表,因为我测试了不同的组合。在实际使用中,我们可以简单地发送与请求 OPA 决定访问权限的唯一书籍相关的数据,以保持性能。以下是相关数据:

"books": {
    "978-2409002205": { "id": "978-2409002205", "title": "Performance in .NET", "editing": { "author": "00024", "status": "published" }},
    "978-0000123456": { "id": "978-0000123456", "title": ".NET 8 and Blazor", "editing": { "author": "00025", "status": "draft" }}
}

注意,前面的代码不是用 JSON 数组表达,而是作为一个结构。关于作者的数据也是如此:

"authors": {
    "00024": { "id": "00024", "firstName": "Jean-Philippe", "lastName": "Gouigoux", "user": "jpgou", "restriction": "none" },
    "00025": { "id": "00025", "firstName": "Nicolas", "lastName": "Couseur", "user": "nicou" }}

组织结构图允许我们定义谁是“大老板”(frfra),定义哪些销售人员编辑在他之下(我的例子中有三个人),最后,将两位作者放置在编辑之下,代号 mnfra

"org_chart": {
    "frfra": {
        "frvel": {},
        "cemol": {},
        "mnfra": {
            "00024": {},
            "00025": {}
        }
    }
}

然后,我们模拟用户目录可能会发送的内容——例如,每个用户所属的组。这有点人为,因为我们通常会从通过身份验证传递的 JWT 令牌中提取这些信息,但这就是我们在代码中遇到困难时将要做的。现在,我们将保持以下树状结构的象征性:

"directory": {
    "frfra": {"groups": ["board"]},
    "frvel": {"groups": ["commerce", "marketing"]},
    "cemol": {"groups": ["commerce"]},
    "mnfra": {"groups": ["editors", "quality"]},
    "jpgou": {"groups": ["authors"]},
    "nicou": {"groups": ["authors"]}
}

当然,现在我们有了组,我们需要映射来将它们链接到角色,以实现真正的 RBAC 方法:

"group_mappings": {
    "board": { "roles": ["book-direction"] },
    "commerce": { "roles": ["book-sales"] },
    "editors": { "roles": ["book-edition"] },
    "authors": { "roles": ["book-writer"] }
}

由于我们决定尽可能完整,我们将允许——除了纯 RBAC 以外——也声明一些用户和角色之间的直接关联,而不需要组作为中介:

"user_mappings": {
    "frvel": { "roles": ["book-edition"] }
}

现在一切准备就绪,服务器可以输出一些结果(规则和数据),我们可以进行下一步,即设置一个真实的 OPA 服务器,用这两个文件给它提供数据,并尝试一些决策。

基于 Docker 的 OPA 服务器部署

使用 Docker 部署如此简单,不使用它来测试 OPA 就会麻烦不断。运行服务器的命令行非常简单:

docker run -d -p 8181:8181 --name opa openpolicyagent/opa run --server --addr :8181

服务器启动后,我们将开始调用将其中的策略定义推送到服务器:

curl --no-progress-meter -X PUT http://localhost:8181/v1/policies/app/abac --data-binary @policy.rego

使用另一个调用发送数据:

curl --no-progress-meter -X PUT http://localhost:8181/v1/data --data-binary @data.json

最后,我们能够使用以下代码中展示的简单请求来测试 OPA 服务器:

{
    "input": {
        "user": "jpgou",
        "operation": "all",
        "resource": "books.content",
        "book": "978-2409002205"
    }
}

当使用以下命令将此文本内容发送到使用 POST 的 API 时,OPA 服务器将以 JSON 格式发送结果,其余的命令负责检索我们感兴趣的响应部分:

curl --no-progress-meter -X POST http://localhost:8181/v1/data/app/abac --data-binary @input.json | jq -r '.result | .allow'

如果直接执行,这通常会发送 true,意味着请求的上下文由 OPA 服务器授权。如果你删除命令的最后部分并显示整个响应,你将得到如下内容,这对于调试非常有用,因为它显示了所有中间决策的值:

{
  "result": {
    "allow": true,
    "author_books": [
      [
        "978-2409002205",
        "Performance in .NET",
        {
          "author": "00024",
          "status": "published"
        }
      ]
    ],
    "authors_on_books_they_write": true,
    "book": [
      "978-2409002205",
      "Performance in .NET",
      {
        "author": "00024",
        "status": "published"
      }
    ],
    "editors_on_books_from_authors_they_manage": true,
    "foundpath": [
      "frfra",
      "mnfra",
      "jpgou"
    ],
    "no_author_blocking": true,
    "no_status_blocking": true,
    "permission_associated_to_role": true,
    "permissions": {
      "book-direction": [
        {
          "operation": "all",
          "type": "books.content"
        },
        {
          "operation": "all",
          "type": "books.editing"
        },
        {
          "operation": "all",
          "type": "books.sales"
        },
        {
          "operation": "read",
          "type": "books.editing"
        }
      ],
      "book-edition": [
        {
          "operation": "all",
          "type": "books.content"
        },
        {
          "operation": "all",
          "type": "books.editing"
        },
        {
          "operation": "read",
          "type": "books.editing"
        }
      ],
      "book-sales": [
        {
          "operation": "all",
          "type": "books.sales"
        }
      ],
      "book-writer": [
        {
          "operation": "all",
          "type": "books.content"
        },
        {
          "operation": "read",
          "type": "books.editing"
        }
      ]
    },
    "readable_for_sales": false,
    "roles_graph": {
      "book-direction": [
        "book-edition",
        "book-sales"
      ],
      "book-edition": [
        "book-writer"
      ],
      "book-sales": [],
      "book-writer": []
    },
    "user_accesses_by_roles": [
      {
        "operation": "all",
        "type": "books.content"
      },
      {
        "operation": "read",
        "type": "books.editing"
      }
    ],
    "user_author": [
      {
        "firstName": "Jean-Philippe",
        "id": "00024",
        "lastName": "Gouigoux",
        "restriction": "none",
        "user": "jpgou"
      }
    ],
    "user_direct_roles": [],
    "user_group_roles": [
      "book-writer"
    ],
    "user_groups": [
      "authors"
    ],
    "user_hierarchy_ok": true,
    "user_roles": [
      "book-writer"
    ]
  }
}

测试授权

这些示例授权并不复杂,但复杂程度足以手动处理起来困难。有许多具体案例提出了问题。例如,如果我说一位经理请求访问尚未出版的书籍的销售数据,你认为会发生什么?更重要的是,你认为应该发生什么?

此外,Rego 语法的学习曲线很陡峭。编写之前展示的规则花费了我几个小时,如果不是一整天,因为我不是一名专家,而且我不确定它们是否确实按照我预期的那样工作。

这就是为什么拥有优秀的测试人员至关重要,他们能够定义测试活动,找出所有边缘情况,与产品负责人/客户进行讨论等等,这将非常有帮助。这样的测试活动将使用 Gherkin 语法创建(见以下示例场景)。如果您使用像 SpecFlow 这样的工具,您可以创建许多这些场景并自动测试它们,以确保规则的语法修改不会破坏任何东西。一旦您的完整测试集准备就绪,您将获得一个报告,显示所有测试系列是否通过,最终让您放心,您考虑到的所有模式都是正确的。

为了在 Visual Studio 中安装 SpecFlow,请遵循 docs.specflow.org/projects/getting-started/en/latest/index.html 上的说明。然后,您需要创建一个类型为 SpecFlow Project 的项目。结果将是一些示例类,展示如何使用 SpecFlow,我们将根据我们的特定需求对其进行调整,即测试我们在 OPA 中设置的授权规则。在这里,我们将使用 xUnit 作为底层测试框架,但当然,您可以根据自己的喜好进行修改:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.20_B21293.jpg

图 13.20 – 创建 SpecFlow 项目

创建的项目结构将基于一个名为 Calculator 的示例,并且第一个动作是将名称更改为符合我们自己的目的,即测试 OPA:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.21_B21293.jpg

图 13.21 – SpecFlow 项目结构

在第一步中,OPA.feature 的内容被修改为以下 Gherkin 内容:

Feature: OPA
Scenario: An author has all rights to the content of their book
    Given book number 978-2409002205 with author id 00024 is in workInProgress status
    And user jpgou belongs to group authors
    And organizational chart is {"frfra":{"frvel":{},"cemol":{},"mnfra":{"00024":{},"00025":{}}}}
    And user jpgou is associated with author 00024 who has a level of restriction none
    When the user jpgou requests all access to the books.content petal of the book number 978-2409002205
    Then access should be accepted
Scenario: An author has no rights to the content of the book from another author
    Given book number 978-2409002205 with author id 00024 is in workInProgress status
    And user jpgou belongs to group authors
    And organizational chart is {"frfra":{"frvel":{},"cemol":{},"mnfra":{"00024":{},"00025":{}}}}
    And user jpgou is associated with author 00024 who has a level of restriction none
    When the user nicou requests read access to the books.content petal of the book number 978-2409002205
    Then access should be refused
Scenario: An author that has been blocked has no rights, even on their own books
    Given book number 978-2409002205 with author id 00024 is in workInProgress status
    And user jpgou belongs to group authors
    And organizational chart is {"frfra":{"frvel":{},"cemol":{},"mnfra":{"00024":{},"00025":{}}}}
    And user jpgou is associated with author 00024 who has a level of restriction blocked
    When the user jpgou requests all access to the books.content petal of the book number 978-2409002205
    Then access should be refused
Scenario: An editor has all rights to the content of the books from the authors they manage
    Given book number 978-2409002205 with author id 00024 is in workInProgress status
    And user jpgou belongs to group authors
    And user mnfra belongs to group editors
    And organizational chart is {"frfra":{"frvel":{},"cemol":{},"mnfra":{"00024":{},"00025":{}}}}
    And user jpgou is associated with author 00024 who has a level of restriction none
    When user mnfra requests all access to the books.content petal of the book number 978-2409002205
    Then access should be accepted
Scenario: An editor has no rights to the content of the books from the authors they do not manage
    Given book number 978-2409002205 with author id 00024 is in workInProgress status
    And user jpgou belongs to group authors
    And user mnfra belongs to group editors
    And organizational chart is {"frfra":{"frvel":{},"cemol":{},"mnfra":{"nicou":{}}}}
    And user jpgou is associated with author 00024 who has a level of restriction none
    When user mnfra requests all access to the books.content petal of the book number 978-2409002205
    Then access should be refused
Scenario: Refusing salesperson access to work-in-progress book
    Given book number 978-2409002205 with author id 00024 is in workInProgress status
    And user frvel belongs to the group commerce
    And organizational chart is {"frfra":{"frvel":{},"cemol":{},"mnfra":{"00024":{},"00025":{}}}}
    When the user frvel requests read access to the books.content petal of the book number 978-2409002205
    Then access should be refused

这种语法应该很容易阅读,即使对于非开发者来说也是如此;行为驱动开发的思想是,功能人员能够用这种语言表达他们的需求,这种语言称为 Gherkin(为了简单起见,我们在这里展示了比这更复杂的功能)。为了将这种 Gherkin 语法转换为自动化的 xUnit 测试,我们需要在场景的行和实现此测试部分的功能的 C# 函数之间建立对应关系。这是在 OPAStepDefinitions.cs 文件中完成的。例如,对于 GivenAnd 关键字(它们具有相同的概念),相应的函数将如下所示:

[Given("book number (.*) with author id (.*) is in (.*) status")]
public void AddBookWithStatus(string number, string authorId, string status)
{
    _books.Add(new Book() { Number = number, AuthorId = authorId, Status = status });
}
[Given("user (.*) belongs to group (.*)")]
public void AddUserWithGroup(string login, string group)
{
    _users.Add(new User() { Login = login, Group = group });
}
[Given("user (.*) is associated to author (.*) who has level of restriction (.*)")]
public void AddAuthor(string login, string authorId, string restrictionLevel)
{
    _authors.Add(new Author() { Login = login, Id = authorId, Restriction = restrictionLevel });
}
[Given("organizational chart is (.*)")]
public void SetOrganizationChart(string orgChart)
{
    _orgChart = orgChart;
}

在包含此函数的类的初始化部分,我们当然会有一个成员来存储书籍(以及为测试场景所需的其他实体所需的其他列表):

private static HttpClient _client;
private static List<Author> _authors;
private static List<Book> _books;
private static List<User> _users;
private static string _orgChart;
private static string _result;

相应的类包含所有需要改变规则上下文的内容。正如您所看到的,作者的名字和姓氏尚未整合到模型中,因为我们有充分的信心认为它们不会影响规则引擎的输出:

public class Author
{
    public string Id { get; set; }
    public string Login { get; set; }
    public string Restriction { get; set; }
}
public class Book
{
    public string Number { get; set; }
    public string Status { get; set; }
    public string AuthorId { get; set; }
}
public class User
{
    public string Login { get; set; }
    public string Group { get; set; }
}
Some methods will be used to initiate the values for each scenario, and also for the entire feature:
[BeforeFeature]
public static void Initialize()
{
    _client = new HttpClient();
    _client.BaseAddress = new Uri("http://localhost:8181/v1/");
}
[BeforeScenario]
public static void InitializeScenario()
{
    _authors = new List<Author>();
    _books = new List<Book>();
    _users = new List<User>();
}

这将使我们能够在调用与 When 关键字关联的函数时,实现所谓的系统测试(我们想要验证的是 OPA 服务器,它应该已经启动并使用 Rego 内容进行了定制,并将监听我们设置中的端口 8181):

[When("user (.*) request (.*) access to the (.*) petal of the book number (.*)")]
public void ExecuteRequest(string login, string access, string perimeter, string bookNumber)
{
    StringBuilder sb = new StringBuilder();
    sb.AppendLine("{");
    sb.AppendLine("    \"roles\": {");
    sb.AppendLine("        \"book-direction\": { \"access\": []},");
    sb.AppendLine("        \"book-sales\": { \"parent\": \"book-direction\", \"access\": [{ \"operation\": \"all\", \"type\": \"books.sales\" }]},");
    sb.AppendLine("        \"book-edition\": { \"parent\": \"book-direction\", \"access\": [{ \"operation\": \"all\", \"type\": \"books.editing\" }]},");
    sb.AppendLine("        \"book-writer\": { \"parent\": \"book-edition\", \"access\": [{ \"operation\": \"read\", \"type\": \"books.editing\" }, { \"operation\": \"all\", \"type\": \"books.content\" }]}");
    sb.AppendLine("    },");
    sb.AppendLine("    \"books\": {");
    for (int i=0; i<_books.Count; i++)
    {
        Book b = _books[i];
        sb.Append("        \"" + b.Number + "\": { \"id\": \"" + b.Number + "\", \"title\": \"***NORMALLY NO IMPACT ON RULES***\", \"editing\": { \"author\": \"" + b.AuthorId + "\", \"status\": \"" + b.Status + "\" }}");
        if (i < _books.Count - 1) sb.AppendLine(","); else sb.AppendLine();
    }
    sb.AppendLine("    },");
    sb.AppendLine("    \"authors\": {");
    for (int i = 0; i < _authors.Count; i++)
    {
        Author a = _authors[i];
        sb.AppendLine("        \"" + a.Id + "\": { \"id\": \"" + a.Id + "\", \"firstName\": \"***NORMALLY NO IMPACT ON RULES***\", \"lastName\": \"***NORMALLY NO IMPACT ON RULES***\", \"user\": \"" + a.Login + "\", \"restriction\": \"" + a.Restriction + "\" }");
        if (i < _authors.Count - 1) sb.AppendLine(","); else sb.AppendLine();
    }
    sb.AppendLine("    },");
    sb.AppendLine("    \"org_chart\": " + _orgChart + ",");
    sb.AppendLine("    \"directory\": {");
    for (int i = 0; i < _users.Count; i++)
    {
        User u = _users[i];
        sb.AppendLine("        \"" + u.Login + "\": {\"groups\": [\"" + u.Group + "\"]}");
        if (i < _users.Count - 1) sb.AppendLine(","); else sb.AppendLine();
    }
    sb.AppendLine("    },");
    sb.AppendLine("    \"group_mappings\": {");
    sb.AppendLine("        \"board\": { \"roles\": [\"book-direction\"] },");
    sb.AppendLine("        \"commerce\": { \"roles\": [\"book-sales\"] },");
    sb.AppendLine("        \"editors\": { \"roles\": [\"book-edition\"] },");
    sb.AppendLine("        \"authors\": { \"roles\": [\"book-writer\"] }");
    sb.AppendLine("    },");
    sb.AppendLine("    \"user_mappings\": {");
    sb.AppendLine("    }");
    sb.AppendLine("}");
    var response = _client.PutAsync("data", new StringContent(sb.ToString(), Encoding.UTF8, "application/json")).Result;
    string input = "{ \"input\": { \"user\": \"" + login + "\","
        + " \"operation\": \"" + access + "\","
        + " \"resource\": \"" + perimeter + "\","
        + " \"book\": \"" + bookNumber + "\" } }";
    response = _client.PostAsync("data/app/abac", new StringContent(input, Encoding.UTF8, "application/json")).Result;
    if (response != null)
    {
        _result = response.Content.ReadAsStringAsync().Result;
    }
}
}

测试执行的最后一部分是由与 Then 关键字关联的方法执行的,该方法运行断言以模拟自动化测试:

[Then("access should be (.*)")]
public void ValidateExpectedResult(string expectedResult)
{
    JsonTextReader reader = new JsonTextReader(new StringReader(_result));
    reader.Read(); // Get first element
    reader.Read(); // Read result attribute
    reader.Read(); // Get element for result
    reader.Read(); // Read allow attribute
    bool? actual = reader.ReadAsBoolean(); // Get boolean value for allow attribute
    if (actual is null)
        throw new ApplicationException("Unable to find result");
    bool? expected = null;
    if (expectedResult == "refused") expected = false;
    if (expectedResult == "accepted") expected = true;
    if (expected is null)
        throw new ApplicationException("Unable to find expected value");
    Assert.Equal(expected, actual);
}

您现在可以通过从菜单访问或使用 Ctrl + E + T 快捷键来显示测试资源管理器。测试可能最初不会显示,您可能需要运行解决方案生成来使它们出现。一旦它们显示出来,您可以逐个或同时运行场景,如果一切正常,它们应该确认规则按预期工作,并在圆圈上显示勾选标记:

https://github.com/OpenDocCN/freelearn-csharp-zh/raw/master/docs/entp-arch-dn/img/Figure_13.22_B21293.jpg

图 13.22 – SpecFlow 测试的结果

六个场景对于这样一组复杂的策略来说并不多,在现实世界中,几十个这样的场景将受到欢迎,以形成一个强大的测试框架,使每个人都确信系统按预期完美工作。但再次强调,由于这不是本书的主要内容,我们将在这里停止对授权规则的自动化测试。顺便说一句,我展示了使用 SpecFlow 创建的自动化 BDD 测试,因为这是我习惯的框架,但根据您的需求和上下文,可能还有其他更合适的替代方案。重要的是,您是否使用 SpecFlow、Postman 或任何其他方法,但重要的是像授权这样重要的规则应该得到仔细验证。

OPA 面临的挑战

OPA 是一种优秀的授权规则实现方法,但它仍然带来了一些挑战。

首先,正如之前讨论的那样,编写规则的复杂性。尽管我们试图将一些复杂的函数算法拟合到仅仅几个关键字中,这本身是相当逻辑的,但它确实限制了那些试图采用 OPA 和 Rego 语法的人,他们可能会因为许多错误的规则编写尝试而被阻碍。

我个人有过这样的经历,而且坦白说,我仍然不太明白以下规则是如何工作的:

permissions[entity_name] := access {
    data.roles[entity_name]
    reachable := graph.reachable(roles_graph, {entity_name})
    access := {item | reachable[k]; item := data.roles[k].access[_]}
}

我知道这是真的,因为我已经测试过了,我可以看到遍历树并选择路径上一些数据的观点,但是 access 的额外递归评估以及 _ 关键字和 reachable 函数的使用,使得我很难自己编写,除非参考别人写的示例。这可能是因为缺乏实践,但我在近四十年的编程生涯中尝试过许多稀有语言,我仍然认为 Rego 可能是我遇到的最复杂的逻辑之一。尝试使用 OpenFGA 几次后,可能更容易提供等效的授权规则,但我不能对此做出承诺,因为我还没有在生产就绪的模块中使用这项技术。

幸运的是,一些文档,例如 www.openpolicyagent.org/docs/latest/policy-reference/ 展示了高级示例,我还在 www.fugue.co/blog/5-tips-for-using-the-rego-language-for-open-policy-agent-opa 找到了一些高级技巧,而像 medium.com/@agarwalshubhi17/rego-cheat-sheet-5e25faa6eee8 这样的链接则对这些复杂语法的工作原理提供了清晰的解释。

OPA 的另一个挑战可能来自这样一个事实:大量的 HTTP API 调用可能会导致性能问题。如果你的授权规则很复杂,那么你很可能会被迫逐个应用到业务实体上。那么,你将如何处理请求实体列表的调用呢?调用 API 成百上千次显然不是可行的选择。而对于本地 Docker 容器来说,这一点对于云服务(如 OSO www.osohq.com/,它提供授权规则的 SaaS 解决方案)来说更是如此。

当然,最好的方法仍然是分页显示结果,这不仅对生态友好,有助于减轻资源压力,而且从人体工程学角度来看,为用户提供更少数据杂乱、更容易阅读和理解的屏幕。然而,在某些需要大量数据的情况下,多次调用 HTTP 服务器并不是一个优雅的选择。幸运的是,如果您使用 Go 语言,可以直接从您的代码中访问 OPA,或者甚至作为一个 WebAssembly 模块,这使得从许多平台在代码级别集成它成为可能(尽管目前并不容易)。

在授权管理方面,这里有一个需要注意的最终事项——在本章中,您已经看到了将要更真实地应用于后续章节中的语法和数据简化版本。例如,我使用了简单的标识符而不是 URN,一些属性被重复使用以简化规则执行,等等。我本可以展示最终形式的策略,但考虑到以下两个原因,我认为展示工作进展状态更好:

  • 避免这种额外的复杂性使得集中精力研究授权规则主题变得更容易

  • 在我们需要做出这些调整的精确时刻展示这些调整,希望也能使它们更容易理解,因为情况将展示简单方法可能导致的演变问题,并有助于解释变化

摘要

在本章中,我们展示了业务规则管理系统的作用,它在信息系统中的有用性,以及我们如何实现它,从功能示例开始,然后演示了与授权相关的另一个示例,授权是软件应用程序中最常用的业务规则集之一。

就像 BPMN 引擎一样,BRMS 引擎并不常用。事实上,在绝大多数情况下,业务规则都是通过代码表达式实现的,或者编译到应用程序中。这绝对是正常的,因为 BRMS 代表了一个重要的投资,而实现如此复杂的应用程序确实需要一个强大的业务案例,其中业务规则频繁变化或与严格的监管或营销约束相关,例如需要跟踪所有业务规则及其变化,能够模拟业务规则集新版本的影响,等等。因此,我们可以得出结论,这种方法目前仅限于非常罕见的情况。当然,随着我们渴望的信息系统设计的工业化,未来事情可能会发生变化,但到目前为止,BPMNs 和 BRMSs 几乎总是过度设计的工作。

由于理想系统的三个部分中有两个在大多数组织中不值得使用,这意味着这个理想系统实际上是非常乌托邦的。此外,即使是集中式的主数据管理MDM)方法也很复杂。MDM 实践本身适用于每个业务领域,因此数据参考服务没有问题;它们设置起来并不复杂,正如我们将在第十五章中看到的那样,并且它们带来了大量的商业价值和优势。然而,理想系统追求的是通用的 MDM,能够动态地适应应用业务环境中的每个实体。这一步也超出了本书的范围,尽管为数据参考服务生成静态代码正在成为一种可行的选择,正如我们将在第十五章的结尾展示的那样。

此外,我们已经表明,理想信息系统的三个职责最终是相互交织的:

  • MDM 在其数据验证过程中使用业务规则

  • BRMS 需要从 MDM 获取数据以便应用业务规则并决定其输出值

  • BPMN 主要作为一个数据收集器,为 MDM 提供数据,同时也从 MDM 消耗数据

  • BPMN 也使用业务规则来确定在不同网关中的走向(有时,在给定任务期间计算一些额外的数据)

所有这些都证明,从技术上讲,这个由 MDM、BPMN 和 BRMS 三个通用服务器组成的组合并不那么可行,也没有实现完美的解耦。那么,为什么我们在第五章和最后三章中要讨论这样一个理想系统呢?答案再次在于业务/IT 的协同。理想系统并不是今天信息系统实践中可以实现的(当然,至少在接下来的几十年内也不可能实现),但它有一个巨大的优势,就是迫使架构师从三个通用、始终适用的功能职责的角度来思考。即使你使用一个独特的软件应用,了解如何分离数据管理、业务规则管理和业务流程执行,也能为解耦你的信息系统迈出重要的一步(例如,n-层架构根本无法实现解耦)。正如你将在接下来的章节中看到的,本着这些原则构建信息系统将帮助我们实现一个非常复杂的目标,即能够非常容易地修改重要的功能规则和行为,在大多数情况下,对实施没有显著影响。

在下一章中,我们将利用到目前为止所学的所有知识来设计DemoEditor的信息系统。在接下来的章节中,我们将最终动手实现这个信息系统的各个不同部分,使用 C#和.NET 作为编程平台,以及 Docker 作为部署架构。

第三部分:使用 .NET 构建蓝图应用程序

在理论部分和架构原则部分之后,我们将通过实现示例信息系统的某些重要部分来深入探讨该方法的技术方面。我们将创建一些实现 API 契约的 ASP.NET 服务,以及使用这些服务并实现一些业务流程的图形用户界面。由于一些功能已被外部化以提高工业级质量,我们还将展示如何以松耦合的方式与这些模块交互。将服务插入 Apache Keycloak IAM,使用如 OAuth 和 JWT 等标准,当然是一个重要步骤,但我们还将以标准方式展示电子文档管理系统,并讨论许多其他外部服务。最后,将展示业务流程的外部执行,包括编排和协奏范式。

本部分包含以下章节:

  • 第十四章, 分解功能职责

  • 第十五章, 插入标准外部模块

  • 第十六章, 创建只写数据引用服务

  • 第十七章, 向数据引用服务添加查询

  • 第十八章, 部署数据引用服务

  • 第十九章, 设计第二个数据引用服务

  • 第二十章, 创建图形用户界面

  • 第二十一章, 扩展接口

  • 第二十二章, 集成业务流程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值