-NET-企业级架构-全-

.NET 企业级架构(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

感谢您购买这本书!它源于工业化的信息系统需求。软件无处不在,但除了少数高成本应用外,它充满了错误,维护成本很高,大多数开发者有一半的时间在重新发明轮子或与信息系统抗争以添加一些新功能。

这是我从超过 25 年的行业经验以及为许多不同组织专门从事数字化转型工作几年来的严酷观察。这些不同的经历使我确信,根本问题始终相同:软件行业实际上并不遵循工业化的规则,即分解系统为更小的系统,并使用遵守标准接口的模块将模块连接起来。

与学者们合作帮助我正式化这些业务/IT 对齐的问题,以及它们的来源和可能的解决方案。但我的大多数客户都告诉我,我之所以能作为一个顾问脱颖而出,是因为我不仅能够为董事会提供清晰的转型计划,而且还能提供从语义到 API 合约的咨询建议,甚至在代码的难点部分提供一些实际的帮助。他们说这使得整个合作更具可感知性,并提供了可见的业务成果,因此这就是我想让这本书成为的样子:首先是一些理论,因为我们需要改变我们思考和创建信息系统的思维方式,但也需要一些代码,因为太多的人现在效率不高。

看到许多公司,从小型律师事务所到工业/物流企业,再到一些大型农业合作社,业务效率的积极影响,我确信将工业概念应用于软件设计和架构将变得越来越具有成本效益,尤其是在软件和数据成为大多数组织当今竞争支柱的情况下。

使用 API 接口的规范和标准,外部化诸如身份验证、授权、电子文档管理等功能,最重要的是,从业务功能而不是技术角度思考,如果要将信息系统从成本中心转变为组织的强大资产,这是至关重要的。

这种方法已经帮助了许多公司和政府机构,我希望它也能为您的信息系统带来同样的效果!

这本书面向的对象

这本书旨在帮助您以使其更具灵活性和为您的组织提供更多业务价值的方式创建或演进信息系统架构。由于它是理论性的、面向业务的,并且以.NET 应用为技术应用,它可以在多个层面上被许多人阅读:

  • 董事会和高级管理人员可以通过阅读前几章来了解数字化转型及其战略规划。了解信息系统中最常见的问题应有助于他们投资于正确的领域并优先处理工作,不仅要考虑功能,还要在合理的情况下优先考虑技术债务的减少。

  • 技术领导、架构师和研发经理也可以从阅读下一章中受益,这些章节使策略更加具体,并解释了在构建信息系统、选择工具和方法以及在其工作中应用一致性方面的最佳实践。

    • 最后,开发人员或实际操作的架构师和技术总监将继续阅读本书的最后几章,通过编码不同的概念、连接外部服务以及编排 API 和 webhooks 来创建一个虽小但实用的示例系统。

本书涵盖的内容

第一章信息系统的悲哀状态,解释了什么阻止了大多数信息系统提供应有的价值。它还提出了一些衡量信息系统效率的方法,重点关注大多数观察到的问题的根本原因。

第二章将工业原则应用于软件,首先解释了工业化的特点,然后提出了一种将转型应用于信息系统管理的方法。它侧重于标准和规范的重要性,以降低大多数虚拟系统中的复杂性。

第三章实现业务一致性,为前几章分析的问题增加了一层理论,解释了康威定律以及一些其他关于业务/IT 一致性的原则、模式和反模式。本章还介绍了四层图,该图将在本书的其余部分中使用。

第四章处理时间和技术债务,阐述了技术债务的概念,并解释了信息系统通常一开始与其环境非常适应,但随着时间的推移逐渐偏离效率。它还反思了如何采用敏捷方法来处理架构。

第五章乌托邦式的完美 IT 系统,讲述了一个绝对完美的信息系统愿景,该系统将完全符合业务需求,并且变更将像原始设计一样简单。即使这个乌托邦式的系统在现实中永远不会存在,它也会帮助你理解外部化功能和严格分离责任如何使现有的信息系统更加高效,并随着时间的推移简化其演变。

第六章, 从代码到系统的 SOLID 原则,开始了一系列更技术性的章节。它讨论了通常适用于代码的 SOLID 原则,并将它们应用于系统架构。我们还展示了将作为本书其余部分应用实例的示例信息系统。

第七章, C4 及其他方法,列出了软件设计和架构中最著名的几种建筑方法。它分析了它们之间的差异和共同点,并为你提供了一些根据具体情况选择最适合的方法的途径。

第八章, 面向服务和 API,从对历史上用于实现服务概念的所有技术的描述开始,解释了人们对这一概念有什么期望。它将解释标准和关键格式在服务中的有用性。

第九章, 探索领域驱动设计和语义,关于拥有一个清晰的业务领域语义定义的重要性。尽管这听起来可能有点远离软件架构的概念,但它实际上是业务/IT 对齐方法的核心。

第十章, 主数据管理,提供了一个结构化的视角,用于管理一个良好对齐的信息系统中的数据。主数据管理远不止是在数据库中持久化数据。它涉及定义其生命周期和相关的治理,并确保数据管理不断进步,尤其是在一些遗留数据存储库已经存在的情况下。

第十一章, 业务流程和低代码,解释了业务流程在信息系统中的位置,以及如何使用如 BPMN 等标准正式描述它们。尽管这仍然是一个理论章节,但它描述了可以在信息系统中用于实施业务流程管理的工具。

第十二章, 业务规则外部化,在数据和流程管理之后,完成了理想 IT 系统三个部分的闭环。它解释了什么是业务规则管理系统,并展示了某些实施示例。

第十三章, 授权外部化,解释了身份和授权管理的概念,并展示了如何将其外部化以促进整个信息系统的发展。例如,基于角色的访问控制RBAC)和基于属性的访问控制ABAC)等范例也通过实例进行了说明。

第十四章分解功能职责,是我们开始将前几章的知识应用到示例中的地方;我们将通过代码和服务来实际操作。本章首先使用四层图来建模系统,然后为将在外部应用中更好地考虑的功能准备技术规范。

第十五章插入标准外部模块,考虑了这些规范,并为已识别的不同功能提出了一些现成的软件解决方案,例如 Apache Keycloak 用于身份和访问管理(IAM)、MongoDB 用于持久化、Alfresco 用于电子文档管理等等。

第十六章创建只写数据参照,专注于示例信息系统的业务领域数据管理。它展示了如何用.NET 编写一个 API 服务器,以尽可能尊重标准以及之前描述的所有业务/IT 对齐原则来提供服务。

第十七章向数据参照服务添加查询,在前一章的基础上增加了数据管理系统的一些功能,同时也展示了如何以工业方式对其进行测试。本章使用了如开放数据协议(Open Data Protocol)等标准来改进实现。

第十八章部署数据参照服务,展示了如何部署前几章创建的服务以及如何将它们插入到 IAM 中。由于安全性是一个如此重要且复杂的主题,因此这是本章专门讨论的主题。

第十九章设计第二个数据参照服务,使用前一章中提出的所有原则来构建第二个 API 实现,这次由于该服务必须使用 webhooks 并在本地缓存数据以减少对第一个数据参照服务的调用而变得复杂。尽管它没有增加任何新的原则,但它确实展示了如何在实践中实现松散耦合。

第二十章创建图形用户界面,为我们的示例信息系统添加了一些图形界面,目前该系统仅暴露 API 端点。在展示基于 Web 的单页应用程序并解释业务/IT 对齐的概念和建议也适用于 GUI 领域之后,它详细说明了如何将数据分页机制与后端连接,并观察到职责的严格分离。

第二十一章扩展接口,在前一章的基础上,将一些行业标准应用到之前构建的 GUI 上,即通过自动化测试它,同时也展示了如何自动化导入数据以将系统从其遗留数据管理迁移过来。它最后展示了良好的组件分离如何帮助,结合合适的技术栈,快速构建移动应用。

第二十二章整合业务流程,通过解释如何通过 GUI、BPMN 引擎或 n8n 等低代码/无代码现代编排工具将业务流程集成到示例应用中,结束了与示例应用对应的章节集。它最后解释了何时需要专用服务来实现编排或编排。

第二十三章对系统进行修改,通过展示已构建的信息系统现在可以通过受到副作用的影响或失去进一步发展的能力来支持各种变化,总结了本书的整个想法。在这一章中,我们将更改数据结构、GUI 和业务规则,特别是授权,甚至调整业务流程,希望展示初始架构规划使所有这些变得容易。

为了最大限度地利用本书

Docker 已被用于尽可能减少所需的工具。由于所有镜像都在线可用(甚至为示例信息系统创建的定制镜像,可在hub.docker.com/repositories/demoeditor找到),你只需要 Docker(有关安装说明,请参阅 https://docs.docker.com/engine/install/)来与该应用程序一起工作。镜像版本遵循代码分支。

如果你想要调试应用程序并对它进行一些更改以遵循书中的说明,那么你还需要.NET 8.0 SDK (dotnet.microsoft.com/download)、Visual Studio Code (code.visualstudio.com/download)和 Git (git-scm.com/book/en/v2/Getting-Started-Installing-Git)。Postman (www.postman.com/)也将被用来快速注入数据。

本书涵盖的软件/硬件 操作系统要求
.NET 8.0 SDK Windows, macOS, 或 Linux
Visual Studio Code
Git
Docker
Postman

如果您正在使用本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件 github.com/PacktPublishing/Enterprise-Architecture-with-.NET。如果代码有更新,它将在 GitHub 仓库中更新。这个 GitHub 仓库是根据本书中示例构建步骤创建的分支:github.com/PacktPublishing/Enterprise-Architecture-with-.NET/tree/main/DemoEditor#versioning

我们还提供其他来自我们丰富图书和视频目录的代码包,可在 github.com/PacktPublishing/ 获取。查看它们吧!

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“想象一下,我们使用一本书和author实体之间的链接。”

代码块设置如下:

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

任何命令行输入或输出都应如下编写:

catch (Exception ex) {
  transac.Rollback();
  throw new ApplicationException("Transaction was cancelled",ex);
}

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“一旦连接,您将看到欢迎页面界面,您可以通过点击Business Central或屏幕左上角的家图标在任何时候返回该界面。”

小贴士或重要注意事项

看起来像这样。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有任何疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问 www.packtpub.com/support/errata 并填写表格。

盗版:如果您在互联网上发现我们作品的任何非法副本,无论形式如何,如果您能提供位置地址或网站名称,我们将不胜感激。请通过电子邮件发送至 copyright@packt.com 并附上材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

分享您的想法

一旦您阅读了《.NET 企业架构》,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。

您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。

下载本书的免费 PDF 副本

感谢您购买本书!

您喜欢在路上阅读,但又无法携带您的印刷书籍到处走?

您的电子书购买是否与您选择的设备不兼容?

别担心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何设备上阅读。从您最喜欢的技术书籍中直接搜索、复制和粘贴代码到您的应用程序中。

优惠不会就此停止,您还可以获得独家折扣、时事通讯和每日收件箱中的精彩免费内容。

按照以下简单步骤获取优惠:

  1. 扫描下面的二维码或访问以下链接!二维码

    packt.link/free-ebook/9781835085660

  2. 提交您的购买证明

  3. 就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件

第一部分:与业务对齐的架构及其解决的问题

本书的第一部分完全不涉及技术,旨在提供对业务/IT 对齐的深入了解。它解释了现有信息系统中存在的困难和不足,以及一些工业化方法可能如何帮助克服这些问题。这对于面临数字化转型的人来说很有用。对于那些 IT 系统无法实现其功能承诺的人来说,很难发展,而且成本效益不高,也应该更好地理解为什么是这样。在解释原因和症状之后,本书的这一部分提出了一种理论方法,即乌托邦式的系统,将消除所有这些不足,并将指导下一部分的工作。

本部分包括以下章节:

  • 第一章信息系统的不堪现状

  • 第二章将工业原则应用于软件

  • 第三章实现业务对齐

  • 第四章处理时间和技术债务

  • 第五章乌托邦式的完美 IT 系统

第一章:信息系统的不幸状态

在提出解决方案之前,分享对情况的彻底诊断是至关重要的。在信息系统和更广泛地,计算机使用的情况下,任何用户都知道“bug”这个术语,并经历过与故障相关的挫败感,有时这种挫败感具有很高的个人影响(个人数据丢失、收入后果等)。对于公司来说,IT 故障可能具有严重的后果,因为它们越来越依赖计算机来实现其业务运营,从而实现其财务目标。

在定义信息系统是什么以及解释其效率(或缺乏效率)如何计算之后,我们将尝试分类导致此类问题的原因。至于解决方案,这将是本书其余部分的主题。但到目前为止,我们必须了解信息系统出了什么问题,它是如何发生的,更重要的是,为什么会出现这种情况。

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

  • 什么是信息系统?

  • 为什么软件开发仍然是一门手艺,既有好的一面也有坏的一面?

  • 如何评估信息系统的效率

  • 如何分类可能发生在信息系统上的不同影响,以及它们的起因和后果

什么是信息系统?

在讨论信息系统状态之前,给出一个清晰的信息系统定义,甚至系统的定义可能是有用的。

系统是一组共同运作以实现共同目标的物品集合。这是系统与个体联合的基本区别:系统的各个部分共同朝着愿景努力。

信息系统(我们有时将其缩写为IS)进一步将此定义为一组共享信息以实现共同目标的物品集合。严格来说,信息系统不一定由软件组成,尽管本书中我们将讨论的大部分内容都是关于计算机化信息系统。即使在最复杂的信息系统中,也仍然存在一个不可忽视的信息部分,这部分信息不是软件包含的。这种情况我们将讨论,但本书的主要部分,我们将假设基于软件的信息系统,因为它们现在几乎在每家公司和组织中都很普遍。

因此,信息系统被理解为一系列旨在实现目标的软件工具。对于拥有系统的大多数公司来说,这个目标通常被设计为一个业务流程。需要注意的是,软件始终依赖于硬件,但这种依赖性越来越被隐藏在幕后,信息系统越来越被视为软件手段,通过有效地实施功能流程,组织在一起以实现商业目标。

信息系统简史

当处理一般主题,如信息系统的质量时,回顾过去并分析其向当前情况的演变总是很有趣。

如果我们遵循之前提出的定义,信息系统只有在至少两个实体协作时才会出现。在我们基于软件的信息系统假设中,这意味着至少有两台计算机已经连接。这意味着最初需要占用整个房间的计算机,尽管它们经常被称为“大型系统”(在这种情况下,组装的物品是计算机制、短期内存和长期内存),但不应该被视为系统。

让我们再向前推进一点时间,谈谈 IBM 的客户-服务器大型机:这些是我们可以认为是信息系统的第一批,因为它们有连接到中央计算机的客户工作站,信息在它们之间流动。该协议是专有的,这使得 IBM 能够提供这些系统的高质量服务(其中许多至今仍在运行)。由于协议的唯一实现由同一团队定义,因此兼容性和互操作性并不是一个大问题。这是遗产,但当系统运行良好且现代化风险很高时,企业逻辑上会选择不移动任何东西(这是信息系统管理的第一规则:如果它运行良好,就不要去动它)。

快进到九十年代,这是个人电脑(PCs)的时代。尽管全球都在努力保持机器之间的兼容性,但当时使用电脑的人都知道,如果某个软件不支持嵌入式显卡,那么这无疑是部分失败。当然,从那时起,事情有了很大的改善,视频电子标准协会VESA)为显卡和屏幕制定了国际标准,如 VGA 或 VESA,以及许多其他规范,使得在当今时代更换 PC 的组件而不会破坏整个系统成为可能。然而,对信息系统造成的损失相当大:当单台机器的组件都难以组装时,我们怎能期望机器网络能良好地协同工作呢?专有格式、不同的编码、几乎完全缺乏强大的数据交换协议:所有这些都导致了困难的情况,只有高预算的公司才能在专家的帮助下操作复杂的计算机化系统,这些专家会对电子板上的跳线、编译器参数等任何东西都感到紧张。

幸运的是,随着互联网的扩张和其对标准化计算机之间交换的激进方法,Y2K 问题得到了解决。现在,每个遵守 TCP/IP、HTTP、Unicode 和其他互联网标准协议的计算机都可以与世界上任何地方的另一台计算机交换数据,无论其硬件和操作系统实现如何。这是 IT 历史上的一大步,也是我们今天“现代”信息系统根本定义的根源。

在此基础上增加了几层软件,以便于在系统中重用功能。它从低级开始,通过例程进行本地代码重用,然后是库和组件。随着网络容量的出现,这演变为分布式组件、Web 服务,最终是面向服务的架构SOA)。这也是引入 n 层架构的时候,在软件应用内部建立了第一层责任分离,包括图形用户界面GUI)管理、功能服务的展示、业务规则的实现和持久性管理。

最后一点,SOA,导致许多公司遭受了昂贵的失败,绝大多数尝试实施此类架构的努力都导致了重要的经济损失和项目的放弃。技术步骤太高,难以顺利运行,出现了更轻的替代方案来消除 SOA 方法的地方性困难:

  • 标准化消息中间件:为了对抗在 SOA 上运营并使用它来锁定其客户的那些大型软件公司提出的专有交换协议

  • REST 方法:为了减轻 SOAP/WSDL Web 服务和所有相关规范的重量

  • 企业服务总线:这项技术用于降低中间件的重要性,达到“哑管道”范式,其中参与系统的软件应用能够相互通信,而无需一个中心软件,该软件可能会成为单点故障

正如一些参考书籍(Sassoon, Longépé, 和 Caseau)所示,设计一个强大且具有进化能力的 IS 的最佳实践在 20 世纪 90 年代末就已经存在,尽管并不广为人知。但直到 21 世纪初,这些实践在社区中的份额才有所增加,SOA 和其他基于服务的途径蓬勃发展,导致在 2010 年代初出现了微服务架构。这套实践在撰写本文时仍被视为参考架构,尽管我们将看到,并非所有建议都应在没有对其在研究环境中有用性进行强烈分析的情况下应用。正如我们将看到的,服务的粒度是获得高效 IS 的关键。但到目前为止,我们将讨论软件构建的一般性,并试图理解这个所谓的“行业”的当前局限性。

软件构建——仍然是工艺

本书对信息系统精确定义的解释,以及其演变简史,已经给出。这个历史不仅非常简短,而且展示了许多最近的演变,其中大多数与之前的技术状态截然不同。这种非常快速的演变表明,信息系统设计不是一种可以被认为是稳定和完全理解的东西。

在软件信息系统设计和部署中,仍保留着大量的工艺。工艺有其优点:对细节的关注、定制化的功能、独特性以及更多。它也有许多缺点,如成本高、难以在受控方式下进化、依赖少数创作者等。这些缺点在现代公司中超过了其优点,因为信息系统已经成为运营的骨干。

精心构建的信息系统是从任意演化的系统中演变而来的,工匠的工作没有什么是可耻的,但今天的道路是走向信息系统的工业化方法。这正是本书的主题。

工艺与之前缺乏质量相对立

在 IT 的多个领域,工艺被用于与较老、更随意和自我组织的旧方法相对立。例如,许多 IT 会议在其名称中包含“工艺”一词,作为他们解决质量和异质问题的意愿的声明。

信息系统之所以长期存在,仅仅是因为系统的各个部分被随意组合并连接起来,而没有对整个系统本身进行任何反思。这种情况通常发生在所谓的“点对点集成”中,在这种集成中,不同软件模块之间的连接仅考虑了链接的源和目的地,而没有考虑所有存在的链接的映射,有时甚至复制了一个已经存在的链接或反转了两个模块之间功能依赖关系的初始预期方向。

这种在没有人考虑整个功能的情况下诞生的系统,几乎没有机会长期保持稳定。在仅创建少数几个连接的罕见情况下,系统可以正常运行,但我们都知道 IT 发展非常快,业务需求不断增加(“系统中唯一稳定的东西就是其需要进化”)。如果没有人对整个系统有一个全局的视角,那么其整体进化的理想发展就无从谈起。这将是纯粹的运气,而墨菲定律指出,如果软件系统中有可能出错,那么它肯定会发生。

软件工艺涉及不愿意让系统自行创建并错误发展,而是要额外关注软件质量和构建持久且可进化系统的方法。当应用于代码(这通常发生在代码中)时,软件工艺包括测试自动化、重构方法、质量指标监控以及许多其他超越最小实践的方法和技术。

我们可以争论这些实践与工艺相悖,但这仅在已经建立的行业中才是正确的,在这些行业中,工艺与标准化的工业生产相对立。在 IT 领域,工业化尚未发生。IT 运动中使用的“工艺”一词,甚至可以说,是进入软件领域工业化的第一步,因为这是第一次坚决的行动,旨在使代码干净,不让它变成 IT 术语中通常称为“一团糟”的东西。

或许看起来我反对工艺和工业化,但一个并不比另一个更好:它们只是领域发展的两个阶段——在我们的案例中,是软件。就我个人而言,在 38 年的编程生涯中,我始终追求干净利落,因此我认为自己是工匠;我的目标是这本书——以及我过去 15 年的架构师生涯——谦卑地帮助迈出一步,使软件摆脱其幼稚问题,成为一个成熟的行业。

作为一个附带说明,软件架构师之间讨论的一个主题是我们的工作领域是否能够成为行业。我倾向于认为,成为一名优秀的软件工程师所需的创造力将使软件生产完全工业化变得不可能,但许多事情应该被工业化以达到这个成熟、成人的阶段,届时 IT 将最终实现其全部价值。

关于“持续架构”一词

持续架构的概念是,在某些人类构建(主要是软件应用)中,架构并非一开始就确立,而是在对象的构建过程中逐渐显现。这个概念通常与敏捷方法一起使用,在敏捷方法中,软件构建是迭代的,而不是一次性完成一系列阶段,如“V 型循环”方法。敏捷软件开发不是一次性的设计/开发/测试步骤系列,而是在这些步骤上多次循环,每次都基于前一个周期逐步向最终愿景迈进,甚至在步骤中可能还会演变。

在这种情况下,每一步都涉及实现循环所需的最小设计活动,以保持简单的心态。因此,没有最终架构的完整愿景,这有时可能被视为敏捷方法的严重局限,但同时也是它们的优势,因为它们能够不断适应。尽管如此,这种做法也可能出错,这通常发生在项目参与者期望架构自然出现的情况下。这种混淆在软件开发中很常见,其中没有架构师参与,开发者认为个人最佳实践、善意和工艺将对结果产生积极影响。但现实是,这些实践将积极影响敏捷步骤的每个个体的结果,但不会将整体架构引导到任何合理的位置,因为没有长期的方向。

因此,理解持续架构确实存在,但需要积极参与才能逐步实现这一点非常重要。它自然发生在模块级别,在那里单个开发者会仔细精炼和重构代码。但要在系统级别工作,也需要同样的参与度。

工艺与工业化方法的对比

在上一节中,我们将手工艺与代码中缺乏质量意图相对立,从而展示了积极手工艺的积极面。在这里,我们将指出它与工业化相对时的局限性。手工艺承载着高度熟练的个人谨慎操作的想法,与质量相比,花费的时间并不重要。

尽管手工艺(在其崇高的意义上是对手工打磨、高质量工作的奉献)值得赞扬,但它也表明该领域的成熟度仍然较低。当达到成熟时,学科往往会将工作的不同步骤分开,自动化其中的一些步骤,标准化实践和工具,并总体上提高效率,甚至将质量提升到仅靠个人人类方法无法达到的高度。此外,工业化使整个过程规范化,并使遵循规范的人——而不仅仅是高技能的人——能够达到这一高水平的质量。

这就是所有行业(这也是我们为什么这样称呼它们的原因)所做的事情,这是人类工人试图达到的自然演变。在软件领域,可预测性、质量和上市时间是被追求的品质,而工业方法对于实现这些品质是必要的。

必须指出,非工业方法并没有什么错误。软件还不是一种行业。毕竟,桥梁已经存在了 4000 多年,所以这已经成为一种受控和成熟的作业方式是完全可以理解的。另一方面,软件构建只存在了几十年,仍然处于起步阶段。

但这里重要的信息是,尽管手工艺(尽管它有所有这些优点)是工业化的前一步,但现在的许多信息系统所有者真的渴望一个能够达到这一步的 IT 团队。对于他们中的某些人来说,竞争优势主要来自信息系统。有人说过“今天所有的公司都是软件公司”,这再次强调了信息系统的重要性以及达到更高质量水平的绝对必要性。

技术债务的概念

技术债务是一个通过隐喻来解释的概念,它说明了软件开发质量低下如何对未来的发展产生负面影响。在金融债务中,你必须支付定期的利息,这取决于你借入的金额。在软件隐喻的这一面,通过削减成本和降低整体质量来争取一些时间,只要低质量的模块保持活跃,就必须定期支付。修复错误和维护模块将花费一些时间,团队将无法为新功能投入价值对用户有意义的资源。模块的质量越低,“利率”就越高——在我们的案例中,就是维护所需的时间。在最坏的情况下,软件的质量如此之低,以至于所有可用的资金/开发时间都只用于支付债务的利息(保持应用程序运行),这意味着没有资金剩余来偿还债务/修复软件,更不用说支付具有更高价值的功能了。

这个概念将在第四章中详细讨论,但既然我们在谈论工艺,现在立即解释这两个概念之间的联系是很好的。

工艺通常被视为一种软件开发方法,其中技术债务保持在尽可能低的水平,有时几乎不存在。一位优秀的工匠开发者会以拥有零缺陷软件、100%的自动化测试覆盖率、完全自动化的集成和部署系统为荣。

与工艺相抵触的工业化方法主要导致整体质量的提高,但在处理技术债务方面也超越了这一点。与粗心开发让技术债务可能不受控制、失去控制相反,工业化方法管理技术债务。金融隐喻仍然适用:而不是完全拒绝任何债务,一个深思熟虑的运营商将仔细管理他们的资本,如果确实有利可图,就会借款。与工匠相比,工业化导向的开发者将更加意识到上市时间的重要性,如果这有助于他们在竞争对手之前接触到用户,就会采取受控的技术债务水平,从而带来部分受技术债务减少影响的利益。

软件与机械系统长期比较

软件行业与机械行业(在大多数已知案例中,尤其是汽车行业)的比较,已经成为双方竞争者的习惯立场,以至于这种比较已经变得陈旧。此外,比较并不一定具有逻辑性,这取决于它们是如何实现的。例如,在著名的“微软与通用汽车”的梗中,将一辆汽车与一个软件产品进行比较,从每一方都得出了一些奇怪的结论(“如果汽车行业像软件行业一样快速发展,我们将会拥有每加仑行驶 1000 英里的汽车”/“如果汽车行业像软件行业一样运作,汽车每行驶 1000 英里就会意外撞车”)。这种不恰当比较的潜在错误在于,两边的复杂性水平并不相同。如果你要比较一个在工业工厂中制造的单一汽车,其成本应该与信息系统中的一个功能性过程的单一操作进行比较,因为只有一个功能是单独操作的(例如,汽车是运输少数人,软件应用是计算工资等)。如果我们反过来,想要比较软件的内部操作与其可能的上十万行甚至数百万行代码,正确的比较应该是与汽车工厂本身进行比较,因为它具有必要的模块化来改变其汽车型号的生产,并且它还包含成千上万的函数和更多的运动部件来完成这样的移动任务。

简而言之,错误的比较可能会非常误导人,并且几乎没有实际意义。但是,将信息系统构建与汽车工厂的设计进行比较,例如,更接近每个系统的复杂性的现实,并且可以提供有趣的见解,只要你仔细地将其置于上下文中。正如之前所述,如果我们保持行业标准年龄的基准,将信息系统与桥梁进行比较可能是相关的。IT 活动,作为一个只有几十年历史的行业,如何达到一个已经发展了几千年的活动的成熟水平呢?

我们现在将停止这种比较,但在这本书中,你将了解到几个软件“行业”与更传统行业之间的比较,这些行业更值得这样的称号。我们将努力使比较尽可能有帮助和合理。再次强调,说 IT 尚未完全工业化并没有任何评判的意思:一些系统无疑是工业化的,而一些则不是。这不应该被用来反对任何人,因为行业需要许多人类世代才能达到目前的水平。基于软件的信息系统只是没有足够的时间做到这一点。这本书是关于帮助走向这个方向的方法,考虑到其他行业的实验,同时牢记比较有时可能会误导人,并且在使用时应注意其适用性。

既然我们已经确定了信息系统的定义以及它可能因为该领域尚未工业化而存在的许多幼稚问题,就需要一个合理的工程方法来评估这种不成熟。

信息系统的效率

工程学是关于以受控的方式制造事物;信息系统不会避开这个备受期待的转型,因为它们的效率可以提升。为了做到这一点,我们需要指标、衡量标准和一种方法来获取给定信息系统的结果。幸运的是,在这个专业领域,对此有一致意见,正如我们现在将要看到的。

系统效率的衡量

由于系统是一组朝着既定目标共同工作的项目,如前所述,衡量整体的效率意味着不仅仅是知道每个项目的效率指标并将它们以某种方式相加。常有人说,一个好的信息系统的价值远远超过其各个移动部件的价值,而最积极的影响来自于它们的相互作用。

评估信息系统效率的一个好方法就是衡量它在其支持的功能过程中帮助节省了多少时间和金钱。然而,这相当复杂去衡量,因为一个单独的信息系统通常运行多个过程,并且它们各自的重要性和成本都应该被评估。如果你考虑到过程也是部分由人操作的,并且效率的提高并不仅仅来自软件部分,而是来自功能团队使用应用程序的方式、他们的培训、投入硬件性能的投资等等,那么使用这些指标来评估整个系统的效率就会变得困难。此外,输出可能更接近于投资回报率的计算,而不是效率指标的计算。

正因如此,信息系统的效率通常通过更简单、更易实现的指标来评估,即用于维护的成本百分比。这个简单的比率使我们能够知道有多少钱被用于保持系统运行(这是一个运营成本),以及有多少钱被投入设计它并使其变得更好(这是一个投资成本)。由于功能特性是系统所要求的东西,为了保持其运行而进行的维护越多,与从系统中获得价值相关的钱就越少。当然,关于软件效率有许多观点,我们将在本书中一起浏览很多这些观点。但维护只是开始评估信息系统状态的最简单方法。

维护成本

维护可能是软件仍然最缺乏的领域。关于软件设计和开发的方法论已经出现,并开始帮助将手工艺转变为更工业化的方法。但设计和开发只是软件旅程的开始。一旦投入生产,应用程序就必须部署、修补(有时在运行时进行)、改进,并在长期内维护。这就是不良设计、技术债务和维护成本高昂显示出设计阶段缺陷的地方。

再次强调,情况并非全然糟糕。毕竟,有经过验证的方法可以在软件应用程序运行时对其进行升级,如果我们将其与在运行时更换汽车轮胎的机械对应物进行比较,这确实是一项了不起的成就。但分析信息系统维护的总成本表明,这仅仅是冰山一角,总体情况相当糟糕。

一项由 Gartner 在约 10 年前进行的基于 3700 家公司的研究表明,IT 预算的 72%用于维护。你读得没错:IS 的成本几乎有四分之三不是由设计和改进决定的,而是在生产过程中进行调整和保持其运行。再次想象一下,一旦你为房屋的建设支付了费用,每年只需支付三倍的费用来防止其倒塌!这表明了今天信息系统所处的悲惨状态。

你会发现关于这方面的许多其他研究,但 Gartner 的研究可能是最知名的。为了提供另一个证实观点,同一时期的一项研究显示,“大约 20%的技术支持项目是失败的,并且这个数字随着项目规模和复杂性的增加而增加。然而,失败的项目在执行开始之前往往表现出明显的失败迹象。”

好消息是,我们并非注定要继续这样下去。在汽车制造业的初期,与最初的制造成本相比,维护成本巨大。最初的汽车并非工业化生产,而是手工制造,其年度维护成本几乎与汽车本身相当,轮胎需要每隔几千公里更换一次,机油需要频繁添加,以至于除了汽油罐外,还必须有专门的油罐,而且发动机每隔几个月就需要由机械师进行检修。

现在,当你查看工业化产品的维护指标时,情况要好得多。一家值得信赖的公司生产的新车在首次进维修厂之前可以轻松行驶数万公里,而且随着发动机制造技术的改进,添加机油已经成为一件往事。另一个例子是机油滤清器,其标准化程度如此之高,以至于两种尺寸几乎覆盖了整个欧洲个人汽车市场。

在工厂中,由于财务收益高度依赖于维护统计数据,我们有数据可以量化维护成本。一般来说,工厂的成本可以分为三组,这些组的比例对于确定整个工厂的效率很重要:

  • 固定费用:这些是与生产量无关的费用。通常,建筑成本(通过债务偿还或租赁,取决于公司是否拥有或租赁场地)并不取决于你生产多少。为了提高效率,你希望减少这些成本,但提高你的生产水平也会有所帮助,因为它将减少它们对你的收入的相对影响。

  • 增值相关费用:这些是与你的生产直接相关的费用。你生产越多,你为供应商提供的商品支付的金额就越多。你会得到更多的收益,除非你以低于收入价格的价格销售,这在大多数国家都是被禁止的,而且通常是一个非常糟糕的商业决策。总的来说,这些费用被认为是好的费用,因为它们越高,带来的钱就越多!

  • 维护费用:这些是你需要为生产工具持续平稳运行而计划的开支。维护费用是一头难以驯服的野兽,因为如果你为了小利而忽视它们,它们可能会反过来狠狠地咬你(在便宜的润滑油上节省一点,你的百万美元机器可能在几年后就会损坏)。维护成本有两个缺点。首先,与固定费用相反,维护费用会随着生产量的增加而增长(你的机器运行越多,它们需要的维护就越多,而且随着旧机器需要更多维护,这种增长是指数级的)。其次,与第二种费用相反,它们不会给你的产品增加可见的价值:如果你在销售的汽车中加入了更好的引擎,相关的价值会立即被感知;如果你购买更好的油来维护用于制造这些引擎部件的机器,没有任何客户会意识到这一点,更不用说为此付费了。

因此,我们理解为什么工厂经理非常关注维护成本:它们是“坏”开支,也是难以管理的开支。固定价格需要较少的关注,因为……好吧,它们是固定的!而且增值成本也不是什么大问题,因为,当它们增加时,这意味着你的业务正在增长。所以,维护是关键,大多数工厂经理的业绩将由所有者或他们的上司根据他们的维护指标来评判。如果你不控制维护,成本激增,你将被另一位经理取代。如果你对维护预算过于严格,可能会出现代价高昂的故障,而且,再次,他们将被认为是混乱的负责人。

统计数据帮助管理者通过将其与过去在类似情况下所做的事情进行比较,找到这个复杂方程的正确方法。例如,在重工业工厂,合理的成本分配被认为是以下这样:

  • 固定费用:预算的 10%

  • 增值相关费用:预算的 85%

  • 维护费用:预算的 5%

当然,这些数字之间有一些可容忍的差异,但维护比率高于 7%必须得到证明,超过 10%则是对维护不再受控的强烈警报。

现在,让我们回到 Gartner 的研究,其中被调查的 3,700 位 IT 领导者中,用于“维持运营”的平均预算为 72%。再次,这种比较对于所谓的 IT 行业来说似乎很负面:糟糕了 10 倍!但是,必须考虑一些情况。首先,由于设计仅在大脑中进行,不需要像重工业那样昂贵的材料原型,因此 IT 中的成本自然和最优共享必然是不同的。此外,由于“作为服务”的物品的可用性,固定成本的比例正在变得越来越低。在几十年前,购买大型计算机对预算有很大的影响,尤其是在最初几年,它只部分使用时,云操作使我们能够按需购买计算机能力,这使得这些成本落入投资成本类别。

因此,我们可以认为这些数字并不直接可比。尽管如此,IT 行业在维护成本方面存在问题。作为工程师,我们立刻想到的问题是:这从何而来?下一节将有望向您展示,原因——大多数情况下——是容易确定的,因为当设计信息系统时,会犯一些通用的错误。这些错误是观察到的不足的根本原因。

IT 系统禁止演变的例子

到目前为止,事情可能有点理论化。大多数信息系统都是没有明确的计划和全局、架构化的视野而创建的,这反映在整个系统的维护成本——以及总拥有成本——上。但在实践中这意味着什么呢?这是否很糟糕?

您可能听说过“意大利面盘”或“数据孤岛”这样的表达。在前一种情况下,IS 的模块之间交织得如此紧密,以至于无法触及系统的某个部分而不对另一个部分造成副作用。在这种情况下,演变变得复杂。第二个表达与 IS 的模块相关,这些模块彼此之间非常紧密地分离,无法共享公共数据。这通常会导致数据重复、质量损失,有时在整个系统中出现矛盾的过程。这些只是几个关于可能发生的通用问题的例子。

以下章节将深入探讨此类失误,并详细说明哪些反应链使信息系统变慢、操作困难,在最坏的情况下,甚至完全停止工作。作为一名近 10 年的架构师,然后是小型到大型公司的信息系统演变顾问,我观察到了足够多的阻碍信息系统的情况,能够创建一个分类,说明出了什么问题以及如何进行分析。这种与法国一个研究实验室分享的经验导致了多篇科学论文的发表,其中对这种分析进行了形式化处理,并记录了业务/IT 对齐反模式。其中一些将在接下来的章节中详细说明。

原因分类

下面的图表是从 2021 年发表的一篇科学文章中提取的——《业务-IT 对齐反模式:从实证角度的思考》——Dalila Tamzalit,LS2N / CNRS,以及我在 2022 年 6 月的 INFORSID 会议上展示的。

图 1.1 – 业务/IT 对齐反模式的分类

图 1.1 – 业务/IT 对齐反模式的分类

小图是识别不同不匹配模式的视觉代码,其含义将在下一章中变得更加清晰。现在,只需定义不同块的位置。

横轴表示在已研究的信息系统中找到该模式频率的表达。诚然,这些是有问题的信息系统(ISs),因为像我这样的顾问被雇佣来处理它们。然而,在几乎 100 个来自不同背景的组织/行业系统中积累了十年的经验表明,这在信息系统(ISs)中——遗憾的是——是非常普遍的,而高质量、快速发展和成本效益高的信息系统却极为罕见。这些系统只为那些需求有限的小公司或从一开始就知道他们的 IT 系统是其脊柱和大脑并相应投资的大公司和富有的公司所保留。

图表的垂直轴评估反模式对系统功能的影响。反模式的位置越高,它对 IS 的正确运行和/或进化的阻碍就越大。

这种分类模式的结果是,右上角的反模式(级别 1)影响最大,观察到的频率也最高。这种情况对应于业务流程直接在系统的软件层中实现。在第三章中,我们将考虑 IS 在四个不同层上的分解,但就目前而言,只需说这些层之间的良好对齐是质量的最重要来源,而四层图的重要性如此之大,以至于记录的 14 个反模式的符号都基于它。

如前图右上角所示的前三个反模式(第 2 级)比第一个稍微少一些,但造成了大量观察到的困难。它们对应以下三种情况:

  • 具有多种实现方式的特性:这导致根据使用的软件不同,业务规则也不同,并由此产生明显的错误作为后果

  • 孤岛:这些导致数据重复和额外的工作,以及由于同步或缺乏同步而导致的错误

  • 单体:Heer,一个单独的软件应用程序集中了如此多的业务功能,其进化变得复杂,并成为整个系统的瓶颈,有时甚至成为阻塞点

剩余的 19 个反模式(第 3 级)观察较少且/或对信息系统的发展或正确运行的危险性较小,但了解它们可以帮助我们在系统图中发现它们并改善情况。

下面的图表(自愿模糊以保护客户信息)显示了信息系统快速地图如何帮助我们直观地找到“热点”。在这种情况下,两个接收了大量“硬耦合”(我们将在第四章)数据流的应用程序导致了进化问题,尤其是其中一个已经过时,另一个由于商业和监管原因难以进化:

图 1.2 – 一个信息系统手工绘制的地图,揭示了两个软件应用之间的高度耦合

图 1.2 – 一个信息系统手工绘制的地图,揭示了两个软件应用之间的高度耦合

这个图表背后的故事以及它为什么在这里展示,尽管不可读,是因为在设计这个信息系统地图时,一位非技术董事会成员走进房间,并立即指向两个带有大量红色电线指向它们的笔记,说:“我想我知道问题在哪里。”这让我们意识到我们不需要继续更精确地绘制地图,因为现有的分析已经足够清楚,我们可以开始对主要的耦合问题采取行动。还发现这两个应用程序也是最过时的。

研究所有可能引起信息系统问题的反模式组合超出了本书的范围,因为主题是这些系统的架构,我们将主要集中讨论如何从设计阶段开始避免这些问题。尽管如此,如果你对此好奇,欢迎阅读 Business-IT Alignment Anti-Patterns: A Thought from an Empirical Point of View 论文(参考 进一步阅读 部分),以获得关于分类的学术性和更正式的介绍。

阻塞信息系统的经典症状

在原因的背后,存在 ISs 的症状,包括一些(有时很多)这些反模式。如果你有经验,那么你肯定会熟悉其中的一些:

  • 软件应用程序或流程的测试时间呈指数增长。发布时间增加,有时甚至长达数年

  • 一个版本会影响那些对包含的进化或功能不感兴趣的客户或用户,因为它是在他们不使用的功能上

  • IS 模块的一些副作用无法解释(已确立的缺陷,但还有性能影响、不可预测的行为等)

  • 内部信息系统的满意度低,参与外部/客户信息系统的应用程序市场份额流失

大型系统的更新率

当这种影响发生,并且随着时间的推移变得更大、更困难时,通常感知的解决方案是丢弃现有的 IS 并构建一个新的,这通常被称为大爆炸方法。这不仅是最昂贵的解决问题的方式,而且它还碰巧是最有风险的,因为现有遗留应用程序中发现的错误注定会在其中一些重新出现,或者被新的错误所取代,这使得达到令人满意的情况变得极其缓慢。甚至有很高的可能性,在重写过程的最后,新系统也将远远偏离预期的行为,因为在此期间业务需求已经再次发生变化。

这就是为什么始终逐步改进现有系统,依靠其良好的行为,并逐步改进给用户带来最多麻烦的模块总是更好的。

根据信息系统质量水平,可能会发生以下情况:

  • 它可能处于如此精细的状态,以至于所有进化都可以实现,而不会对不相关的功能产生任何副作用

  • 实施一个简单的新工作功能并部署它可能需要几天的工作

  • 它可能存在一些耦合,使得实现它的时间更长,但并不更困难

  • 它可能充满了问题,以至于实现新功能变得复杂,需要专门的项目管理和影响分析

  • 它可能处于一种状态,进化几乎不可能,或者以整个系统更高不稳定性的代价为前提

ISs 的进化通常基于持续几年的阶段。经过 2 到 3 年,在最佳情况下 5 年,业务的未来变得如此难以预测,以至于规划底层 IS 的进化没有任何意义。在高度波动的活动中,甚至 2 年的计划也可能被认为太长。

当然,许多信息系统不能在短短几年内完全纠正,需要几个计划来重新调整它们。在这种情况下,方法与敏捷软件开发项目中的方法相同:首先实现一个步骤——尽管时间更长,通常在学期左右——以重新调整最紧迫的问题,此时目标将重新分析,然后进行后续的重新调整步骤,以此类推,采用持续改进的方法。

尽管在某些情况下(尤其是对那些不掌握与稳定化相关的困难的技术人员来说)非常有吸引力,但大爆炸方法很少是解决方案,如果你必须处理一个低效的信息系统,你很可能会计划逐步演变。当你在一个信息系统中进行这样的改变时,这很快就会对遗留模块有所帮助。尽管 IT 行业很快就会将旧技术视为垃圾(只需在浏览器中输入任何技术名称,然后跟“is dead”,你就会意识到这一点),但对待信息系统演变的负责任的方法是尊重遗留系统。它成为遗留系统的原因是它已经为很长时间提供了价值。

摘要

在本章中,我们讨论了什么是信息系统以及为什么,尽管在软件设计方面可以给予真正的手工艺技术所有可能的专业知识和关注,但将这些应用程序联系在一起的系统可能会出现许多问题,主要是在维护成本和随时间演变以及应对新的业务流程和功能请求的能力方面。许多症状可以提醒我们某个特定信息系统的状态,但它们都归结为一个主要原因:与其它实际产业相比,IT 尚未达到真正工业化领域的状态,因为它仍然是一项非常新的人类活动。

在下一章中,我们将讨论工业化原则如何应用于软件。这可以总结为两个行动:降低复杂性和标准化接口。

进一步阅读

第二章:将工业原则应用于软件

本章解释了如何使信息技术成为一个真正的行业,这始于应用工业化的主要原则,即把复杂性切割成小块,然后标准化模块及其接口。我们将与城市发展进行比较,其中水管的标准化、电力的标准化和其他接口的标准化已经允许持续进化。

在本章中,我们将解释行业的概念,因为这个名称被非常频繁地使用,但并不一定每次都能精确理解其含义。我们还将了解通过将复杂问题切割成小问题,然后通过标准化使小问题变得简单和可重复,工业化的运作方式。我们还将了解从这种做法中可以得出什么好处,特别是在信息系统方面。

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

  • 什么是行业?

  • 复杂性的管理

  • 标准和规范的好处

  • 信息系统中的城市主义隐喻

什么是行业?

在上一章中,我们将手工艺与工业化进行了比较,希望表明,虽然前者没有什么可羞愧的,但后者是其随时间自然演化的结果。所有行业都是从工匠开始的,随着工作越来越受控和可重复,最终可能成为真正的行业,工匠逐渐转化为工程师能力和工作。大多数人无需解释就能理解这一点,因为这在许多日常经验中都可以看到。例如,当一个人第一次尝试完成一项新任务(比如剪发)时,最初的尝试与随后的尝试不可比。经过一段时间,这个过程开始变得更加规律(头发被剪得很细,最初接受成为你的小白鼠的客户也不再抱怨)。经过足够的训练,一个人就会在该领域获得专业知识,并发展出一套常规(头发被剪到定义的长度和预期的形状,以一种可以在未来的剪发中精确复制的方式)。

尝试将工业化背后的东西形式化如何?换句话说,我们如何描述构成工业化的要素?正如我们所见,有一个可复制的概念,这意味着应该达到一个可测量的规范。此外,这个规范在所有领域的知识人士之间是共享的,这意味着它成为了一个标准。在我们的理发例子中,有剪发的名称,该领域的每个人都了解“修剪”或“缩短”的含义,这使得顾客不会带着他们没有预期的发型离开理发师。此外,从一位理发师到另一位理发师,一旦使用正确的词汇表达出来,就可以期待获得一个全球相似的结果。我们就是这样获得同质化的质量的。

在工业化过程中,就像在理发一样,也存在关注整体中各个小部分的概念。除非你想要看起来像名人,否则你不会将你的发型描述为整体,而是描述构成完整发型的各个小部分。例如(尽管从美学角度来看不是很好):后面长,顶部短,侧面渐细。关注部分而不是整体,将复杂问题分解成简单的小问题,这些小问题可以简单地解决,这是工业化的基础,也是工程乃至整体问题解决的基础。

一个好的理发师可能是一位优秀的工匠,但一旦你可以在该领域的许多专业人士那里得到类似的发型,它就简单地变成了一个行业。在接下来的章节中,我们将应用这个定义来探讨信息技术领域,并展示工业化是如何发生的。为了做到这一点,我们首先需要更深入地了解实现该概念背后的真正含义。

工业化的两个根源——模块化和标准化

在简单介绍我们将应用于信息技术领域时所说的工业化概念之后,我们将更深入地探讨概念中的两个相关运动,即把大问题分解成小问题,这可以称为模块化(因为我们期望整个系统的小模块),以及用标准化的方法解决这些小问题以达到均匀的质量,这可以称为标准化

模块化以降低复杂性

在解释复杂性的概念之前,让我们通过另一个例子来看看它与模块化的关系。这次,我们将通过分析汽车的各个模块来进行机械比较。现代汽车是工程学的杰作,将许多部件以复杂的方式组合在一起,以至于一个人单独构建这样的系统几乎是不可能的。当将汽车分解为模块时,肯定有明确、分离的模块,每个模块都有其目的:

  • 发动机将为汽车提供动力

  • 车身将保护驾驶员和乘客

  • 轮胎和驱动系统将把动力转化为运动

  • 底盘将以刚性的方式将其他模块固定在一起,等等

发动机本身仍然相当强大,但我们可以进一步将其分解为子模块:

  • 注射系统会将气体引入气室

  • 活塞会将爆炸转化为线性运动

  • 曲轴将线性运动转化为旋转运动

  • 润滑系统将确保系统不会因磨损、加热等原因而退化

再次,复杂性已经降低,如果我们进一步分解模块,润滑系统可以描述如下:

  • 泵确保油液循环

  • 油液起到冷却作用并允许无摩擦运动

  • 油滤清器可以去除可能增加摩擦和磨损的小碎片,等等

这次,我们已经达到了如此低的复杂性水平,以至于几乎任何人都可以对这些模块采取行动:换油可以由任何知道在哪里倒油的汽车车主完成;更换油滤清器就像拧下旧的,然后拧上新的,放在同一个地方一样简单。

模块化,实际上是将复杂事物切割成更小部分以便于管理的艺术。如果模块化做得好,每一步都会降低复杂性。想象一下,如果我们把发动机分成左右两部分:我们肯定不会让它更容易观察和维护。确实,模块化不仅仅是系统的切割;它是在智能方式下切割的艺术,以便降低复杂性。但我们如何做到这一点呢?这就是工匠的经验和长期制造历史的帮助发挥作用的地方,提供了足够的专长来知道系统应该在何处,以及什么会使它更简单。最初的发动机肯定没有油滤清器,但经过一段时间,在行驶了几百公里后被迫从发动机中取出所有油,过滤它,然后再倒回发动机,很明显,在发动机油流中插入一个滤清器是明智之举。如果我们试图用一句话来总结这一点,模块应该在功能之后被切割。油滤清器之所以存在,是因为在润滑过程中,必须有一个过滤功能。将这个功能分配给一个模块是有意义的。

为了确保模块化,标准化是有帮助的

不同模块之间的关系、它们如何组合以及它们如何相互作用,这些都是必须考虑的其他标准。仅仅减少模块数量是不够的:如果想让整个系统正常运作,定义更小的模块是第一步,但一旦创建,它们必须重新组合以达到全局目标。这正是模块切割方式的重要性所在,我们之前已经解释过,它应该遵循功能划分。

但如何将它们重新组合呢?如果模块与功能对齐,我们如何确保它们能够很好地结合?实际上,这个问题解释起来很简单,但有时解决起来却极其困难,需要大型工程团队来完成:我们必须确保它们共享的公共功能完全相同。如果两个功能需要重新组装,这意味着它们有一个小的连接子功能是共同的,这通常被称为接口。这个接口必须在两边以类似的方式定义。

让我们再次以我们的油滤清器为例:它已经从润滑系统和其他发动机部分分离出来进行定义,但它也必须放回发动机系统中才能运行并参与更高层次的功能,即向汽车提供动力。为此,已经解释了油滤清器必须拧回到发动机中的位置,这就是需要接口的地方。这个接口只是一个螺纹:油滤清器将提供一个螺纹油,发动机在滤清器必须放置的位置提供一个螺纹凸起,当然,它有一个孔,允许油流入和流出滤清器。接口本身由功能定义:

  • 它应该提供稳定的连接

  • 它应该足够紧以防止漏油

  • 它应该允许足够的流体循环,等等

我们向前迈出了一步,但为了达到工业化,还有另一步要做:接口必须标准化,这意味着所有前面的功能都应该以易于替换的方式指定,以便每个供应商只需了解接口就可以参与更高层次的模块。在我们的例子中,为了参与发动机系统,油滤清器必须遵守以下规定:

  • 使用精确的螺纹直径(对于欧洲油滤清器,它是 20 毫米直径,螺纹步进为 1.5 毫米)

  • 通过一个直径为 62 毫米的圆形接口确保了防漏油

  • 基于流体循环保留碎屑的能力,以及因此滤清器使用的持续时间,由滤清器的体积决定,有两种标准尺寸,等等

这里是一个如何将油滤清器连接到汽车发动机的非常低级的示意图:油滤清器上的螺纹孔与发动机外部的带孔金属螺纹件相适配,以便于访问:

图 2.1 – 油滤清器在汽车发动机上的示意图位置

图 2.1 – 油滤清器在汽车发动机上的示意图位置

这就是我们要找的!如果我们现在回到解释,我们会发现模块非常标准化,可以在任何地方购买,它们将具有相同的接口,尽管它们的内部功能可能不同;组合在一起的模块将各自具有其功能,但为它们共同形成的全局系统提供更高层次、更复杂的功能。再走几步,由许多模块和子模块组成的整个系统将具有这种工业方法无法解决的问题的复杂性。

以另一个与我们日常经验更相关的例子来说明,小型电池和充电器目前正受到政府标准化推动的目标。这在欧洲共同体中尤为明显,USB-C 甚至被强推给像苹果这样的大规模反对者。大型公司几十年来一直在使用许多不同的非兼容连接器和充电器,导致电子系统的大量浪费和用户日常生活中的复杂性,迫使他们在许多不同的设备之间来回切换。这项法律已经在使公众为手机充电变得更加简单方面产生了一些效果。

谈及复杂性,我们不得不更精确地定义这个术语背后的含义,这正是我们将在下一节中要做的。

复杂性的管理

术语复杂性指的是由许多不同部分组成的某物的特性。它常常与复杂性混淆,后者意味着某物难以理解。大多数信息系统都是复杂的,而如何处理这种复杂性可以使它们变得复杂。

复杂性的不同类型

在谈论如何通过将大型、难以操作的系统切割成更小的、更容易处理的小系统来减少复杂性时,我们之前引入了复杂性的概念。在本节中,我们将回到这个复杂性的概念,并首先声明存在两种类型的复杂性,即内在的、功能性的和可避免的偶然的。第一种来自功能本身,如果一个模块要提供这个功能,它不能做得少于这一点。第二种是在实现功能时添加的一切,这不能被视为功能本身的纯粹必要部分。当然,整个目标将是尽可能减少第二种,因为两者相加,而第一种根据定义不能减少。

在我们关于汽车发动机油滤清器的例子中,滤清器内部吸收纸的折叠是内在的复杂性,因为不同的纸张堆叠以及它们如何形成复杂的油流路径是滤清器工作的方式,在纸张的折叠处保留重金属颗粒,而油以更清洁、更纯净的特性到达滤清器的输出端。实际上,滤清器的金属外壳不能被视为过滤操作的参与者。当然——它有助于将纸张片固定在一起并便于操作,但它并不参与过滤:这是油滤清器中的偶然复杂性。

信息系统中充满了偶然的复杂性,考虑到如今最小的文本笔记应用也使用了数千行代码和数兆字节的内存,这仅仅开始显示出问题的严重性。

计算机科学作为处理复杂性的方法

考虑到计算机的设计初衷是提高生产力,复杂性达到如此高的水平可能听起来有些奇怪。毕竟,早期的计算机是为了大幅加速那些否则需要几天、几周甚至几个月的计算而建造的,并且需要仔细的双重检查以尽可能避免错误。由于最初设计的复杂性增加,投资于创建计算机的成本巨大(设计现代计算机和电子芯片是我们文明中最复杂的任务之一),但使用计算机快速产生大量问题的准确结果在很大程度上可以弥补投资。

目前计算机所做的许多工作都具有高度的技术复杂性:在游戏中显示来自 3D 建模的高分辨率实时图像、执行如离散傅里叶变换或蒙特卡洛模拟等长时间计算等。许多这些操作无法由人类或甚至大量人群以相同的精度和低错误率实现。因此,我们可以认为 IT 有助于降低复杂性。

信息系统与复杂性

但与此同时,尤其是对于像我这样在软件领域工作了 30 多年的人来说,这看起来就像是计算机实际上并没有带来那些巨大的功能进步,而这些进步本应是我们对计算能力大幅提升的预期。GPU 的速度快了数百万倍,但游戏体验仅提高了几倍。个人电脑的功率强了数百倍,但语音打字仍然远未完美,文字处理基本没有变化,新功能——大多数时候——充其量是无用的,最糟糕的是变成了臃肿的软件。

正好,随着计算机容量的增加,我们要求它们做更多的事情。虽然其中一些额外的操作带来了新的价值(如机械模型的优化、模拟复杂物理模型的能力等),但很多都是非增值特性(更大的屏幕、无限的颜色细微差别),这些特性确实增加了额外的舒适度,但在业务线LOB)软件应用中,并没有为功能性价值带来任何东西。

总结来说,偶然的复杂性几乎与计算能力的增长同步,因此,剩余的计算能力在处理内在的、以业务为导向的复杂性方面带来的性能提升非常有限。

“作为服务”的概念

幸运的是,关于信息系统的发展也有一些好消息,而“作为服务”的方法就是其中之一。 “作为服务”的方法意味着向用户提供有价值的东西,而不涉及物质部分。例如,基础设施即服务IaaS)为您提供了内存和 CPU,而不涉及计算机的硬件部分;这由其他人处理,通常是云服务提供商。软件即服务SaaS)为您提供可以简单通过网页浏览器调用的软件,而无需担心先决条件、安装、购买许可证等等。

如果我们将这种方法与之前提出的复杂性概念联系起来考虑,我们可以这样说,目标是通过对函数本身不提供,而只提供函数的结果,即所需的服务,来将偶然复杂性降低到几乎为零。如果周围的大部分工件都不存在;只获得了软件辅助程序的结果。例如,在 IaaS 中,整体基础设施本身并不是买家真正需要的:买家并不渴望占用空间的物理计算机、需要局部温度控制、机架等等,但必须经历这种偶然复杂性才能获得 CPU 能力、RAM 使用、存储空间或网络带宽和连接性。

“作为服务”的概念大大降低了信息系统感知的复杂性。当然,没有免费的午餐,整体复杂性仍然存在(甚至有所增加)。但这次不是模块与模块之间的分离,而是功能与功能之间的分离,在服务提供商处理的高技术复杂性和用户保留的低复杂性之间建立了一个清晰的界限。从后者到前者的财务转移可以用这样一个事实来解释:用户获得了专注于增值、面向业务复杂性的巨大优势。服务提供商如何从处理更高的技术复杂性中获得财务利益(对于一个普通用户来说可能是偶然的,但对于服务提供商来说是标准业务复杂性),这来自于他们是这方面的专家,为许多用户处理大量业务,并应用规模相关的成本节约。最终,每个人都从复杂性的清晰划分中受益,这也可以描述为责任和任务专业化的分离,正如之前所解释的那样,这与工业化是一致的。

链接到一个最小可行产品

许多使用敏捷方法工作的人都知道一张著名的图片,它展示了最小可行产品(MVP)的概念,这是 Henrik Kniberg 在 2010 年代中期创造的(blog.crisp.se/wp-content/uploads/2016/01/mvp.png):它展示了产品演化的第一行,从轮子到两个轮子相连,然后到两个轮子和车身,最后变成汽车。在这个过程中,笑脸一直皱着眉头,只有在最后一步才满意。在图片的第二行中,步骤被滑板(悲伤的笑脸)、自行车(中性的笑脸)、摩托车(相当高兴)和敞篷汽车(极度高兴的笑脸)所取代。

这已经被广泛研究,并且是对软件应用从最小可行产品(滑板)到完整项目(右侧的汽车)这一演变概念的绝佳描述。许多模仿版本并没有传达出太多的意义,因为它们遗漏了一些细节。例如,其中一些最终在两条线上结束于同一辆汽车,这完全错误,正如 Kniberg 故意在两个过程的最后展示了不同的汽车。整个故事在blog.crisp.se/2016/01/25/henrikkniberg/making-sense-of-mvp上得到了完美的解释,我当然不会对其进行释义,而是尝试将其与之前关于“作为服务”方法的讨论联系起来。

那张著名的图片中提到的服务是什么?它有汽车吗?没有——拥有或驾驶汽车仅仅是服务本身的一个副作用,而这个服务是“从一个地方到另一个地方”。使用最小可行产品(MVP)将帮助我们尽快收集关于用户实际需求的反馈。现在,如果我们走向“作为服务”方法的极端,并将人的位移(以及可能的行李)作为唯一请求,科幻般的传送就会变得绝对完美!而且我们对可能性和价格更加合理;正如 Kniberg 所说,可能最基本的方法是向用户提供一张公交车票。

这也会是一个有价值的 MVP,但这是忘记了一个事实:MVP 并不意味着设计师没有考虑最终目标:我们提供滑板来收集反馈(例如,“稳定性很重要”),同时仍然想着我们最终想要一辆汽车,也许是因为最初表达的需求是自主旅行。

最重要的是什么——我们将会回到为什么最终汽车不一样的原因——那就是,在考虑反馈的同时,在这个例子中稳定性很重要,设计迅速演变为自行车,这辆自行车更稳定,更容易保持平衡。但这并不是收到的唯一反馈。例如,车辆没有覆盖并不是真正的问题,设计演变为自行车,然后是摩托车,没有风或雨的保护。最终,提出的汽车没有车顶:这不仅是因为它根本就没有被要求,而且因为驾驶时头发飘在风中的兴趣可能源于反馈循环。如果直接创造了汽车,也许客户就不会想到一个敞篷车顶。但要求持续的反馈已经显示出一种额外的期望特征(虽然不是需求,而只是一个“额外奖励”),否则可能无法检测到。

这就是公司在表达他们希望“取悦客户”的愿望时所谈论的内容。我们大多数工程师并不立即理解这一点,因为我们倾向于看到从初始规范中衍生出的一个优化解决方案的问题,但最好的解决方案为客户带来了他们甚至最初都没有考虑过的价值。而且你知道吗?由于所有公司通常都擅长创造预期的功能,这些意外和令人愉悦的功能将成为客户用来区分你的服务与竞争对手的服务的东西!

现在复杂性的概念应该已经清晰了,我们将提出一种如何减少复杂性的初步方法。

标准和规范的好处

本章的第一节什么是行业?开始讨论标准化以及它是模块化有意义所必需的。让我们想象相反的情况,一个系统被任意切割成几个更小的部分,没有考虑如何定义这些部分,它们如何相互作用,以及它们如何可以被改进的版本所替代。结果将是,模块不能在没有了解整体的情况下设计,也不能由现有的模块所替代,因为它们粘合到其余部分的方式可能已经不存在。至多,这只会使整个问题更容易解决;最坏的情况是,将一切重新组合起来的额外难度将大大超过一次性解决整个系统所带来的复杂性减少。

这就是为什么模块的切割界面及其标准化如此重要,为什么我们将用额外的例子在下一节中强调这一点。

Docker、容器和 OCI

Docker技术是讨论规范和标准的一个很好的方式,因为它的名字本身就以一个工业概念的隐喻开头,这个概念通过标准化而繁荣,即货运和集装箱。

直到 20 世纪 50 年代,货物运输根本未实现标准化,用货物装满一艘船是一项相当精细的手艺:包装大小和重量各异,有的柔软,有的坚硬。将它们绑在一起以便在运输过程中不移动的方法在每个不同的运输中都是定制的。正确填充车辆极其困难,因为几乎没有机会让所有包装都能完美地占据所有空间,同时将脆弱和轻便的包装放在顶部,将沉重和坚固的包装放在底部。如果你再考虑负载平衡、湿度或温度效应,这些效应可能会从一个包装传递到另一个包装,以及偶尔出现的最后一分钟包装,它太重以至于无法放在其他包装的顶部,迫使码头工人卸下一部分货物并重新安排一切,你开始理解当时货物运输是多么复杂的一项工作。

见证马尔科姆·麦克莱恩,他在 1956 年设计了一种基于木箱的运输系统,这种木箱可以轻易地从卡车转移到火车和船上。仅在 10 年后,即 1967 年,这个伟大的想法被广泛使用,以至于国际标准化组织ISO)定义了三种“容器”的标准尺寸。尽管道路/铁路/海上运输活动是一个庞大、全球性的业务,但在仅仅几十年后,地球上几乎每个运营商都使用标准尺寸的金属容器,这有助于优化整个物流链,并具有以下优点:

  • 装载便捷:任何调度员都可以轻松地拿到一个容器,按照自己的节奏装载它,然后联系运输公司,让他们携带容器,而不用担心被拒绝,因为他们无法处理特定的形状。

  • 货物处理改进:由于金属箱具有标准尺寸和带有处理孔的角落,因此不再需要更换用于压包(并可能损坏其内容)所需的工具。现在,抓取工具只需锁定容器的四个角落并抬起它们。处理速度也得到了提高,因为不需要逐个处理不同的包装:机器将一个容器(内部携带许多不同的包装)作为一个单一单元抬起。

  • 优化存储:工业容器是简单的平行六面体形状的箱子。它们的堆叠几乎不浪费空间,只是墙壁的宽度。今天,大型船只的尺寸是根据容器进行优化的。

  • 可互换材料:容器已经变得如此商品化,以至于几乎不存在产权问题。容器可以轻易地修复或被另一个容器替换。容器基本上从不空载。其中一些已经环游世界多次,而它们的最初买家再也没有见过它们。

Docker 的名称和标志清晰地阐述了这项技术的哲学:术语docker指的是在船上装载货物的任务,而 Docker 的标志展示了一只鲸鱼背上的集装箱。这个联系对于运输业来说非常明显,公司希望成为应用运输的工业集装箱的等同物。

正如工业运输集装箱一样,Docker 容器提供了基于标准的优势:

  • 无论容器内部是什么(Java 进程、.NET Web、Python 脚本、NodeJS API 等等),外部接口都是完全相同的,人们只需简单地输入docker run命令,容器就可以启动并运行。

  • 一旦应用被放入 Docker 镜像中,它就可以通过注册表发送到地球上的任何地方,并在任何国家以相同的方式执行。

  • 独立于底层架构:Docker 容器不知道或关心它们是在 Windows 机器、Linux 服务器上还是在 Kubernetes 集群上运行。由于它们具有标准尺寸,它们可以适应任何地方。

在所有这些优秀特性的加持下,Docker 迅速成为了应用部署的事实标准。Docker 本身甚至可能成为最终标准,但存在一些不足,几年后出现了一个更高层次、更广泛的标准:开放容器倡议OCI)创建了一个低门槛但不可否认的标准,每个容器技术(包括 Docker,以及其他虽然不太知名的技术)都必须遵守。

容器无疑使应用部署工业化并极大地改善了应用部署的方式。微服务的兴起与容器技术密切相关,因为使用旧的方法手动为每个应用设置依赖项和资源,部署大量小型应用将变得极其复杂。有些人甚至说,微服务架构的出现仅仅是因为 Docker 允许它们存在。

Docker 是技术如何规范特定软件相关功能(在这种情况下,应用部署)并产生巨大影响的一个例子,通过单一标准化方法,可以取代大量专有和手动方法。但这并不是行业里唯一发生这种情况的时候...

IAM 的另一个例子

身份和访问管理IAM)是 IT 领域的另一个领域,标准化在过去几十年中带来了巨大的帮助,并积极改变了困难的情况。还记得每个软件应用都有自己的用户管理和密码吗?更不用说处理组和授权管理方式的不同、不兼容,等等。这样的混乱...当第一个单点登录SSO)方法出现时,该领域的每个人都感到很高兴,中央认证服务CAS)在可用的软件中实现了它。身份和认证提供商使该领域对初学者来说更加复杂,但避免了成千上万的糟糕设计的 IAM 系统,用在线、始终可访问的身份取而代之。

安全断言标记语言SAML)迅速成为标准,像 Shibboleth 这样的工具帮助以正确、开源的方式扩散处理能力。最近,OpenID ConnectOIDC)、OAuth 2.0、JSON Web TokenJWT)和其他标准化方法基本上结束了关于最佳识别、认证和授权账户方式的任何讨论,考虑到需要考虑的新功能,现在几乎覆盖了该领域的所有需求。Keycloak 是一个生产就绪的、基于标准的开源应用程序,可以作为标准之间的粘合剂,这意味着我们现在有了所有真正以标准方式处理身份和访问管理(IAM)的工具。这些好处如此之大,以至于尚未采用这些方法的公司在接下来的几年里将不得不采取措施这样做,因为安全问题将使停止在专有、脆弱的实现上处理 IAM 成为强制性的。

再次,由于标准和模块化分离的方法,IAM 的功能已经变成了商品:

  • 识别涉及账户及其个人所有者的身份,以及所有相关的元数据。轻量级目录访问协议LDAP)和LDAP 数据交换格式LDIF)是这方面的标准,但跨域身份管理系统SCIM)也可以使用,以及如 SCIM 企业配置文件这样的扩展,例如,以纳入组织结构图。JWT 可以用来以规范化的方式携带这些数据。

  • 认证是关于证明账户身份的过程。当然,会想到 OIDC,但快速身份在线FIDO)和通用第二因素U2F)也是与认证相关的标准,它们引入了物理设备来改善认证管理。

  • 授权——一旦通过身份验证证明了身份——就是处理软件(或者,记住信息系统主要但不仅仅是关于软件)中允许人们做什么的方式。可扩展访问控制标记语言XACML)是这方面的一个基于 XML 的标准,但还存在更近期的方法,例如开放策略代理OPA)。

总之,IAM 是工业化的配方一旦被应用后,信息系统如何积极进化的另一个例子:将这个复杂主题划分为明确的、单独的责任,然后对每个责任应用规范和标准。

本章的最后一部分将与其他可能高度复杂且使用大量标准的系统进行类比,即我们许多人居住的城市。我说的不是智能城市,在那里软件服务于城市管理,而是随着时间的推移在组织上出现的城市。

信息系统的城市主义隐喻

为什么我要花费这么多时间和使用这么多文字来谈论那些已经成为标准并对信息系统的易用性和进化能力产生了巨大影响的技术呢?好吧,因为为应用部署和身份访问管理(IAM)所做的一切都可以应用于软件系统中的任何功能。在你系统中需要操作的每个功能可能都没有一个不可否认的、国际认可的标准,但部署一个本地认可的标准将在你自己的信息系统范围内提供完全相同的益处。

这种通过将信息系统划分为区域并标准化它们之间接口来工业化的方法是保持其长期功能健康状态的最佳方法。根据上下文,你可能会听到“业务/IT 对齐”、“企业架构”或“信息系统的城市化”。第三个表达是指一个隐喻,其中信息系统被比作一个现代城市:

  • 组织遵循层级分区:大区域专门用于住宅、商业或工业。在这些区域内,人们会发现定义区域较小部分的社区。最后,街区将社区内的建筑物连接在一起。在一个精心打理的信息系统中,人们会发现同样的层级,其中包含大型业务领域区域(例如,行政),在这些区域内会出现专业方向(比如说人力资源),最后是功能块(在我们的例子中,是招聘管理)。

  • 流体标准化是为了城市能够正确运行:如果消防员必须适应城市不同地区的不同管道直径,当然会出现问题。同样的情况也适用于电力、水管、排水管等等。今天这听起来可能有些疯狂,因为所有这些都已经完美标准化了几十年,但在 20 世纪初,像巴黎这样的城市有多个不同的电力公司,其中一些运营 110 伏,一些运营 220 伏,一些使用交流电AC),一些使用直流电CC),一些频率为 50 Hz,一些频率为 60 Hz,而且大多数使用不同的插头格式。

  • 一个大城市总是在不断发展,城市东部的在建工程旨在对西区的居民生活影响最小。同样的情况也适用于信息系统,其中变化是唯一的不变因素,对某一部分的影响应尽可能小,以免影响其他应用程序。城市建筑师提供全局的视野和演变方向,但城市的日常变化是有机的,并且可能因为标准化而发生。成熟的信息系统也可以做到这一点。

很遗憾,企业架构似乎并不十分普及。这部分的缘由在于这是一个复杂的活动;但也因为缺乏知识和信息的传播,而本书旨在谦逊地提供一种补救措施。我将在接下来的章节中尝试展示,信息系统中的工业化和标准化方法可以带来很多价值,并且可以极大地减少大多数信息系统的僵化和变革难度,而这些知识和实践远不如大多数 IT 架构师所认为的那样复杂。

摘要

本章详细解释了工业化和标准化的概念,然后解释了它们如何应用于软件和计算机科学领域。正如前一章所述,如今许多信息系统都难以进化,尽管工业化是计算机科学中的一个新兴领域,但它是一种显著提高其效率的方法。

在下一章中,我们将更加注重实践,从本章中——诚然是理论性的——材料开始,并解释如何在信息系统中实施工业方法。将要介绍的最知名的方法被称为“业务/IT 对齐”。简而言之,它指出 IT 的结构必须反映信息系统旨在帮助的业务流程的结构。

第三章:实现业务对齐

在解释了全球信息系统的普遍问题和第二章节关于工业化的一般理论之后,现在是时候介绍一些经过实战检验的方法了!尽管在书的第二部分之前,我们不会接触代码或部署软件,但这一章将更加应用性,并将展示所谓的业务/IT 对齐的原则。这一原则背后的思想是,软件系统应该尽可能地反映它旨在自动化的业务领域的结构。从某种意义上说,这是应用康威定律(将在后面解释)的反向,使用它来获得期望的结果。在实践中,了解地图以在地面上统治是很重要的,因此我们将使用基于CIGREF(即Club Informatique des Grandes Entreprises Françaises)等组织推动的四层图的信息系统映射技术。

本章将涵盖以下主题:

  • 商业软件和对齐原则

  • 康威定律应用于应用和系统

  • 介绍 CIGREF 图

  • 使用四层图

  • 对齐模式和反模式

在描述了方法并绘制了与TOGAF(即The Open Group Architecture Framework)框架或其他方法的相似性之后,我们将将其应用于一个示例 IT 系统,以便你完成本章后真正从中受益。最后,我们将看到最佳实践,但也会看到业务对齐的反模式。就像任何其他方法一样,使用四层方法进行业务/IT 对齐有其优点和局限性。了解它们对于尽可能有效地应用该方法以及了解如何使用它来确定在研究的信息系统中何时何地存在对齐问题是特别重要的。

技术要求

如引言所述,本章将比前两章更实用,这两章是理论性的。这意味着有一个阅读前提——既然我们将讨论分析信息系统的方法,那么至少你应该对它们有先前的分析接触。当然,现在每个人都在使用它们,但你需要的不仅仅是使用它们的经验,特别是关于它们由哪些不同部分组成的某些知识。这里没有什么花哨的,但你需要理解软件和硬件之间的区别,以及信息系统通常是为了自动化业务流程而存在的,这些流程是由旨在达到目标的人力和计算机任务组成的集合。你还需要能够识别这样一个系统的不同部分。如果我们把它们称为系统而不是简单的软件应用,这是因为它们更复杂,由多个模块组成。你需要理解这一点,并能够说出系统由哪些部分组成。

你还需要能够对这些系统部分进行分类。它们是根据功能还是根据更具体、与 IT 相关的标准进行分类,比如它们在本地服务器上的位置与云中的位置?它们是自治的还是与其他功能大量通信,如果是的话,通过哪些接口和协议?这当然对我们大多数人来说只是常识或常识,但这是你需要能够阅读这一章的内容。这将帮助你指出关键问题。例如,当谈论系统部分之间的交互时,我们是在谈论业务依赖还是与 IT 相关的具体数据流?为了更好地理解这种差异,让我们回顾一些示例。业务依赖的一个例子是订单系统依赖于客户名单。确实,我们可以用我们的订单记录公司,但不知道购买列表中产品的客户,记录订单就没有太多意义。另一方面,与 IT 相关的数据流的一个例子是订单系统访问客户数据库以提出现有记录。

商业软件和对齐原则

到这本书的这一部分,这应该已经很清楚了,但回顾一下也无妨,我们只是在讨论专业信息系统。简而言之,我们把自己放在一个软件真正意味着商业的情境中:对公司至关重要的应用程序,帮助商业公司或非营利组织生产的信息系统等。以下的所有建议在小型系统中都没有意义,如果应用到简单的软件应用程序上,将会过于复杂。

话虽如此,假设是,既然有商业,那么对它的了解就很好;我们知道商业的参与者是谁,商业的赌注和目标是什么,正在实施哪种战略(即使它没有 100%明确定义,这在很多公司都发生过),等等。最后这一点至关重要;如果没有定义战略方向,就没有必要设计信息系统。

重要提示

应该强调的是,即使定义(即使不完全精确)商业战略也是分析企业架构和信息系统映射的绝对前提。作为一名拥有多年信息系统经验的分析师,我总是在第一次会议中意识到没有公司级战略时拒绝一个项目,并且我建议每个人都这样做。如果你正在阅读这一章,思考你将如何定义信息系统并意识到没有业务定义的战略,你最好现在就停止阅读,等到这些基本信息更加清晰时再回来。在这方面,请相信我——如果你在信息系统目标愿景(至少是全球范围内)清晰之前就意识到即将到来的步骤,你将会浪费很多时间,而且弊大于利。

为什么这一切如此重要?因为你要使用目标业务的定义来设计与之一致的信息系统。技术应该始终服务于用户,所以事先了解业务将推动信息系统的设计(再次强调,不需要一个完美详细的战略,但至少有一个愿景或方向)。这就是所谓的业务/IT 对齐,或者在本书的背景下,简单地说就是对齐。我们将看到,这是你获得一个稳定、有远见的信息系统的唯一方法,不受第一章中描述的问题的影响。但在我们更深入地探讨实现业务/IT 对齐的方法之前,让我们先看看其他非业务相关但由技术驱动的方法,了解它们的应用范围以及在设计复杂信息系统时的局限性。

技术建议的丛林

如果你是一位真正的软件专业人士,并且担心你交付成果的质量,你肯定读过很多关于帮助解决该问题并提高你的软件技能的技术方法。你听说过V 循环;用于组织软件团队的敏捷方法极限编程测试驱动开发行为驱动开发实践;用于改进代码结构的编程模式;用于跟踪代码质量的特定于开发的关键绩效指标;以及更多,一个完整的章节都不足以描述它们。

虽然大多数内容都值得学习和借鉴,但它们的数量本身也显示了这种方法的局限性:它们只在特定的背景下才是正确的(否则,由于数量众多,有些人会反对另一些人),而且遗憾的是,大多数撰写关于这些方法的人常常忘记定义这个范围,因为他们对解释技术本身更感兴趣。技术帮助他们克服特定障碍越多,他们就越倾向于将其作为一项基本、不可或缺的建议来提出。在极端情况下,推荐这种实践的人甚至没有意识到操作背景的小规模,并将这种实践视为普遍适用的,鼓励他人无限制地使用它。

当然,这里需要读者的批判性思维,但与此同时,读者在逻辑上应该是了解作者专业领域知识较少的人,他们可能难以发现内容的局限性。“知识越多越反动”,互联网上充斥着刚刚学会新技巧的人,他们乐于将其作为解决一切问题的方案公之于众。这种热情是可以理解的,我当然也在我的博客或一般培训活动中这样做过,但这并不意味着没有解决方案,而且,再次强调,前进的道路是提高读者的批判性思维。

以 KISS、DRY 和 WET 为例

让我们以一些你肯定听说过的实践为例:KISS保持简单,傻瓜)和DRY不要重复自己)。第一个指出,在创建软件实现时,简单性应该始终占主导地位。这在敏捷方法中尤其如此,因为额外的功能或用户的反馈肯定会迫使代码重写。第二个意味着代码永远不应该重复,并且相似的代码块应该被放入一个独特的函数中,这个函数可以从代码中需要相同功能的不同位置调用。

在进行任何进一步的分析之前,我们应该注意到,这两项建议似乎存在某种相似性,或者至少是非常强的联系。毕竟,如果我们减少了代码重复,事情就会变得更简单(或者至少它们可能看起来是这样,但这将是以下主题的内容)。因此,我们可以质疑这两种方法的使用,但再次强调,软件工程还不是一门行业,所以每种手艺都有自己的工具和用途。这很公平……

但分析这两种方法的真正要点是它们的适用上下文。与大多数技术最佳实践一样,它们并不盲目适用于所有情况,其使用应仔细考虑。当然,当你发现同一个类中多次出现相同的简单函数时,统一它是合理的,但如果有两个不同软件应用中的类具有略微不同的警告标签,这种情况又如何呢?如果文本不相同,也许这是因为警告是在不同的情况下,因此我们应该分析它们被调用的条件。然而,由于软件模块不相同,变量不一定相同,因此分析触发警告对话框显示的情况的相似性将会很困难。那么耦合呢?如果我们决定只保留一个代码,哪个应用程序模块应该拥有它?或者我们应该创建另一个模块来存储对话框的代码?在这种情况下,两个应用程序的生命周期现在影响这个公共库的版本,这可能会成为一个问题?有时,统一代码可能弊大于利。

这种讨论带来了许多反思,并提出了一个新的良好实践,即缩写WET(与 DRY 相反),代表写三遍一切。实际上,暴露出的犹豫意味着,为了找到正确的决定,等待并收集更多关于使用上下文实际相似性的线索是有益的,该方法创造者建议在考虑统一之前先写三次代码。这是一个合理的方法,因为它避免了 DRY 原则的“非黑即白”的方法,并开启了一个对应实际真相的灰色区域:它取决于上下文。先写代码,然后写第二次并观察相似性,最后写第三次并分析统一带来的投资回报,这确实是一个合理的方法...但这是否意味着它成为每个程序员都应该遵守的法律?当然不是——再次强调,需要批判性思维,三次可能并不适用于你。也许你需要五次,也许你将决定限制是基于时间而不是基于出现的次数(例如,等待一年的维护期)。这取决于你,我怀疑选择三次部分是因为它为 DRY 原则提供了一个相当幽默的对比。

所有这些都意味着,在软件开发中使用的许多技术、与代码相关的方 法有时被当作硬性真理或普遍适用的最佳实践,但,大多数时候,它们只是适用于特定上下文的原则,并且(必须)根据其他上下文进行调整。

内部工具箱/框架的特定情况

在我们切换到之前解释的问题的解决方案(而且不用担心,有一些解决方案)之前,一个最常观察到的例子是,在没有彻底的上下文分析的情况下应用最佳实践可能导致不良情况,那就是定制公司框架的开发。在撰写本章的过程中,我偶然发现了一篇来自 Aaron Stannard(aaronstannard.com/dry-gone-bad-bespoke-company-framework/)的精彩文章,恰好完美地反映了我 25 年来有或没有框架编程的分析,创建了一些并诅咒自己这样做,崇拜一些其他框架并意识到它们为负责的软件带来了巨大的价值等。

Aaron Stannard 在这篇文章中解释了 DRY 原则应用过于严格对开发专用框架以统一编码的团队产生的严厉后果,最终他们得到的结果与预期的相反,包括样板代码减少、扩展能力和代码质量。

一些框架为软件应用和信息系统带来了巨大的价值,你将很容易找到它们:

  • 每个人都知道它们,并且乐于使用它们。

  • 新人认为这个框架有助于提高他们的生产力和速度。

  • 所有用户都可以用很少的句子描述框架的功能。

  • 该框架依赖性非常少,可以轻松使用。

  • 这非常重要,以至于开发者会做到他们不喜欢的事情,只是为了保持它:编写文档。

相反,有些代码框架会阻碍编程过程:

  • 它们是由少数专家创建的,其余团队不使用它们。

  • 新人往往会直接编写等效的代码,并质疑框架是否有助于他们提高生产力。

  • 他们有时做很多事情,以至于没有任何一个功能是稳定的。

  • 他们带来了一些其他限制(仅操作一种类型的数据库、需要脚本模块、提升权限等)。

  • 他们的功能存在于专家(通常是单个人,这对公司来说很危险)的脑海中,并且培训同事使用它是一项挑战。

在这两个光谱端之间知道自己的位置困难,与框架的创造者当然会对他们的“宝贝”有偏见的事实有关。他们总是会高估使用它节省的时间,或者忘记或高估维护它的时间,向团队中其他人传授它的时间,公司把重要依赖放在一个人手中的风险,等等。如果你必须评估框架的使用,你应该将这些因素放入电子表格中,并冷静地评估短期、中期和长期的回报率。

最后,现代编程平台现在已经带来了如此多的工具,以至于框架的存在本身都值得怀疑。例如,.NET Core 框架版本 8.0 拥有如此庞大的生态系统和基类库,十年前框架被创建的所有目的都简单地消失了:

  • 对象/关系映射由Entity Framework负责

  • 带有对象映射的 API 请求/响应由ASP.NET Web API和集成的JSON/XML序列化处理

  • 监控由可以连接到任何第三方监听器的日志堆栈负责

  • 一致的页面描述是通过Blazor完成的,包括样式处理

  • 移动应用的部署是通过Multi-platform App UIMAUI)实现的,它还与 Windows 前端统一,等等

因此,我对这个关于框架的建议是尽可能等待,在创建框架和尝试避免它(大多数时候,承认吧,你想要创建框架并不是因为它对你的业务有好处,而是因为它编写起来很有趣)。如果它必须到来,它就会到来,这就是新兴代码架构(如果你还没有听说过这个概念,我们很快就会回到这个概念)发挥作用的地方。

唯一稳定的指南:与用户的业务对齐

在这次漫长的离题之后,我们回到了最初对软件中何时应用最佳实践和所谓“原则”的难度的思考。然而,我们迫切需要关于信息系统架构的指导,因为风险太高,我们也看到了如果在这方面失败,对业务的影响有多么糟糕。那么我们如何知道?是否存在一个始终适用的合理且稳定的方法?一些我们确实能够真正依赖的、在设计对公司未来至关重要的信息系统结构时能够真正依赖的定律?是的,确实存在,而且它不是一项技术规则,而是一种技术决策的方法:始终将其与信息系统的业务联系起来。这是业务/IT 对齐的根源。

在本章中,我将重复这个规则,并以许多不同的方式解释它,因为它对于正确的信息系统架构至关重要——推动信息系统设计的是业务的架构。这一点对于概念尤其如此,为一家公司创建成功的软件骨架总是从对业务的完美理解开始。客户是什么?我们销售什么产品?公司应该计算机化的主要流程是什么?大多数这些问题听起来很平凡,但它们之所以如此,仅仅是因为人类大脑能够适应背景环境。

让我们首先来探讨一个非常基础的问题:什么是客户?对于公司里的任何一个人来说,这个问题显然得出了答案,以至于很少有人会提出这个问题,如果任何员工质疑它,听起来会相当荒谬。“客户是我们与之做生意的公司”。好吧,那么个人呢?是的,有时我们也会与个人打交道;这是 B2B 和 B2C 之间的区别。如果我们质疑“做生意”这个术语呢?我们谈论的是频率和数量?当然,任何数量都可能被考虑在内,一个人从你这里买一个简单的螺栓可能被认为是一个客户,就像一个公司每月购买数千个螺栓一样。但是时间呢?你会认为二十年前从你那里购买过产品的某个人仍然是客户吗?不,当然不会。那么一年前呢?是的,当然……在这种情况下,界限在哪里?十年?五年?决策,决策,决策!

关于这个先前的例子,你可能会发现一些公司,那里的经理在营销和商业之间对客户的精确定义并不一致……那么,你期望一个愚蠢的计算机如何来决定这个问题呢?残酷的事实是,如果你想用软件和数据流来替代一些任务,你必须让它绝对清晰,才能让它工作。这就是大多数信息系统失败的地方——它们没有以完美的业务视角来设计,让一些细节留给了人类对系统的实施或使用。当人类在计算机中补偿缺失的知识时,它可能在一开始还能工作一段时间,但迟早会出现问题。在最好的情况下,系统永远不会像预期的那样高效。在最坏的情况下,带着他们补偿性知识离开公司的人可能会使系统陷入停滞。

这个例子告诉我们什么?首先,在制作软件信息系统处理业务定义时,应该非常明确。其次,这些概念往往是业务规则,这意味着它们并不完全稳定,而是取决于业务如何运作。如果有一天你的老板决定客户名单不应该包括在过去三年内没有从你那里购买过任何东西的人,而之前这个期限是五年,那么客户的定义可能会随着时间的推移而改变。如果这种情况注定会时不时地发生,那么当然,限制其对信息系统的影响是至关重要的。将这个逻辑放入统一的一行代码中,或者更好一点,放入一个参数中,在这种情况下是一个很好的举措。

相反,将客户列表及其定义存储在单个表中将导致高度耦合,因为当“客户状态”发生变化时,定义(名称、地址、联系方式等)不会改变。如果从客户列表中删除一家公司意味着您必须从数据库中删除条目,那么这可能会影响其他仍需要这些数据的功能(例如,保证管理、会计师等)。我们将在第九章中回到这个例子,并详细介绍如何正确建模的一些细节,但就目前而言,请记住,业务思维必须始终指导您的软件概念如何运作。对齐不是从无中生有或双向产生的——它是由功能驱动的,软件应该盲目遵循业务领域本体。

事实上——这将是本节最后的要点——坚持业务领域现实甚至应该更进一步,特别是要随时间坚持。这意味着您的模型应该始终适应时间,因为业务总是变化的。变化是生活中唯一不变的因素;企业无法逃避这一点,反而高度依赖于它。随着不断变化的业务规则、越来越复杂的组织以及更高的功能复杂性,信息系统必须从一开始就设计来适应变化。时间管理非常重要,下一章将完全致力于此。

关于公司数字化转型的插曲

当我们谈论业务领域的软件表示时,我们不妨进一步探讨这一点,并观察软件是如何“吞噬世界”的(《软件正在吞噬世界》,马克·安德森,2011 年)以及为什么这种积极的数字化转型与之前讨论过的业务对齐概念相关。一个架构胜过千言万语,图 3.1图 3.2应该解释数字化转型对我们运营方式产生的主要差异。

在数字化转型之前,人工操作员是业务运营的中心,并在现实世界和业务(或其部分,如我们之前所见)的计算机化视图中进行操作:

图 3.1 – 当人工操作员在业务运营中心时的架构

图 3.1 – 当人工操作员在业务运营中心时的架构

数字化转型带来了一种全新的方法,其中软件成为人类的主要操作工具,并在现实世界中代表人类用户进行操作。硬件和软件系统从人类界面和传感器接收命令和信号,将它们翻译,并将它们作为图形用户界面发送回人工操作员(以及具体现实,通过机械操作员或其他方式):

图 3.2 – 数字化转型的架构

图 3.2 – 数字化转型的架构

这有一个特定的后果,即 IT 现在处于中心位置,与现实世界和人类进行交互。根据你的观点,计算机帮助人类避免了直接交互带来的麻烦,或者它们使我们远离了与世界的直接互动以及与潜在偏见和现实错误表征相关的所有风险。社交网络是这些负面影响的最重要表现,作为软件工程师,你应该始终意识到这种风险,并相应地设计系统,因为它们对现实世界的影响现在已经确立,该领域的从业人员应该保持道德和负责任的态度。

希望这使数字转型和人与计算机之间的交互更加清晰。现在让我们集中讨论人类如何影响软件的组织方式,并讨论一个称为 Conway 定律的东西。

Conway 定律应用于应用程序和系统

我们在本章的第一节中谈了很多关于所谓“软件定律”的局限性,所以你可能想知道为什么我现在会花几段篇幅来谈论一些乍一看可能相似的东西。没有什么能比这更不同了...Conway 定律是信息系统设计的真实、稳定的指南,因为它不提出建议,而是从多个观察中提炼出一个理论,并让一个人自己对其主题得出结论。

Melvin Conway 在 1967 年指出,“任何设计系统的组织都会产生一个结构与其沟通结构相复制的设计”。在我们的案例研究中,即信息系统,这意味着结果系统的架构将反映定义该系统的团队的结构性组织,这会意味着以下内容:

  • 一个前端和后端之间有强烈分离的团队将产生一个系统,其中这两个软件功能确实是独立的

  • 一个根据人们的业务领域知识分组的人将产生一个具有清晰业务对齐服务的信息系统

  • 一个只有一个人的团队将产生一个非常紧密、类似单体系统的系统

  • 一个部分之间没有或很少沟通的团队将创建一个系统,其中模块之间不能正确地相互操作

这个最后的例子可能看起来很极端,但遗憾的是,大多数信息系统都是这种情况,因为团队通常是以单一软件组件为出发点组成的,导致许多应用程序组成系统时,事先没有考虑到相互操作。因此,链接是在需要时以点对点的方式建立的,导致脆弱的链接和低效的系统。

自从康威的这一初始经验观察以来,同名的法则已经被验证了许多次,尽管它不能像数学法则那样被证明,但现在被认为是非常可靠的。它被认为是一个如此强大的法则,以至于系统设计者开始使用这个法则以结构化团队的方式,使最终系统呈现出期望的形状。在这种方法中,法则不仅被视为一个后果,而且被视为塑造系统所需的辅助工具。我所谈论的不仅来自我的个人经验,而且已经被正式化,命名为逆康威机动,因为许多其他软件工程师也有同样的方法。例如,马丁·福勒(Martin Fowler)提出了这个法则(martinfowler.com/bliki/ConwaysLaw.html),甚至将其与领域驱动设计DDD)联系起来。这将在下一章中进一步解释,该章节专门用于解释与设计信息系统部分相关的语义——与 DDD 中的通用语言概念相关——的重要性。

使用逆康威机动,可以通过影响设计该系统的团队之间的沟通和结构来影响系统的最终设计。这是实现我们自本章开始就讨论的非常需要的业务对齐的绝佳方法。通过定义与业务领域并行的团队,并给他们提供相互讨论的业务相关概念,最终形成的系统将由具有明确功能责任和模块间良好交互操作的模块组成,有利于系统的长期演变。

影响系统的对齐是很好的,但大多数时候,人们会遇到一个现有的对齐,唯一可能的是理解其状态。这就是我们需要一种方法来分析现有对齐的地方,这就是专门的图表方法有用的地方。

介绍 CIGREF 图

正如我们所见,业务对齐与与功能领域相关的词汇和概念的正确表达有很大关系。对于我们更习惯于模式的人来说,存在一种更图形化的方式来可视化这种对齐,这被称为四层图

在法国(我的祖国),它被法国大型企业信息技术俱乐部(一个大型法国软件架构俱乐部)普及,但这是非常普遍的一种思维方式,并且没有人声称拥有这个想法,至少据我所知。这个概念相当简单,是关于将信息系统不同层级分开,每个层级都使用我将要介绍的层级来工作。在图的顶部,人们会找到系统所服务的业务流程,再下一层是为此所需的业务功能。这两个层级完全是功能性的,甚至与软件无关;一些任务和功能可以通过人类实现,而不会产生任何影响。底部的两个技术层级分别是软件和硬件,前者使用后者。指定系统(或更确切地说,其最外层结构)的这种方式可以概括如下:

图 3.3 – 四层图

图 3.3 – 四层图

让我们在接下来的几节中深入探讨每个层级,特别是展示每个层级的详细内容是如何表示的,因为这个方案只是象征性的,并不包含每个层级的实际内容。

重要提示

在本章的剩余部分(以及本书的后续部分),我们将经常通过它们的编号来引用这四个层级或水平,从顶部开始(层级 1层级 2层级 3层级 4)。下一节将详细介绍层级 1,以此类推...

流程层级

业务流程是一系列旨在达到既定目标的任务。由于我们谈论的是 IT,其中当然至少有一些将是自动化的,但也可以有由人类激活的任务。流程用于结构化业务或任何组织的活动,业务流程建模(这是接受的名字)是关于表示这些以记录、更好地理解并提高实体的效率。

存在一种业务流程表示的标准,即 BPMN,代表 Business Process Modeling and Notation。这个标准目前是 2.0 版本。你肯定已经见过这种图表,它自己就能读:

图 3.4 – BPMN 示例

图 3.4 – BPMN 示例

在这个公司新流程的非常简短且不详细的例子中,你会发现 BPMN 标准中最常用的组件:

  • 任务是带有文本的盒子,通常以动词开头,描述所表示的活动。任务内的图标可能指定它是否是自动化的、有用户输入、完全是手工的等。

  • 任务之间的箭头表示流程中的信息流,以及任务处理的顺序。

  • 菱形框会介入这个信息流中,以包含诸如选择或并行活动等复杂性。

  • 事件由圆圈表示。一个流程总会有一个开始和一个或多个结束。它也可以有中间事件,所有这些都可以反映事件类型,如基于时间的、基于消息的等。

小贴士

关于 BPMN 的更多信息,我强烈建议研究可从www.bpmb.de/index.php/BPMNPoster获取的 BPMN 海报。

尽管 CIGREF 图的第一层主要基于 BPMN 图,因为它们是业务流程表示的标准和这一层的主要内容,但在这个层次中也可以找到一些附加信息,这些信息是相互关联的。例如,可以指定与业务领域相关的规则为DMN决策建模符号),并将其添加到这一级,因为它们对业务流程本身有影响。顺便说一下,DMN 是 BPMN 标准中包含的一个“子规范”。

根据想要获得的内容,第一层地图可能非常粗糙,细节很少。这是一个我从一家动物遗传学公司那里得到的例子,该公司已经对其流程进行了完整的 ISO-9001 图示,只需知道哪些信息系统部分与哪些业务流程相关(出于保密原因有意模糊):

图 3.5 – 流程图的示例

图 3.5 – 流程图的示例

由于我的这位客户在流程本身上没有问题,在整个对齐项目中,其表示保持得很简单。这里的唯一微妙之处在于,运营流程已经与支持流程和试点流程分开。

相反,以下图是我为另一位客户创建的地图中第一层众多流程之一,这次是一个 IT 问题主要来自流程定义不足(当然,当目标不明确时,很难有一个高效的 IT 系统)的组织。在以下图中,文本的可读性不是目的,因为我只想让你看整体流程图:

图 3.6 – 详细流程

图 3.6 – 详细流程

第一层当然是你的信息系统地图中最重要的一层,因为没有任何东西依赖于它。所有其他层都依赖于它,但流程层必须从头开始设计,纯粹基于业务领域知识。而且,正如所解释的,如果流程不清晰或设计不当,这当然会影响软件,随着时间的推移,也会影响整个信息系统的效率。

强调这一层的重要性很难过分。这并不意味着它必须用很多细节来绘制;这些细节只有在有人发现流程中的问题并需要深入了解以改进或纠正它时才是必要的。但第一层必须正确,并涵盖整个组织的范围。

功能层

CIGREF 地图的第二层表明了上述流程的任务可以使用哪些功能来实现。这仍然是一个功能层,但这次,组织不同,因为这个层的原子是关于谁可以做什么。

确实存在一些不随流程变化的函数。业务流程必须能够根据战略进行变化,但有些事情是稳定的。在我们的图 3.3示例中,欢迎新员工加入公司的一项任务是拍摄他们的照片。这可能对其他流程中的某些事情有用,但拍照这一行为是组织的一部分,可能由特定的人来完成,并需要专门的材料,如相机。因此,这是一个函数,必须在这一层进行登记。许多流程可能会指向它,但这个函数将在第二级地图的一个地方保持不变,与其稳定的属性一起。

其中一些最重要的属性与组织内部函数的组织有关。事情可能会变化,没有固定的标准,但至少有一个使用了数十年的好隐喻,并且取得了良好的效果,即创建信息系统与城市组织之间的平行关系(在法国,这种方法被称为“城市化”)。这个隐喻导致将 CIGERF 地图的第二层分解为三个深度的层次:

  1. 区域/区域是这一层中的较大群体。它们对应于整个系统(或在我们的隐喻中,城市)的组织方式。在一个城市中,人们会发现商业、工业和居住区通常明显分开;在功能层中也应该找到同样的情况。

  2. 季度/邻里是对系统本地组织的一种更细的划分,其中人们、企业,或者在我们的案例中,IT 功能可能会相互交流。

  3. 块/岛屿是人们相互认识并经常互动的精细划分。在相应的 IT 定义中,这些包含使用相同工具或由相同团队操作的相关函数。

我经常被问及应该使用什么样的分解方法,至少对于最高层来说。这是一个难题,因为业务能力地图BCM)(正如这个 CIGREF 地图的第二层经常被称呼)在大多数公司中实际上是不存在的。由于 ISO 9001 认证和相关方法的标准化,许多公司对自己的流程有清晰的了解。公司也可以追踪他们的软件层,至少对于他们被收费的最大块来说是这样。但在中间,这个 BCM 经常被遗忘,我们将在稍后看到,这是系统不匹配损失的原因。

这一层被遗忘的事实本身在很多信息系统的问题中就占据了很大一部分,因为领导者可能更倾向于过程层(目标)而投入较少的努力在业务连续性管理(如何实现它们)上。然而,正如谚语所说,没有计划的愿景只是愿望,而精心构建的系统功能组织是迈向成功的一大步。

在这个与城市发展相关的隐喻中,业务能力图被切割的方式与一个大城市的组成相关联;就像城市中有工业、商业和居住区一样,在信息系统中也将会有一个全球组织,通常在以下五个区域:

  1. 主数据管理是信息系统最重要的数据被管理的地方(客户、产品等)。这些数据将被系统的许多部分和参与者使用。因此,它值得专门的治理(明确的责任定义、实践和工具等)并在地图的第二层拥有自己的专用区域。当对如此重要的数据缺乏明确的治理时,常常发生不同的小组会重复它们,这不仅成本高昂,而且在需要实现交换时可能会带来复杂的问题。

  2. 共享工具不是数据,但仍然是将在信息系统和大多数流程任务的其他许多部分中引用的常用功能。它们通常被分类在专门的区域,在那里可以找到办公自动化工具、内容管理系统、身份和授权管理等等。

  3. 面向外部的功能(有时称为“协作”)是关于与信息系统本身范围之外的功能进行互操作或交换的所有功能。这通常是人们会找到外联网或商业网站、与合作伙伴的联系等地方。

  4. 治理/试点是那些曾经用于监督系统本身的功能所在的区域。报告功能将被放置在那里,以及关键绩效指标、高层管理功能等等。

  5. 面向业务的职能是最后但同样重要的一个领域。这是公司组织与其核心价值和运营领域相对应的所有职能的地方。如果你是一家制造机械部件的公司,你将在这里找到所有与工程、生产、库存、销售、维护和安装相关的职能。如果你从事电子商务,将会有买卖职能、物流、网络运营、安全等。还将有一些支持核心业务的职能组,如人力资源、法律和行政职能,这些在大多数公司中都是通用的。当实体组织得很好时,这一区域的不同方向与组织结构图中的不同方向相对应是显而易见的(并且是良好一致性的证明)。相反,如果你很难判断某个特定的职能是否属于这样的方向或服务,这可能是一个缺乏一致性的迹象。

下一个图例是一个遵循五个区域原则的 BCM,主要部分按照组织结构图的方向进行分解。再次强调,这只是一个常见的模式,并不构成任何推荐。应根据个人判断调整 BCM 的分解以实现一致性。再次强调,就以下图表的文本可读性而言,文本内容并不重要,重要的是结构:

图 3.7 – BCM 示例

图 3.7 – BCM 示例

对于这个轶事,这五个区域也出现在法国政府的信息系统 BCM 中,四个支持区域围绕在中间,而面向业务的职能位于中间,在这个背景下,由法国政府组成的各个部门将其分开。我用黑色线条突出了这些分隔,因为这正是我想展示的(文本的可读性并不是目的所在):

图 3.8 – 法国政府 BCM

图 3.8 – 法国政府 BCM

我们将回到 CIGREF 第二层的重要性。正如所述,第一层表明了愿景以及公司为市场带来的价值,但第二层则是关于如何操作性地实施这一愿景,这正是无序信息系统所缺乏的地方。在我能够观察或帮助精炼的数十个信息系统中,业务连续性管理(BCM)总是最不受控制的层。这种对它的错误处理是信息系统无法令人满意的根本原因。

软件层

在 CIGREF 地图的下一两层中,我们进入了技术的领域。到目前为止,流程和功能完全是业务相关的,一个人完全可以使用它们来构建一个无计算机的信息系统。但今天我们并不是这样,这本书的分析主题也不是这个。我们将研究基于计算机的系统,至少部分是自动化的系统,而上面两层所依赖的两个技术层是关于用计算机实现功能的。它们的分离非常简单:第三层是所有“软”的东西,这意味着非物质的、虚拟的和非具体的,而第四层则将所有具体的东西分组。简单来说:如果你能触摸到它(计算机、网络电缆、设备、数据中心——即使墙壁不属于你),它将进入我们稍后将要覆盖的下一层。如果它是技术性的但不可“触摸”,例如软件应用、数据库、信息流、API 实现等,它属于 CIGREF 地图的第三层,我们马上会详细说明。

这个第三层在第一次实施时通常对公司来说相当容易:查看账单并询问人们他们使用什么软件,通常足以找出系统中软件的 80%最重要的用途。但是,即使详尽的参考不是目标,这也可能不足以两个原因。

首先,可能存在一些公司不知情的情况下购买的“隐藏”软件(这被称为影子 IT,可能是一个需要维护或强烈所有权的难题)。如果这些软件是战略性的,它们可能不会出现在地图上,而且如果使用它们的人突然离开,相关的功能在没有人的理解下崩溃,这也可能成为一个问题。如果你听说过某个公司在关键人物退休后出现软件问题,这就是我们要讨论的内容。

第二个问题是,软件不仅包括应用程序,还包括数据,而数据通常在系统中更难定位和追踪。当然,你可以通过它们的商业许可证或使用的 IP 和端口来定位数据库。但你会发现数据存在于许多其他地方,比如令人讨厌的 Excel 工作表。再次强调,谁没有听说过一份对公司来说如此重要的 Excel 工作簿,以至于每个人都听说过它?在我陪同的一家在业务/IT 对齐的公司中,有一个叫做“Serge 的 Excel 文件”,当我试图弄清楚公司文章和价格的真实来源时,每个人都向我提起过这个文件。结果发现,在这个近千人的公司中,根本没有任何产品信息管理的治理,而这个名叫 Serge 的人,在某个他急需信息的时候,承担了从商业、行政和工程收集数据并将其汇总到 Excel 工作簿中的任务,尽力追踪变化、新产品、停售日期、价格变动等。由于这不是他的主要工作,他几乎没有时间这样做,文件的内容既不完整也不免出错。但既然这是唯一可用的数据来源,每个人都迅速复制了 Serge 的文件或通过服务器链接引用它。管理者们从未考虑过这种对如此重要数据来源(也许甚至是一家商业公司的首要参考,连同客户名单)的脆弱方法。当 Serge 最终离开公司时,发生了什么?系统逐渐恶化,因为没有人负责维护这个单个人的、未记录的工作。信息变得混乱,订单开始出错,价格无法调整,因为大多数使用文件或在其上创建的连接器中的信息的人根本不知道数据从何而来,等等。

有些人可能会反对说,我所谈论的并不是第三层的一部分,而是与第二层相关,确实,主数据管理、数据治理和所有权必须在第二级详细说明。但在这个案例中,我想表明,糟糕的技术实现(这肯定属于第三层)以及对软件形式中的数据位置缺乏理解是问题的根源,这个问题直接影响到地图的第一层,并导致公司的两个主要流程——生产和销售——脱轨。

对这一第三层内容的分类实际上取决于许多因素。一些拥有强大内部 IT 和编程能力的公司倾向于由操作该技术的技术团队对应用程序和数据分组。我也见过其他人按技术对软件层进行分组,尽管他们的大部分技术是现成的,但他们的主要关注点是内部操作这些技术。在某些情况下,软件可以按编辑进行分类。还有许多其他分类方式。在下一个例子(再次为了保密原因而模糊处理)中,切割是使用功能域进行的,因为公司相当大,软件应用程序和数据的责任受到了业务方向和服务的影响(这是一个很好的实践,因为软件应该始终服务于功能):

图 3.9 – 软件层

图 3.9 – 软件层

硬件层

如前所述,硬件层列出了并组织了从具体的信息系统到的一切。毕竟,计算机化系统的重要部分是数据和虚拟功能,它们极大地加速了过程,但我们绝不能忘记,即使在遥远的云位置没有人真正关心,所有这一切都是由电子在电子芯片和电路板中流动实现的,还有电源、电缆、硬盘和屏幕。

这个层现在非常标准化并且受到控制,在我分析过的数十个信息系统中,几乎没有一个因为硬件问题暴露出这种限制。事实上,这种情况如此罕见,在大多数我制作的 CIGREF 地图中,第四层非常薄,几乎没有细节,有时甚至根本不表示。例如,如果我们看我所展示的不同层的整体图景作为样本,那么硬件层就没有任何表示:

图 3.10 – 只有三层(文本的可读性并非目的)

图 3.10 – 只有三层(文本的可读性并非目的)

幸运的是,公司老板们非常清楚的一件事会始终提醒我们这一层的存在,那就是他们的成本。即使机器越来越虚拟化,变得无形,财务成本仍然存在。随着时间的推移,信息系统和数据中心的环境影响最终也成为方程的一部分,使得这一层变得更加明显。

如果你必须绘制一个硬件层,你会发现很多优秀的绘图系统,它们可以区分服务器类型,可以提供专用图标和元数据,以便你可以单独列出硬件原子等。总的来说,再次强调,这是一个非常受控和标准化的层,这确实解释了为什么在谈论 IT 对齐时,我们很少需要在这个层上工作,除了引用它来完成软件成本和平衡第一、第二层预期的收益。

为了提供一个硬件层的例子,这里是一个公司在逐步外部化其 IT(橙色部分是直接由 IT 服务操作的服务器)中的这样一个层图的时间序列:

图 3.11 – 硬件层

图 3.11 – 硬件层

由于这个层(当然不是层本身)的表示相对较低的重要性,我们不会进一步深入描述如何使用该图来映射硬件层。分组也很明显;大多数情况下,它们基于数据中心,物理位置在顶部,然后是独立的物理服务器,接着是虚拟机等。网络也用标准符号表示,总的来说,对于这个层,图是通用的。

使用四层图

CIGREF 映射的原则现在应该已经很清晰了,因此我们可以看到如何使用这项技术来提高对齐度,从而提高信息系统的效率。正如所说,在接管一个系统时采取的第一个行动就是创建它的映射。否则,简单地没有方法能够舒适地处理这样复杂的集合。这意味着当然,为现有状态的信息系统创建映射是首先要采取的行动,而 CIGREF 映射非常适合这个目的。但关于如何做到这一点仍然有很多问题。本节包含了一些经过实战检验的建议,关于如何使用映射技术。

作为一条重要提示,不要担心你在本章结束时还没有确切地知道如何使用 CIGREF 方法。现在,我将只向你展示如何绘制它以及如何在其中发现业务/IT 对齐的问题,但本书的其余部分将展示许多 CIGREF 映射在实际中用于许多不同目的的例子。这意味着它将有望变得更加清晰,它在分析和构建信息系统方面的强大功能,你不应该因为还不确切知道它在实践中将如何被使用而感到担忧。

我们应该映射什么?

首先,一个人永远不应该详细绘制整个系统。当然,有一个覆盖整个周界的粗略地图来了解我们正在处理什么是至关重要的,但只有需要关注的系统部分应该被详细绘制。这听起来可能很显然,但在我所指导的团队中,有很多次在解释了方法几周后,我意识到他们已经详细绘制了信息系统的每一部分。因此,这个建议需要被提出。

这似乎是一种自然的反应,特别是对于系统中有严重问题的团队来说,绘制一切,因为它给人一种恢复一点控制感的感觉。遗憾的是,这不仅是在创建地图时的浪费时间,而且在人们试图根据系统的演变调整图表时也是浪费时间。这不是系统绘图的用途。地图是用来预测演变并在尽可能多的方向上推动它的。因此,它只应该在我们正在工作的系统部分进行详细绘制。提前绘制是浪费,因为我们大多数情况下在开始处理这个系统部分时还得重新绘制。同样,绘制一切也是浪费时间,而且更多,因为(希望)系统中有一些部分我们永远不会处理,仅仅是因为它们已经运行得很好了!

再次,当解释时这听起来可能很显然,但当一个人开始绘制一个信息系统时,似乎会发生某种狂热的绘制过程,我见过很多受过良好教育且有经验的人意识到他们白费力气了(我最好的例子是一个拥有数千名员工的组织,其 IT 团队为建立进入内部餐厅的徽章绘制了一个流程图;当然,这个流程图从未被使用过,甚至除了它的创作者外没有人读过)。

以下方案从视觉上解释了地图应该如何随时间演变,只有当需要时才详细绘制系统的部分:

图 3.12 – 绘制演变

图 3.12 – 绘制演变

这个问题的另一个后果——而且更为微妙的是——那就是那些已经详细绘制的信息系统部分,在地图被用来改进它们之后,就不再需要更新了。在图 3.12 中,这就是第二步骤中已经细化的四分之一所发生的事情。在第四步骤中,它不再详细了。这听起来可能有些反直觉;的确,一旦对这部分进行了详细的工作,为什么就放弃它呢?同样的原因适用:如果这部分现在组织得很好,并且业务和 IT 之间已经达到一致,那么我们可能不会回到它这里,或者如果我们确实回到了这里,地图可能已经改变了。那么为什么还要浪费时间在上面呢?

如何开始绘制四层图

从一张空白的纸开始总是困难的;当 IT 问题影响到公司的活动时,这更加困难。为了简化开始映射活动,有时使用纸张或白板来给出系统的初步视图会更容易一些。我喜欢展示以下这个信息系统的“初稿”,因为这个系统是因为一个与之相关的轶事而被我分析:

图 3.13 – 使用 CIGREF

图 3.13 – 使用 CIGREF

这家工业公司在稳定科学分析和将结果传达给客户的数据流方面遇到了困难。我们(IT 团队和我)开始花几个小时绘制四个主要流程、不完整但足够接近的 BCM 以及涉及其中的应用程序以及数据流。在那个阶段,我们正准备评估所绘制的交换频率以确定弱点。然后,公司的总监经过,看了我们做的几秒钟,指着代表应用程序的两个便签说:“嗯,问题显然在这里”。这个人没有任何技术或 IT 背景,但看到大量数据流会进出这两个实体,他立刻理解到它们阻碍了系统的效率,特别是其演进能力。经过进一步分析,发现其中一个应用程序已经过时,第二个应用程序存在复杂性问题。因此,系统被重新设计以改进,主要步骤是为第一个应用程序集成一个新应用程序,并为第二个应用程序创建一个超集 API,以更干净的方式公开旧功能。

这显示了良好信息系统映射的力量,因为它有助于所有参与者——而不仅仅是技术熟练的人——理解他们 IT 中的情况,现在几乎在所有地方,他们的主要工作工具就是 IT。

在对信息系统进行对齐时遇到的一个普遍困难

系统中几乎总是会发生的一个问题,并且会对其映射产生影响,那就是在第一层和第三层的依赖关系之间经常出现混淆。大多数功能人员倾向于认为他们的 IT 系统实现了与设计完全一致的过程;他们多么天真啊... 软件不一定是在内部制作的,当从货架上购买时,几乎不可能完美地符合公司的流程。当然,每个新的软件项目开始时,每个人都会发誓他们会遵守编辑的逻辑,调整他们的流程以保持解决方案通用,避免昂贵的特定定制。但大多数情况下,现实是 IT 最终会像在方孔中锤圆钉一样整合软件,结果不会整洁。

当职能人员用技术术语表达自己,而没有完全理解他们所说的话对系统的影响时,这种缺乏区分的情况也会发生。来吧,我们都有过经理或大老板谈论 ESB 或 ETL,好像他们自己能直接用 XML 编写 Camel 路由的经历!这种对 IT 简单性的过度自信,一旦绘制了 BPMN 并使职能作者确信它可以直接执行,就像一个已经存在端点的 WS-BPEL 架构一样,也会带来一些有趣的关于项目持续时间的说法。

让我们举一个例子,并想象你已经得到了一个像这样的 BPMN 架构(法文标签无关紧要;只需观察图表的结构以及所有任务都是手动的事实):

图 3.14 – 手动流程

图 3.14 – 手动流程

现在想象一下,由 IT 提供的实现此过程对应的数据流图,可能就像这样:

图 3.15 – 软件中的流程

图 3.15 – 软件中的流程

任何技术专长都不需要意识到,流程中的任务与软件应用程序和数据库之间创建的数据流之间没有任何关系。因此,信息系统映射的大部分工作将是关于在任务和相应的数据流之间绘制关系。最终,你可能会发现其中一些流是不完整的,需要其他流来弥补数据不足。你可能会发现将过多数据带入甚至没有权限查看的软件中的流。你甚至可能会发现不再有已知用途的数据流。

要关联这两层,您将不得不创建一个强大的业务能力映射,因为第 2 层将用作流程及其软件实现之间的间接层。这就是为什么 BCM 如此重要的原因,如果您在信息系统中工作,您会意识到这通常缺失,仅仅是因为它不如流程和软件/硬件为人所知。然而,BCM 对于减少耦合并促进系统的演进至关重要,因为它提供了一种创建依赖关系的方法,而不会因为一旦设置就难以更改的技术实现而陷入困境,或者因为公司运营方式中的流程被修改而不得不更改 IT。在这种情况下,BCM 充当一个间接层,使得第 1 层的演进成为可能,同时对第 3 层的影响最小,反之亦然。

以下架构图总结了这一点,顺便说一下,我经常告诉我的学生,如果他们只记住我关于 IT 对齐课程中的一张幻灯片,那应该是这一张:

图 3.16 – 优秀的耦合

图 3.16 – 优秀的耦合

随时演进信息系统

当然,在映射现有信息系统状态之后,下一步是改进它,这需要有一个多步骤的战略来使其更加现实。再次强调,CIGREF 地图在这里提供帮助,通过清楚地说明每一步需要调整的内容,并展示如何处理依赖关系。如果通过将其插入到一个新的、改进的软件应用程序中来更改一个功能,所有依赖于这个功能的任务都需要进化以利用这一点,当然,除非有 100%的兼容性,在这种情况下,我们可以认为该功能本身没有改变。

这些步骤的最终目标是尽可能接近(且具有成本效益)一个现实且良好对齐的系统状态,这可以概括如下:

图 3.17 – 理想对齐

图 3.17 – 理想对齐

当然,这听起来可能很理想,而且,大多数时候,我们可能永远无法实现整个系统的对齐。但局部对齐是可以实现的,如以下图例所示,这是我从一个成功对齐其最重要的支持流程(即处理新员工,因为这是一家高结构性流动的公司)的客户那里导出的。通过这种方式,所需的努力可以减少到估计的十分之一,流程的持续时间将减少到最初测量时间的三分之一。这是通过自动化一些任务实现的,但正是对齐使得这种自动化成为可能,因为在此之前,流程完全是手工的,由于 IT 结构明显缺乏(该公司从事非技术业务,在 IT 上投入有限的预算,只有在达到几乎绝望的状态后,才意识到它对其活动的重要性)。以下图表因保密原因而模糊;我们旨在展示流程的流程:

图 3.18 – 应用对齐

图 3.18 – 应用对齐

我们将在下一章回到信息系统随时间演化的过程。现在,只需记住,CIGREF 地图必须用于建立实际情况,但也可以用于模拟一个期望的未来情况以及中间的每一步。

服务提供商的四层图方法

你可能会对在购买软件即服务时使用 CIGREF 地图的用途感到好奇,以及它是否意味着我们在硬件层不绘制任何内容。答案回到了你想要使用你的地图的方式,因为你不是出于拥有完整东西的乐趣而映射,而是因为你的用例需要你精确地了解你的信息系统在其细节中的工作方式。这意味着,如果你的兴趣在于软件匹配功能,你绝对不需要知道服务器在哪里物理上,在这种情况下,在第四层绘制任何内容都没有用;你只需将其留空。另一方面,假设你的一个担忧是关于你数据的位置性,因为你的董事会有一个关于数据主权的限制。在这种情况下,你将在第四层中指出支持软件的数据中心所在的精确区域。这样,就很容易发现任何位于允许区域之外的东西。

这回到了一个非常关键的忠告:只映射你真正需要的。很容易被带跑偏,映射很多实际上并不关心的事情。特别是,硬件层通常很容易映射,因为自动网络探索应用可以提供帮助,而且好的系统管理员通常在每个机器上部署了代理以进行安全和软件清单。因此,人们往往会有一个非常精确的第四层图,即使他们的问题是关于功能与软件的匹配。在这种情况下,你最好用几个通用块来替换整个硬件层——通常是使用的数据中心,包括你自己的服务器室——并用相关的成本标签它们,因为这(在这种情况下)是唯一能帮助你信息的信息。

谈到服务,可以从另一个角度提出一个等效的问题,即关于为软件编辑器或集成商提供软件服务的 CIGREF 表示。我们应该如何表示它?客户使用的云是否是编辑器信息系统的一部分?是否应该以特定的方式绘制?

再次强调,地图的有用性观念应该驱动我们对这一问题的响应。想象一下手头的问题——也就是您建立地图的原因——是因为您在内部功能和客户提供的功能之间有一个耦合问题。这可能是一个安全问题,因为一方面勒索软件可以轻易传播到另一方面。这也可能来自会计团队,他们不知道哪些机器和服务应该向客户收费,哪些成本应该保留在内部。也可能来自系统管理困难,例如关闭一个被认为是内部的服务器最终影响了您的生产。在这种情况下,正确的做法是绘制当前独特的信息系统,然后绘制目标地图,该地图由两个不同的信息系统和它们之间剩余的精确交互表示(例如,从生产信息系统向内部信息系统发送使用数据,以便会计可以为不同的租户建立账单)。

这通常是您在业务能力图(CIGREF 表示的第二层)的“外部面向功能”区域使用区域的地方。在生产信息系统中,您会在这个区域找到“按租户报告数据使用情况”或“按租户发送总 API 调用次数”的功能。而在内部信息系统中,您当然会在“业务”区域/“会计”区域找到处理这些数据并计算租户账单的功能。另一个例子是在“外部面向功能”区域可以找到的“请求租户访问阻止”或“存档租户”等功能。它们通常由内部信息系统调用,以指示生产信息系统客户未支付账单,至少应该被阻止,也许以后,完全移除。

两个系统之间的另一个链接例子,当然是当内部软件-生产工作流程产生了一个经过验证的、完整的、销售给客户的软件新版本时(这是软件编辑公司的主要角色)。必须存在某种链接,因为向客户租户展示此软件的信息系统将使用这个可交付成果在某个时候更新其服务。在保持非常低的耦合的同时建立这种链接的最好方法之一是创建一个容器注册库,该库将填充来自第一个系统(当然带有正确的标签)的镜像,并由第二个信息系统通过拉取它需要的镜像来消费,以在租户中公开。

唯一剩下的问题是注册表应该放在哪里,如果需要一个非常稳定的注册表,答案是在每一侧都放置一个:一个注册表在软件编辑公司的一侧集中管理所有持续集成的生产,另一个注册表作为图像缓存在另一侧。这使得集成公司作为持续部署的一部分更容易继续创建租户,即使第一个注册表不再可访问。这种清晰的分离甚至可以用来实现一些高级规则,例如“只有带有 STABLE 标签的容器图像应该投入生产”,通过仅在第二个 Docker 注册表中缓存这些图像。

有些人可能会争论说,由于在这种情况下两个系统之间存在调用,这可能意味着它们是一个单一的系统,应该这样表示。再次强调,地图的目的不是反映世界的全部现实,而是帮助你履行信息管理职责。如果你希望实现的是良好的关注点分离(出于安全考虑,应该是这样),那么你的地图应该反映你的目标,因为它将帮助你完成实现这一目标所需的一切。

最后,有人可能会反对这种观点,声称,如今,地球上每个信息系统都存在某种形式的连接,无论是通过互联网网络,它几乎覆盖了所有本地系统。此外,当公司收购其他公司时,它们会连接它们的信息系统,有时连接得如此紧密,以至于最终成为一个单一的系统。同样,这完全取决于你的策略,因此 CIGREF 地图应该简单地与愿景保持一致。

对齐的模式和反模式

几年的信息系统咨询使我观察到,大多数问题都与系统中的几个不匹配有关,而这些不匹配本身只属于几个模式。在有限业务领域工作了一段时间后,当我开始与农业合作社、化学风险评估公司、律师协会以及其他不同领域的公司合作时,我惊讶地发现,这些模式(或者更确切地说,反模式,因为它们会引起问题)无处不在。

法国 CNRS 的研究员 Dalila Tamzalit 与我合作,对这些反模式进行分类,并记录了一种寻找它们并利用信息以更好地对齐受其影响的信息系统的方法。这导致了 2021 年国际信息系统开发会议(可在aisel.aisnet.org/isd2014/proceedings2021/managingdevops/3/)上发表的一篇文章。你将在下一节中找到一些有助于管理业务对齐的信息摘要。

对齐的悲哀现实

首先,应该知道,大多数信息系统,如第一章中解释的那样,都存在一些基本问题,这些问题限制了它们的效率。从四层图的角度来看,这些问题可以总结如下:

图 3.19 – 常见问题

图 3.19 – 常见问题

由于很少有任何 BCM,因此过程可能被很好地理解,但相应的实现通常是通过点对点的临时互操作性完成的,这很快就会使系统看起来像“意大利面盘”,数据流以一种不受控制的方式发生。

我们可以追求的目标

这一点已经提到,听起来可能合乎逻辑,但我们的目标并不是一个完全对齐的系统。为两个业务流程设计的一个良好系统可能就像以下这样简单,其中超过十个调整良好的数据流实现了全部的业务需求,使用相同数量的应用程序(在这个例子中,大多数已经存在,并且只是正确地连接),几乎不需要额外的硬件:

图 3.20 – 良好的对齐(出于保密原因,文本已模糊)

图 3.20 – 良好的对齐(出于保密原因,文本已模糊)

对齐中的主要反模式

现在目标已经明确,让我们回到我们的对齐反模式,并介绍其中主要的四个(我们只解释它们是什么,以及如何对抗和减少将在本书的其余部分进行讨论):

  1. 纯粹的技术集成发生在过程没有在第一层设计,而是直接在软件中实现的情况下。结果是,公司战略、基于领域的规则或甚至过程的简单优化都会导致软件的改变。这是听到“我们进化不够快,因为我们被 IT 拖累了”或“由于软件限制,无法实现这个业务功能”(及其变体“在整个软件链中添加这个新的数据属性,从界面到报告,将需要六个月,并需要四个应用程序的新版本”)的根本原因。

    这个反模式的象征性表示如下:

图 3.21 – 反模式编号 1:纯粹的技术集成

图 3.21 – 反模式编号 1:纯粹的技术集成

  1. 应用孤岛出现在组织的两个部分在未相互沟通的情况下处理他们的 IT 需求时。结果系统在图中显示了这种结果,即两个独立的系统。可能有一些情况下,完全隔离被认为很重要(人力资源、财务、其他高度机密区域),但根据经验,总会有建立不同区域之间联系的时候。在这些情况下,这可能会成为残酷的现实,因为数据已经被完全复制,使用不同的格式,或者使用没有选择以简化互操作性的技术等。这种情况的主要风险是数据源简单地被向其他区域开放,这将导致重大的授权问题。在我所见过的最糟糕的案例中,一名实习生为了实施差旅费报销而将完整的 HR 数据提供给 ERP,这当然是一项重大的保密性违规,直到纠正之前,公司都面临着潜在的 GDPR 问题。

    这种反模式的符号表示如下:

图 3.22 – 反模式编号 2:孤岛

图 3.22 – 反模式编号 2:孤岛

  1. 单体是集中了大量功能的程序。这本身可能不是问题,因为一个特定的应用程序可能实现了业务领域所使用的所有内容。问题在于这些应用程序还实现了应该共享或已经在系统其他部分存在的功能。数据冗余是信息系统中的一个巨大问题,因为它们从未从一边对应到另一边,这使得很难知道哪个来源最接近真相,从而导致不良决策或错误的计算。

    这种反模式的符号表示如下:

图 3.23 – 反模式编号 3:单体

图 3.23 – 反模式编号 3:单体

  1. 功能多重实现是一个问题,因为不同的实现几乎不可能以兼容的方式工作。可以很容易地理解,如果一个财务预算总结在一个应用程序中以某种方式计算,而在第二个应用程序中以不同的方式计算,该应用程序应该做同样的事情,那么要采取明智的行动来管理公司是困难的。我在一家报纸公司见证的一个案例表明,根据查询的应用程序不同,读者数量会有所不同,差异如此之大,以至于在某些情况下,报纸公司没有额外的计算就无法知道他们是增加了还是减少了读者。

    这种反模式的符号表示如下:

图 3.24 – 反模式编号 4:功能多重实现

图 3.24 – 反模式编号 4:功能多重实现

一些其他反模式及其提出的分类

只展示了四个最重要的反模式,整个业务/IT 对齐反模式(BITA,其建议的简称)如下:

图 3.25 – 反模式分类

图 3.25 – 反模式分类

要使用这种分类来改进现有信息系统的对齐,每个反模式都附带一张结构化的身份证,包含以下信息:

图 3.26 – 反模式的身份证

图 3.26 – 反模式的身份证

反模式出现以及——更重要的是——纠正它们的标准操作的完整解释也可以在之前引用的完整文章中找到。

你可以在 GitHub 上查看完整文章,它将展示在第一个 BITA(业务/IT 对齐反模式,简称 BITA)上可用的此类信息的数量:github.com/PacktPublishing/Enterprise-Architecture-with-.NET/blob/main/Business-IT%20Alignment%20Anti-Patterns%20A%20Thought%20from%20an%20Empirical.pdf

这个来自经验的分类使我们提出了一个结构化的方法来改进信息对齐,这可以用以下 BPMN 图来总结:

图 3.27 – 在信息系统中检测反模式的方法

图 3.27 – 在信息系统中检测反模式的方法

我鼓励你尝试将这种方法应用到你的学习信息系统中,并反馈意见。这对学术界以及希望对其他读者也有帮助。

摘要

这相当长的章节展示了如何使用形式化的绘图技术来更好地理解信息系统,并记录它将遵循的演变。地图是控制特定地面的先决条件,而图式胜过千言万语,这使得这一步在任何信息系统架构活动中都是必须的。

由于 IT 是一个相当特定的环境,我们需要一种专门的方式来绘制信息系统的地图,这就是 CIGREF 地图的内容。它的四层布局有助于将面向业务的方面(流程和原子功能)与技术的方面(软件和硬件)分开。

这种表示信息系统的方式也有助于可视化其一致性,通过检查第三层(软件位)是否很好地调整到它所实现的第二层(业务能力图)。业务/IT 一致性是复杂信息系统质量的最重要指标和演变能力,这是一个必须追求的特征。

下一章将扩展当前章节,考虑时间的维度。我们已经在之前的章节中简要提到了这一点,解释说 CIGREF 地图可以用来记录信息系统当前的状态,也可以记录其应逐步达到的期望未来状态(大爆炸方法永远不是一种实用的替代方案)。但是,正如您将看到的,时间出现在信息系统的许多其他方面,并且可能是一个相当难以处理的参数。

第四章:处理时间和技术债务

信息系统就像一个活生生的有机体:它总是移动和变化。然而,大多数系统都是“一次性”设计的,没有考虑到它们在时间上的适应性,而只是考虑了它们在设计时的处理业务需求的能力。许多 IT 问题都可以与时间相关联。

在上一章中,我们讨论了业务对齐以及基于业务关注点构建结构的重要性。这也必须应用于将时间作为良好信息系统的方程参数:如果业务功能是一次性/可丢弃的,其技术实现将不会比原型更复杂,快速编码并在使用后很快被丢弃。另一方面,对于将在生产中使用数十年的功能,你必须仔细打磨设计并完善实现,尽可能减少移动部件,因为一个好的架构师知道维护这样一个模块最终将比其初始开发成本高得多(例如,参见natemcmaster.com/blog/2023/06/18/less-code/关于这个问题的讨论)。代码的质量及其易于维护(因此是开发者害怕的活动——文档)将比快速交付功能以适应市场时间的重要性更大。

本章将分析信息系统时间适应性这个问题,因为大多数系统都是基于固定时间目标设计的,很少考虑到时间的演变。这就是为什么它们在时间上性能迅速下降,也是为什么当它们的构建时间过长时,结果甚至不符合表达的需求,因此出现了敏捷软件开发。将解释技术债务的概念,以及耦合的概念。希望到本章结束时,你将提高对需要概念验证PoC)方法以及需要强大、可演进的、设计的批判性思维。

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

  • 功能变化对系统时间的影响

  • 敏捷方法旨在如何解决这个问题

  • 技术债务的概念

  • 信息系统的经验证蓝图方法

功能变化对系统时间的影响

在设计系统时,最难做到的一件事就是考虑时间。毕竟,要想象一个复杂事物在某一时刻的样子已经很困难了。考虑到时间需要额外的思考深度,这可能会使问题更加复杂。此外,随着时间的推移,系统的各个方面都会发生变化,但它也应该在功能运行时考虑到时间,就像另一个变量一样。

一些关于不当比较的有趣之处

下面是一些你可能在日常生活中听到的关于计算机行业以外的行业的句子:

  • 维修工这周末更换了我的汽车引擎;现在它又可以再使用 10 年了

  • 我用普通药片代替了商业品牌:它们更便宜,我没有注意到任何区别

  • 由于我们开始定期维护锅炉,我们在整个冬天都没有出现任何故障

  • 零件的尺寸略有变化,但我们只需在数控机床上更改参数;对于这样的小改动,不需要加工专家

现在,让我们尝试将其转移到 IT 行业,看看我们是否能在至少不露出一个苦笑或微笑的表情的情况下听到相同的表达:

  • 我们在本周末更换了 ERP 系统;周一早上一切似乎运行得相当顺利

  • 我使用了一些免费软件作为商业套件的替代品:更便宜,而且由于它与 100%兼容,一切工作都和以前一样

  • 由于我们定期维护我们的信息系统,我们从未遇到过任何重大故障或错误

  • 由于监管系统,业务人员需要调整系统,但由于这只是业务规则,他们不需要 IT 团队进行这样的小改动

这些句子听起来现实吗?如果你在信息系统方面有一点经验,你会知道它们并不现实,甚至听起来很幽默。没有什么比这些乌托邦式的句子更远离现实的了。相应的句子应该更像是以下这样:

  • 管理层已经决定更换核心 ERP 系统;我们预计信息系统将有一个至少 6 个月的稳定期,并且最初的包括分析、部署和培训的项目肯定至少需要一年时间。

  • 我转向开源软件以消除许可费用,但由于我必须调整大部分流程,我失去了一些功能,而且在这个技术领域专家很难找到,我不确定最终的总拥有成本是否会降低。

  • 由于新的网络安全合规性规则,我们让整个 IT 团队推送更新到信息系统中的所有软件应用;我们希望大多数服务器都得到了覆盖,但我们知道员工的工作站仍然存在高水平的风险。

  • “新的 GDPR 将迫使我们必须发布一个全新的软件版本,并调整信息系统中的大部分数据流;IT 部门在接下来的 6 个月里肯定将把大部分非维护时间花在这上面。

这些版本看起来更加真实,但听起来像是对形势的绝望评估。这样的信息系统的灾难性容量是从哪里来的?正如在第一章中解释的那样,信息系统尚未实现工业化。但如果你更仔细地注意这些句子,你会意识到它们都包含时间演化的概念。这正是它们听起来很愚蠢的原因。如果时间从等式中去除,它们可能看起来还不错:

  • 我们目前使用的 ERP 系统运行正常

  • 我在使用免费软件,它按预期工作

  • 我正在使用最新版本;目前一切似乎都很正常

  • 我们已经设置了软件的初始规则(并且我们希望未来不需要更改它们)

简而言之,IT 可以工作并提供优质的服务,但大多数时候,这是时间流逝和 IT 必须进化以解决问题的时候。

软件世界的后果

上述比较可能看起来像是轶事,但它们反映了一些现实,因为变化是生活中唯一不变的因素,因此在信息系统也是如此。我听说的一个有趣的故事说,一个完全稳定的信息系统是可能的,但需要三个组件:一个人,一台计算机和一条狗。计算机做工作,人喂狗,狗保护计算机免受人的触碰。

再次强调,尽管这个笑话带有幽默的成分,但其中确实有一些真理:完美的系统之所以被认为是完美的,是因为它是稳定的(狗阻止人造成变化,从而造成混乱)。计算机可以做得完美,因为它不需要改变它被编程去做的事情。抛开幽默,时间和进化对信息系统的影响可以正式描述,并且与它们的不同类别相关联,所有这些我们都会在这里描述:

  • 与信息技术中时间相关联的第一个概念——并且这个概念对于任何使用计算机的人来说都是众所周知的——就是软件升级的概念。随着时间的推移,无论在设计和发展过程中投入了多少质量努力,一款软件都必须经历定期的版本更新和至少安全补丁,以保持完全运行。一个软件应用本身就是一个复杂的系统,有时有成百万行代码。如果我们继续用机械工业来做比较(对于再次回到这个话题,我感到抱歉,这肯定来源于我在机械系统方面的学术背景),这意味着一个标准的行业级应用在复杂性上更接近商用飞机,而不是标准汽车。难怪它在其生命周期内需要升级和调整,就像客机需要重型维护一样。困难之处在于,大多数应用的设计方式并不像我们预期的那样模块化,意外的依赖性经常发生,使得应用作为一个统一的实体运行。如果你拿一款“拉法尔”战斗机来说,由于整个飞机都是按照这个限制来设计的,两个机械师可以在几小时内更换发动机。那么你的 ERP 软件呢?你能在几小时内切换授权引擎吗?当然不可能...这就是为什么大多数软件应用都有有限的预期寿命:经过多次版本升级后,整体质量总是下降,随着时间的推移,应用对业务的适应性也越来越差。当然,有些应用可以持续超过 10 年,有时甚至 20 年或更久。但如果你问用户,原因是不是因为软件完美无缺,他们是否喜欢它,你总是会得到同样的答案:这块软件之所以还在这里,仅仅是因为尝试移除它太过危险了!

  • 时间对信息系统的影响的第二种类型并非来自软件部分,而是由于商业本身。正如本章开头所述,信息系统是活跃的实体,并且由于商业本身的演变而持续发展。新的战略、法规变化、大型公司重组、与被收购公司的融合、出售业务单元等等——有如此多的因素可以影响信息系统的使用,以至于它们几乎不可能长时间保持稳定,即使在非常稳定的商业领域也是如此。此外,在法律相关法规之上,许多商业规则是特定于公司的,这使得难以开发出真正能够达到“适合所有情况”状态的应用程序。即使有最好的意图保持事情简单,公司往往最终会调整他们购买的软件应用,或者通过专用连接器或定制代码将它们集成,以符合他们的业务方式,这仅仅是因为这样做成本更低(至少最初是这样),而不是重新组织相应的功能。但这是一种陷阱,也是时间再次介入游戏的地方:随着时间的推移,这种特定性将变得越来越昂贵。首先,每个新的主要版本的应用程序可能会使其失败,并且需要花费金钱来保持特定代码与新版本兼容。大多数情况下,这并没有完全预算,这意味着随着时间的推移,整体成本会不断增长,有时最终会花费比最初调整流程到软件上更多的钱。其中也有一部分心理学因素:功能专家会因为外部编辑的一些代码认为另一种方式更好而感到不高兴去调整他们的工作方式。他们对自己的工作了解多少呢?

  • 信息系统中与时间相关的第三个联系既不在软件也不在功能上,而是在于软件如何适应业务功能。这可以通过集成、定制、调整应用程序参数、调整应用程序与其他软件部分交互的方式以及更多方式来实现。这里的联系稍微微妙一些,但所有这些方式基本上仍然是专家的工作。由于专家很少见,所以在软件项目的这个步骤中花费的时间往往比预期的要多。更改参数很快,但在一个复杂系统中分析所有可能的影响需要对其有很好的理解(我们在第三章中讨论了信息系统的地图需求)并且可能需要花费大量时间。这就是为什么 ERP 项目——一个众所周知的例子——在一家公司中花费了如此多的时间(尽管销售人员可能会告诉你关于它的所有事情,但在实践中,至少无法将其时间缩短到 6 个月以下)。这个后果的另一个结果是供应商锁定:随着越来越多的参数从默认值更改,随着越来越多的连接器或集成被添加到系统中,更改软件以适应其他供应商变得越来越困难。在一段时间后,应用程序已经深深嵌入到系统数据流中,定制新的应用程序将需要巨大的努力(尤其是由于文档不是这些项目的最佳资产),因此一些 IT 能力的发展停滞了。

所有这些后果都意味着使用信息系统的公司在某种程度上失去了其业务流程,因为 IT 以如此多的方式阻碍了发展,并可能阻止快速演变。当然,IT 有助于自动化流程,一旦实施,可以提供有趣的收益。但使其工作并保持其按时间工作所需的努力可能并不那么有趣(记住 Gartner 的统计数据,显示 70%的 IT 预算仅用于维护!)

最后,技术债务也是一个与时间流逝紧密相关的概念。实际上,它非常类似于熵,并且倾向于随着时间的推移而不断增长。但这个概念非常重要,所以我们将在本章稍后的单独部分对其进行分析。现在,我们将探讨敏捷实践如何帮助我们处理时间问题。

敏捷方法旨在解决时间问题的方法

敏捷与时间管理有很大关系,因此它可能有助于我们处理信息系统周围的时间问题。为了解释这一点,我们将回到敏捷是什么,然后观察它以解决我们需要驯服的时间复杂性的不同方式。

解释敏捷的隐喻

敏捷是关于考虑时间因素的。在 V 周期开发过程中,一切都被规划好,随着时间的推移,事情应该只会在流程中向前推进。敏捷方法认识到时间本身就是项目的一个因素,它无处不在:

  • 由于质量不容妥协,增加资源并不能让软件项目更快完成(“五个厨师不可能在 10 分钟内而不是 50 分钟内烤好一个蛋糕”),因此,调整风险的唯一方法就是增加时间或减少功能范围(如果客户仍然希望最初请求的完整范围在项目结束时实现,这又回到了增加时间的问题)。

  • 时间是组织敏捷项目时的一个主要决策:如果你使用敏捷工作,冲刺应该有多长?如果你使用看板方法,应该使用什么节奏?我们应该以多高的频率组织稳定冲刺?持续集成应该有多快才能有效?团队使用的时间节奏有多可持续?

  • 填充冲刺是对可用时间的谈判,以及如何估算待办事项所需的时间,并将其加起来以填充冲刺。

我找到的最好的比喻之一,用来向我的客户或学生解释敏捷软件开发,也谈到了很多关于时间的内容。这个想法是将两种射箭的方式进行比较:通常的方式是瞄准目标,仔细考虑风力和目标距离,当一切准备就绪时,射箭并希望不会突然刮起一阵风,我们估计的角度是正确的,等等。猜猜看?如果目标足够远,在这些条件下命中靶心几乎就是一个运气的问题。这就是 V 周期的内容:在时间上仔细规划项目开发,尽可能考虑初始条件,最终启动项目,希望一切不会偏离目标……遗憾的是,总会有外部条件的变化,客户改变主意,团队生病,重要的依赖项没有按时发布,等等。

以敏捷的方式射击每一枪都能命中靶心,或者至少有相当高的概率:你必须握住手中的箭,逆风走向靶心,如果靶心移动,就纠正你的路径,最终在你足够接近时将箭射入靶心。当然,手里拿着箭走向靶心比箭射出后的飞行时间要长得多。但你确定这会比在风中射出多支箭,最终有一支射中靶心,哪怕不是正中心吗?区别在于项目的条件。如果一切都很稳定,没有外部依赖,并且你处于完全受控的环境中,那么提前规划一切可能比逐步调整要快一些。然而,绝大多数软件项目并不属于这种乌托邦式的情境。大多数项目都是在极其变化的环境中开发的,到处都是危险。

回到涌现架构的概念

第三章中,我迅速提到了涌现代码架构,并说我将回过头来讨论这个话题。现在正是时候。既然我们讨论了敏捷开发,而且我们正处于关于时间的讨论中,让我们看看与涌现架构密切相关的东西。这个概念是关于在没有提前瞄准架构、没有模式和计划的情况下实现良好的架构,通过在软件项目的发展过程中细化架构,并在迭代开发的每一步中重构代码结构。没有提前瞄准……这让你想起了什么?这是我们之前用来解释敏捷方法达到软件项目目标的隐喻。再次强调,时间是允许我们在架构(其意义为提前结构化)和在工作之前无法了解复杂业务领域的不可能性之间达成协议的概念。这种对立及其解决方法如此重要,以至于需要一个专门的章节。

架构与敏捷方法之间的明显对立

十年前,当我开始理解敏捷软件的原则并将它们应用到我所带领的技术团队中时,我很难理解为什么一个典型的 Scrum 团队会包括开发者、测试员、产品负责人和 Scrum 大师。为什么没有架构师呢?因为那时我的名片上写着这个头衔,所以我对此感到个人受到了打击。这有点令人不安,因为在同一时间,我意识到敏捷与那时我们使用的老方法相比具有巨大的价值。

在与许多将这一概念带到法国的敏捷领导者讨论之后,我最终在 2013 年就如何将架构与敏捷方法结合起来进行了专题讲座(法语版本:www.infoq.com/fr/presentations/concilier-architecture-et-agilite/)。在揭示了众多矛盾以及“象牙塔”建筑师在短迭代中会遇到的困难之后,我最终解释了一种可能的折衷方法,即如何协调“提前看到”和“在短迭代中行动并调整愿景”的效用。像大多数模式一样,这些模式不是被发明的,而是由许多人独立发现的,这个新兴架构的概念仅仅是任何试图消除之前所述矛盾的任何工作的结果。

再次强调,时间在这里是伟大的方程式解算器:如果你将架构的时间范围设定为只有几个迭代,那么架构和短迭代并不对立。这样,目标移动很大的可能性会大大降低,架构仍然是有用的,因为它有助于结构化这些少数迭代的开发。

这解决了建筑师面临的难题,因为他们的工作仍然是必要的,即使考虑到工作是要提前思考长远,也会有很大的变化。但话又说回来,即使在敏捷方法出现之前,那些在象牙塔中的建筑师(在没有掌握现实的情况下想象很长时间,并向团队提供计划……他们不会遵循的计划)在很大程度上被视为没有意义。

它还帮助我们理解新兴架构的概念,该概念指出,如果在每个冲刺结束时正确地进行重构,代码的最终结构将完全适合功能需求……就像完美的建筑愿景(在我们的比喻中,箭头在目标中心的远射)在纯理论中会做到的那样(但除了非常小的项目外,在现实中几乎不可能做到)。

除了时间之外,语义学也有助于消除之前暴露的矛盾。架构这个词被用于两种不同的方式:

  • 建筑作为项目新兴的全球形态,是关于团队产生的代码结构

  • 建筑作为在应用程序中(甚至更高,在整个信息系统)构想结构的行为,是关于通过最初思考和行动在系统上尝试达到这种结构化状态

这意味着这种理论上的矛盾可以被克服。但这并不意味着没有实际影响,我会向你展示一个例子,因为它将帮助我们回到将技术方面与业务方面对齐的观念。但在那之前,我将添加一个外部分析。

著名建筑师的地位

就像任何科学学科一样,我们软件架构师通过“站在巨人的肩膀上”节省了很多时间,在我们的情况下,这涉及到反思该主题真正专家建立的艺术水平。马丁·福勒无疑是软件架构领域最好的参考资料之一。关于“黑客、编码和修复”与“前期大设计”之间的对立问题,我强烈推荐阅读马丁·福勒在www.martinfowler.com/articles/designDead.html上发表的优秀文章。标题设计已死?虽然挑衅性强,但隐藏的真正背景主题正是我们在这里讨论的。

马丁·福勒对架构的反对意见的回应仅仅是只应用设计来增加系统演变的能力。像往常一样,在两个极端之间没有“对或错”的答案,即极限编程(它明确承认其极端性)和前期大设计(它通常不承认甚至不承认其极端性,并迅速产生“象牙塔架构师”)。

这就是架构师的工作变成一门艺术的地方,因为他们需要运用良好的技能,在不强加不可移动的限制的同时,通过一些前期设计获得微妙的平衡,但仍提供真正有助于开发者长期快速生产功能的健康指导(而不是像马丁·福勒文章中描述的那样,被软件熵所阻碍)。

由于,在软件开发中,变化是唯一的不变因素,因此提前编写因功能变化而可能过时的内容是没有用的,但这并不意味着架构师的工作被取消了:相反,这是关于简化未来的演变。架构不是关于 UML 或代码框架,而是关于系统应该如何构建的指导方针:它的固定点是什么,它围绕哪些关节转动;我们应该在哪里关注质量,在哪里可以承担可丢弃的代码,因为业务发展如此迅速,投资于稳定性是没有意义的?有时,大量的架构努力正是为了适应一个需要频繁变更的重要模块。例如,使用业务规则管理系统(我们将在第五章中更详细地讨论这一点)。

同样的情况也适用于编码模式:仅仅因为代码能够正确运行,并且持续不断地重构你的代码,自然会形成代码中的模式,即使你事先并不知道这些模式(我告诉你了,工艺并没有消亡!)。如果你还没有在编码活动中亲身体验过这一点,那么一个很好的证据是,当你阅读大量的代码(尽管很少有人喜欢这样做,尽管大多数伟大的作家在成为作家之前都是饥渴的读者)或者当你跟随一群学生时,你会意识到这些模式经常被重新发现。这正是模式的定义,因为它们是普遍的,无论它们是如何被发现的,只要你正确地组织你的代码,上下文就会让你最终使用正确的模式来解决这个具体问题。

提前思考函数合约

在之前,我谈到了一个实际例子来阐述我们之前讨论过的长期架构和新兴架构之间的对立,让专家(马丁·福勒)发言。我提到的例子来自我的个人专业经验,是为实现功能流程的几个应用程序之间的复杂互操作场景而创建的图表。在项目开始时分析业务需求(或者更确切地说,是它们的初始表达,因为它们随着项目的发展而变化),我创建了以下流图:

图 4.1 – 数据流示例

图 4.1 – 数据流示例

根据第三章,你应该通常能够认识到函数和软件实现之间的双重性,特别是关于依赖关系应该指向第 2 层而不是直接指向第 3 层的基本建议,这将导致点对点互操作,从而创建软件解决方案的“硬耦合”(如果你不知道这个表达式的意思,我们很快就会回到这个话题)。

即使使用机器符号,底层实际上还是关于软件服务器,因此是第 3 层。中间区域也是软件层的一部分,因为它包含将 API 调用转换为专有调用的连接器,如果需要的话。顶层的编排层显示了由 API 合约表达的功能任务是如何组合起来创建细粒度过程的。它基本上可以被认为是 CIGREF 图中的第 2 层,带有一些第 1 层的触感。

我展示这个架构的原因有两个。首先,它说明了当架构走得太远时会发生什么,以及新兴架构如何有助于节省时间:我是在我还是一个年轻的建筑师时画的,当时并不完全了解新兴架构的概念,所以它走得太远了。当然,有一个项目愿景是有帮助的,但最终,几乎所有的连接器和数据流都没有按照我预想的方式工作,时间在这里被浪费了。

其次,这个架构提供了关于 CIGREF 地图第 2 层和第 3 层之间交互的更多细节,表明它们是关于 API 合同(我们不是在谈论带有代码的 API 实现,而是在谈论合同,即用精确的技术术语表达的功能能力的列表)。这尤其有趣,因为尽管技术实现并不是(完全)预期的,而且自从原始蓝图以来,流编排已经改变了很多次,但结果却是 API 合同在 很多年里都没有变化

我无法告诉你,在项目几年后意识到这一点给我带来了多大的满足感。当思考代码时,我的最初愿景是失败的,其中只有一小部分得到了实施。当思考编排时,由于流程和业务规则的变化,API 的粘合方式已经改变。但与业务知识人士设计的 API 合同在几年后仍然存在,基本上没有变化,并且它们与业务的极端一致性使得所有这些变化在代码或定制中产生了非常有限的影响。

简而言之,代码架构应该限制在几次迭代之内。但业务一致性架构是一项值得在项目初期就进行的投资,因为它的价值不会衰减。

这就是为什么先合同后 API 设计(再次强调,我说的不是 API 实现,而是仅仅定义合同)如此重要的原因:可以通过纯粹的功能知识建立合同,将每个技术方面都排除在外,从而确保定义业务模块及其依赖关系的基础非常稳定,这将作为软件实现后续的强大基础。

我们将在第八章和第九章回到“先合同后思考”这一概念,但在此,请记住时间范围对架构在两个意义上都有影响。对于技术架构,时间范围应该是有限的,以便“箭头能够达到目标。”但对于功能架构,时间范围没有限制,因为在这种情况下,我们所做的是了解目标在哪里。这是即使仅仅想到要击中它也是必须满足的前提!

技术债务的概念

技术债务可能是过去十年与 IT 管理最相关的讨论概念之一。作为本书的基本目标之一,软件质量是我们必须清楚地描述的内容。

技术债务的一般定义

如果你正在阅读这本书,你肯定对软件质量和如何正确构建事物感兴趣,因此你肯定对技术债务的概念有很好的理解,或者至少已经接触过它。尽管如此,我仍会给出一个快速的定义,以便你可以尝试将其形式化。

技术债务是什么?

技术债务是你允许进入项目的意外复杂性及其内在复杂性的增加。

让我们稍微分解一下。当你开发一个软件项目时,它总是旨在在给定的业务领域范围内产生一个功能。有一个内在的、确定的、稳定的复杂性,来自于你试图解决的领域:在信封上打印地址比优化国际机场的航班要简单得多。

或者不是吗?作为一个旁注,请务必始终确切地知道我们在谈论什么,当业务需求被表达出来时,如果你不确定,不要犹豫,提出关于可以做什么和将要做什么的保留意见。用一句话简单地解释功能需求并不一定意味着目标本身很简单,而且可能隐藏着很多内容。在这个例子中,你应该立即产生一种反射,询问地址应该如何打印,如果支持不同的信封格式,国际地址是什么,如果有一些规范和标准需要遵守,数据将如何提供,等等...

这种第一种复杂性通常被称为内在复杂性,因为它伴随着你想要解决的功能业务领域,而且无法逃避。除非你做得少于客户期望,否则你无法减少这种复杂性。这并不意味着你应该立即接受所有这些复杂性:记住敏捷方法将项目切割成小块,一次处理一块(“你是怎么吃大象的?一口一口吃”)。如果你的客户希望你处理完整的业务领域功能和复杂性,你只需添加所需数量的冲刺以达到所需的内在复杂性水平。这只会花费更长的时间,因此成本更高。

现在,让我们来谈谈第二种复杂性:意外复杂性。为此,让我们以第一个表达的功能需求为例,即打印信封上的地址。为了使内容简短,假设我们只需要在标准 A5 格式信封上打印四行标准地址,地址数据以我们想要的任何格式提供,并且硬件部分(用于信封的特殊打印机)由其他人负责。作为开发者,我们如何实现这个请求的功能?

想到的最简单的方法之一是使用 Office Word 应用程序的融合功能,从集成助手消耗 XML 数据,并保存文件供客户未来使用。

但实现一个函数的方法(方式!)不止一种,你可能会发现自己正在使用一个从头开始创建的 Java 应用程序来读取任何格式的地址、创建 PDF 文档并将其发送到打印机。这并不复杂,但已经有比第一个技术解决方案更多的移动部件...而且它们的维护是你的责任,而不是办公室编辑的责任!你将不得不处理 Java 运行时的版本兼容性问题。PDF 生成也可能有点棘手。也许开发者会在代码中留下一些 TODO,表明一些边缘情况需要在将来解决。最终,尽管不是极其复杂,但这个解决方案在技术复杂度上比我们最初提出的方案要复杂,尽管它在功能上达到了相同的目标。

我们所说的 delta 就是所谓的偶然复杂性,因为这是可以避免的复杂性——与功能性复杂性相反。有时人们会将它与技术复杂性混淆,但这并不是正确的说法。实现一个函数总是需要某种技术复杂性:具体的执行不能从无中来,并且必须有一些软件来执行函数。偶然复杂性,正如其名称所暗示的,是建立在实现功能性需求所需的最小必要努力之上的技术复杂度。因此,它被视为一个意外,因为事情本可以不这样做,而且它之所以存在,是因为外部的不愿原因。

技术债务的原因及其与时间的关系

这些不想要的原因是什么?嗯,它们如此之多,以至于很难一一列举:懒惰、没有足够的时间达到适当的质量、缺乏培训、我们都有的倾向,即使用一个众所周知的技术而不是一个更适合但需要首先学习的技术(“当你只有锤子时,每个问题看起来都像钉子”)、缺乏技术监控,结果是我们根本不知道还有更好的做事方式,还有更多。

在这些原因之上,还有一个更深层次的原因,即大多数技术专家实际上在内心深处热爱复杂性。我经常将开发者比作气体(没有恶意:我自己也是,几十年来都是,而且仍然会陷入这个陷阱):他们总是会占据你给他们的所有空间。

这与热力学有另一个相似之处,因为我正在将熵作为 技术债务 的隐喻。

让我给你一个(并非完全不切实际)的例子。组建一个由软件专家组成的百万美元团队,要求他们创建一个计算两个整数之和的函数,你几乎可以肯定,没有人会提议简单地使用Int32.Add。他们会在假设你知道自己在做什么的情况下工作:既然你组建了如此庞大的团队和预算,你肯定有更高的目标,即创建一个高性能的函数,以几乎无限制的大小添加整数,在所有条件下都能产生可预测的结果。

这是因为开发者是工程师,很少是商人。如果他们是,团队中第一个联系的人会告诉你,你不需要其他雇佣人员,他们会自己承担整个工作,只需要五十万美元。接下来,他们会组装一个复杂的机器,它只是调用Int32.Add,让你等待几个月来隐藏它的极端简单性,然后之后将最终产品交付给你。

一个关键发现是始终给你的开发团队提供一些边界约束;否则,他们可能会添加意外的复杂性,有时甚至数量很大...而且总是很痛苦地知道你最好的客户业务流程被一个添加的“以防万一我们将来需要它”的过度热情的开发者引入的函数中的错误所阻塞。

技术债务的第一个原因对开发者来说非常关键,但等等——我们还得谈谈功能人员!

解释精确需求时的懒惰?与开发者的沟通不足?过度依赖他们来理解技术复杂性(之前,我谈到了一句业务需求的危险:它往往隐藏了请求者本身对需求的理解不足)?无法测试结果并调整功能需求?这个清单可以继续下去,还可以挑剔我们心爱的产品负责人。并不是因为他们自己不是技术人员,功能人员就不能造成意外的复杂性!

所有这些关于应该做什么的功能定义的变化,对技术实现产生了巨大的影响:我们改变代码的方式,就像更换汽车上的轮胎一样!函数之间存在联系,代码的整体复杂性在几行之后就会超出人类大脑的处理能力。因此,如果指示总是变化,结果无疑将是质量低劣的代码,为了节省时间而牺牲质量,充满了“临时”的解决方案(谁在骗谁?我们都知道它们会一直存在,直到应用程序的生命周期结束),为假设的同事留下TODO指示,以便神奇地出现并重构那些愚蠢的不完整代码成为美妙优雅的新版本,等等。我甚至没有提到那些将永远膨胀应用程序的无效代码,仅仅因为复杂性——以及缺乏文档——使得删除它并产生副作用的风险如此之大...

技术债务与时间的关系

为什么,为什么,为什么今天的软件行业中会有这么多垃圾代码?嗯...又是时间。技术债务是另一个与时间密切相关概念。尝试对上述症状进行根本原因分析,经过几个连续的“为什么”,你几乎总是会得到同一个答案:“时间不足。”缺乏初级培训?我们没有时间。缺乏产品所有者的可用性?他们没有时间。缺乏文档?我们没有时间...

时间以另一种方式出现在技术债务中:如前所述,技术债务,就像熵一样,总是增长的。而且,就像熵一样,可能会有一些特殊的地方,局部上的无序正在减少,但这总是通过消耗能量并在其他地方增加无序来实现的,这使得整个系统的熵增长。

技术债务是为什么在超过十年后仍然可以正常运行的应用软件如此之少的原因。旁观者可能会认为这是因为软件是一个快速变化的领域,但当你仔细想想,变化并不那么快。Java 是 90 年代的事情,.NET 在十年后出现,2010 年代见证了 JavaScript 在它本不应被用于的地方的使用,2020 年代标志着尝试一些新语言,但没有一种语言现在标志着它的时代——变化的速度并不那么快...那么,我们为什么改变软件这么快呢?简单地说,是因为它们充满了技术债务,我们不能再以高昂的成本来维护它们了!

这又是一个技术债务暴露时间的例子:随着时间的推移和技术债务的增长,它对项目造成的开发速度减慢的时间成本也在增加。这就是为什么我们称之为技术债务:就像金融债务一样,只要你还保留一些借来的资本,就必须支付利息。借入的资本越高(你的技术债务深度,这与你在开发过程中多次走捷径的次数有关),利息就越高(向你的应用程序添加功能所需额外的时间)。

既然我们在谈论债务水平与消耗软件开发时间的线性关系,这意味着存在一个比率,就像在金融贷款中一样。现在是一个分析这个比率更详细的好时机。

债务或高利贷?

我们中的许多人至少在一生中贷过款,用来买房子。与我们可以从中获得的优点相比,支付所获得金额的一小部分是合理的:贷款结束后拥有房子,不再支付租金,等等。根据经济环境(以及个人偏好在选择中也起着很大作用),可能有些情况下租房而不是买房更好,但从长远来看,积累一些资本总是胜出的。

然而,这只有在利率足够小的情况下才成立!如果你必须以每年 10%、20%甚至 50%的利率借钱,你会感到多么不舒服?在这种情况下,当然,没有人会借钱,因为 2 年后,贷款的成本就会和本金一样多:在这种情况下,你最好推迟 2 年购买,用现金支付。

除了有些情况下你不能这样做。当然,你可以租房子住而不是买房子。但是,当你需要钱吃饭或因为生活中发生的困难事件需要临时住所时怎么办?如果没有监管,银行可以随心所欲地提高利率,在某些情况下,你可能会被迫贷款,因为你的生活依赖于它。在这种情况下,当你情况改善时,你将不得不偿还这笔贷款,但利率如此之高,以至于它会吞噬你所有的储蓄,你最终会陷入另一个贷款的恶性循环。这是为了避免几个世纪以来,政府和甚至进行金融操作的个体行为者通过所谓的高利贷利率受到限制的情况。

如果你不了解这个金融术语,高利贷指的是以如此高的利率贷款,以至于实际上无法偿还本金。社会进步是为什么现在在大多数国家都是非法的,那里的最高利率是固定的。例如,在撰写本文时,法国的高利贷利率为 5.33%,这意味着银行不允许以高于这个价值的利率贷款。

现在,让我们回到技术债务,并评估我们借款的比率。这并不难找到,因为 Gartner 关于信息系统维护成本的研究已经被引用过两次:令人震惊的是,占 IT 预算的 70%!好吧,这并不包括技术债务的 70%利率,因为你还应该计算 IT 系统为公司带来的好处以及不这样做带来的成本。我将让你自己计算,因为这会根据你所在组织的具体情况而有所不同。但总的来说,你可能会得到一个数字,这个数字你无论如何都不会容忍从银行获得的金融贷款,而且这个数字将远远高于高利贷利率。

那么,我们为什么要容忍这种情况呢?这种情况的原因已经在前文中提到;现在是时候提出一些解决方案,以消除过度的技术债务。这就是我们在下一节将要做的。

技术债务的平衡方法

人们经常谈论与技术债务作斗争或压制它。这种说法并不准确,因为它意味着技术债务(以及因此产生的意外复杂性)应该降至零。这听起来像是一个很难实现的目标,因为完美需要花费很多钱:事实上,你的目标不应该是不再有任何技术债务,而应该是控制它,就像你不应该试图找到一个 0%的利率贷款(你永远找不到)而是找到一个合适的利率,这样可以让你的项目更具成本效益,平衡利率、贷款金额和期限等等。

因此,一点技术债务是可以接受的。如果你必须按时推出这个功能,以便在年度研讨会上向所有客户展示,谁会在意你当时没有为这次活动设置日志记录呢?真正重要的是,你已经将工单放入开发工具中,并且产品负责人同意在将功能投入生产之前,在即将到来的冲刺中完成它。如果他们违背承诺,试图推迟这个“技术”工单,并对它大吵大闹,提醒他们后果,发送一封电子邮件解释这将如何影响未来,要求他们书面同意承担责任,将其升级给大老板……无论需要做什么,都要确保这个功能回到正轨!否则,技术债务开始增长的责任将是你。

当然,最好的方法始终是简单地不让技术债务溜走。当然,说起来可能比做起来容易,但知道问题可能如何产生已经是一个很大的进步。记住,“技术债务”这个概念在 2000 年代并不为人所知或正式化;现在,甚至非技术经理在 IT 或软件开发领域工作也可能听说过它。这已经是一个很大的进步,并让你能够向他们做出有根据的论点,解释减少延迟、缺乏培训或文档和质量的时机将导致几个月后开发缓慢。再次强调,如果你是技术负责人或 CTO,控制技术债务是你的首要且最重要的职责之一。

但您可能处于一个软件应用程序的技术债务已经很高的境地——要么是因为您过去让它滑落了(真傻啊),要么是因为您负责的软件本身就已经处于糟糕的状态。首先,要清楚地表明——如果利益相关者并不完全了解——情况很糟糕:您不会想到糟糕的软件团队可以编造多少借口来掩盖他们无法交付的能力,而且您确实需要有能力改善这种情况。如果您接受了这份工作但并未迅速发出关于不稳定状况的警告,那么每个人都会认为软件没问题。而且您以后也无法对其状态发出警告,因为您是一个技术专家,逻辑上每个人都会认为您应该在此之前就看到它,尤其是如果它像您描述的那样一团糟。您甚至可能会发现一些不负责任的先前所有者发誓说,在将软件转交给您的团队之前,软件完全没问题!

在这种情况下,建立应用程序中技术债务的地图(使用 CIGREF 四层图,不要忘记技术债务甚至可能来自设计不良的过程或定义不明确的功能,以及不正确的治理)。可能有些地方一点技术债务是可以接受的。而有些地方则大部分维护时间和预算都被消耗,必须紧急处理。当评估纠正这些风险时,许多参与最初混乱的人会告诉您,影响将会非常高,试图纠正软件将不会奏效,甚至可能已经尝试过并失败了。在这种情况下,做出您最好的估计,并要求经理们做出决定并承担责任。当宣布重写相关功能将花费 10 万美元,并带来 20%的影响风险,额外增加 20 万美元时,在座的每个人肯定会皱眉...但如果您也解释说,这款软件在过去十年中每年已为公司造成 4 万美元的损失,因此已经浪费了 40 万美元,决策者会迅速进行计算并让您尝试。

这意味着您可能通过消除技术债务(在金融贷款的比喻中)来偿还部分资本,即使向经理们解释这种做法的好处通常很困难。毕竟,业务影响不是立即可见的,也没有客户抱怨软件不工作。所以,再次强调,您真的必须通过评估技术债务给您带来的时间成本,同时可以为客户的愉悦完成哪些功能,以及偿还债务并使应用程序达到良好质量水平所需的时间来构建一个强有力的论点,同时不要忘记影响分析——在“汽车行驶时更换引擎”总是存在风险。

大爆炸的诱惑

大爆炸方法怎么样?你知道的——把所有东西都扔掉,重新启动一个全新的、干净的产品。所有工程师的梦想... 如果你仔细想想,这并不是正确的做法。如果你无法阻止它发生,那么这是最好的结果。 让我来解释一下:大爆炸方法,尽管它可能很有吸引力,但永远不是正确的选择。如果你的软件中存在技术债务问题,那是因为你的开发过程是错误的。所以,如果你开始另一个应用程序,希望它比前一个更好,但又不修复这个过程,你只会浪费几年时间,最终达到相同的状态。如果你知道过程中哪里出了问题,并且已经纠正了它,应用程序将会改进,所以再扔掉它也没有用了。

这是否意味着大爆炸永远不会发生?不,当然不是。即使这不是一个好主意,人们仍然会尝试去做... 并失败。但是一张干净的纸是如此吸引人,以至于即使在其他方面营销能力较差,应用程序所有者也会不遗余力地争取利益相关者的同意和预算。他们会通过承诺提高性能、提供更好的上市时间、未来功能的易于改进以及许多其他质量来做到这一点,以至于参与者最终会 wonder 为什么之前没有提出这个建议。而且,他们还是会失败。这不是我说的话,而是从这些类型的项目中拥有经验的人都会找到的经验之谈。

在任何规则中都有一些例外,确实有一些大爆炸项目是成功的。我注意到在那些大爆炸并非团队意图而是通过旧项目因自身重量而崩溃的事实中。在我居住的法国,我们有很多这样的大型政府项目,失败如此严重,以至于从项目中没有任何东西可以挽救,新的软件公司不得不从头开始。例如,“卢沃伊”项目(管理士兵的薪水),该项目浪费了数百万欧元。回到我之前提到的这些事件中的共同责任,这个项目中技术问题很多,但功能特性描述严重不足,几乎没有缩减项目规模,这导致了这场工业灾难。

耦合的不同类型

应用程序之间的耦合是我们将要讨论的关于时间的最后一个概念... 就像技术债务是必须存在的东西(技术复杂性,以实现功能复杂性)的略微过多一样,耦合是指从功能角度来看比所需更强的依赖关系。

让我们考虑一个例子:你想要向你的客户之一发送一份带有电子签名的合同。当然,合同模块将依赖于电子签名应用程序,以及提供你所需要关于该客户信息的模块(即他们的财务或法律联系电子邮件地址)。但是,有依赖关系,还有依赖关系...

假设第一个依赖关系设计得非常好,你只需通过在信息系统调用一个 API 来发送一份合同进行签名,然后它会处理所有事情。你甚至不需要知道哪家公司将实际完成这项工作,也不需要知道这如何具有法律约束力:你只需调用 API,如果它返回 OK(技术上称为HTTP 200),你就没问题了。这种依赖关系是一种低耦合的依赖关系:实现中可能会有变化,你的公司可能更倾向于选择另一个电子签名供应商,或者根据调用 API 的人将文档以不同的方式路由进行签署:你不必关心,因为你只需使用POST命令调用类似mycompany.com/document-sign这样的东西。这就是你所知道的一切;幕后发生的一切都不关你的事。你仍然依赖于函数的完成来签署你的合同,但这种依赖性非常灵活,你可能永远不需要改变调用函数的方式;你所承受的耦合度很低。

现在,让我们来看第二个依赖关系,并想象一下另一端的情况:你需要获取客户的财务或法律联系人的电子邮件地址,为此,你必须知道客户 ID。遗憾的是,这并不是你在服务内部使用的同一标识。因此,首先,你必须调用负责客户参考的服务来了解确切的使用标识。当你有了这些信息后,你必须深入到一个通过其 IP 号共享的文件夹中,并沿着文件夹结构向下走,从消费者被记录的年份开始(看起来像是你得到的标识符的前两个数字,但你并不确定),然后是一个名为Contact的文件夹,最后是一个包含联系类型的文件夹,即FIN代表财务和JUR代表法律。在那里,你最终会找到一个 Word 文档,你必须跳过一些无用的信息,直到你最终到达第 2 页,在那里你找到了你一直在寻找的电子邮件地址。

这听起来有些牵强,但这是我咨询生涯中遇到的一个真实世界的例子(诚然,尽管如此,这是我 15 年来在客户系统工作中见过的最差的信息系统之一)。而且我们还没有完成!一些客户有多个标识符;当他们在数据库中删除并重新输入时,他们的标识符被恢复了...但他们的数据在对应续签年份的文件夹中,而不是初始创建年份的文件夹中。在某个时候,联系文件夹被重命名为CONTACTS,这破坏了信息恢复自动化的几次尝试,并且联系类型的代码也发生了变化。最后,文件夹中的 Word 文档格式发生了变化,电子邮件地址不再位于同一位置,让人们怀疑新位置是否包含正确的数据,或者是否是关于新的电子邮件信息。所有这些无用的复杂性使得对电子邮件地址的依赖变得非常紧密(这又是我所见过的最糟糕的例子)。

当然,低耦合通常更好,但就像技术债务一样,有一些耦合是可以接受的,重要的是要控制它。可能有些地方,极端紧密的耦合并不是一个令人担忧的问题。例如,紧密耦合到一个预算结构通常不是什么大问题,因为这些结构是监管强加的,并且它们的变化频率以十年计算。所以,在这种情况下,你必须彻底审查你的流程和软件应用,这并不是什么大问题。另一方面,如果你使用的是供应商的商业依赖,而该供应商在你意识到你用他们的工具做了很多业务时提高他们的许可价格(你肯定知道这样的编辑器),你将希望有低耦合。在这种情况下,向你的 CEO/CFO 展示你通过一些参数更改和一个小型的迁移过程就能切换供应商,这将使你成为他们喜爱的 CTO 合作伙伴,因为他们将带着极其有力的论据和一条容易逃生的后路回到与供应商的谈判中。

最后,耦合也与时间有关。有很多种耦合方式,但有一种耦合是按时间顺序衡量的。如果你要在模块 A 中执行任务,需要来自模块 B 的信息,那么这种依赖是同步的(你肯定会发现它通过同步调用实现,例如 HTTP GET调用)。如果你的模块 A 在调用模块 B 中的函数后可以自由继续,那么这种依赖是异步的(你肯定会看到它,例如,一个返回回调 URL 的POST API,你可以在之后调用它来查看工作是否完成——或者更好的是,你可以注册一个 webhook,一旦工作完成就会通知你;这将发送另一个你可以联系以获取外部任务结果的 URL)。在第十七章中,我们将回到这种方法,并特别解释编排和协奏之间的区别以及何时使用每种方法——就像往常一样,没有对错之分,正确的技术取决于确切的功能需求和其上下文。理想情况下,在良好的信息系统中,两种方法都应使用,每种方法在其首选解决方案的上下文中使用。

到目前为止,你应该已经清楚地理解了技术债务及其对信息系统演变的影响。随着时间的推移和技术债务的累积,IT 组件在演变和有时仅仅是功能上会因技术债务而减速。我们能做些什么呢?嗯,这正是下一节的主题。

信息系统的经验证蓝图方法

在本节的最后,我想解释一个方法,我过去几年中一直在使用它来创建信息系统演变的蓝图,并与几家工业客户一起完善了它。里面没有什么特别之处,也没有特别创新,因为这仅仅是将常识应用于达到功能目标...但它足够正式,以至于我可以想象你在描述所使用的步骤时能找到价值。

完整的方法相当复杂,需要一本专门的书籍来详细说明,所以我将专注于本章的精确主题,即处理时间和技术债务。我将使用的例子将涉及从一个充满技术债务的单体软件应用中提取依赖项,但遗憾的是,它被用作客户公司业务的基石(是的,这是一个最坏的情况)。为了在限制对日常业务的影响的同时提取这个依赖项,不得不创建一个多年的计划蓝图。以下章节将解释如何完成这项工作,重点在于方法而不是具体行动,以维护客户的机密性。

所有这一切都始于...映射!

第三章所述,我们必须从对问题的正式映射开始,CIGREF 地图建立在对研究边界的周围。由于问题在功能和软件层,过程完全没有表示,关于硬件基础设施的第四层只是略过,因为我们只需要相关机器的全球成本。结果是以下结构,其中你可以看到软件层中左上角的大白方块(这是我们研究的单体主题):

图 4.2 – 地图中精确度的演变

图 4.2 – 地图中精确度的演变

这是一件非常重要的事情,我想再次强调:只有与学习相关的部分被绘制在地图上。还记得上一章中的这个图表吗?

图 4.3 – 具有减少内容的真实世界地图示例

图 4.3 – 具有减少内容的真实世界地图示例

这是因为,在 20 个左右软件应用中,它只是该公司整个信息系统的一小部分。至于业务能力地图,它更为详尽,但这仅仅是因为我们需要整个边界线用于另一个项目。正如你所见,只有少数这些功能与正在研究的软件应用相关(沿着层 2 和层 3 之间的线条)。

回到巨人的肩膀上,我将简单地回顾一下马丁·福勒关于类所提出的观点,这完全适用于函数和软件应用映射:马丁建议不要在 UML 图中绘制所有类,只绘制重要的类。然后他继续解释,图表的主要问题在于绘制它们的人试图使它们全面。图表应该帮助我们理解简洁明了的信息,而代码应该是全面信息的来源。

寻找原子操作

由于一次提取单体并为其新应用进行更改是不可能的(记住,“没有大爆炸……永远”),我们必须设计一种方法,逐步从应用中提取功能并将它们迁移,一步一步地,并尽量减少对新的、现代实现的影响。但顺序是怎样的?这就是地图将帮助的地方,它显示了模块及其依赖关系。从现在开始,我将使用任意架构来更好地解释方法,即使它们与这个项目发生的事情有所偏差。让我们想象一下,我们需要“拯救”的三个重要特性基于以下依赖关系的五个软件模块(注意,有功能实现——层 2 和层 3 之间的链接,以及技术耦合——层 3 内部的链接):

图 4.4 – 函数及其实现的简单示例

图 4.4 – 函数及其实现的简单示例

一旦我们完成这个,我们就可以使用提供的信息来绘制两种时间顺序的方法(在这个部分中,时间关系最为明显)。第一种方法可能是,尽可能快地将第一个功能推出(市场时间策略):

图 4.5 – 以市场时间优先级的场景

图 4.5 – 以市场时间优先级的场景

在这种情况下,重写具有功能(或功能)1 和 2 的引擎的责任最终被推迟,但至少功能 3 的新实现可以快速可用。这种方法的缺点是,由于它将技术债务推迟到以后,开发过程将保持缓慢,直到项目结束时功能将更难发布。

这需要另一种方法,其中首先解决技术债务。这种方法的优点是未来功能将快速流动。然而,这种替代方法的缺点是,我们将在一段时间后才能看到结果(正如之前解释的,这是你最好有一个强大的商业案例来说服利益相关者资助这项投资的地方)。这种第二种方法可以在以下图表中看到:

图 4.6 – 以 TCO 优先级的场景

图 4.6 – 以 TCO 优先级的场景

为了总结每种方法的优缺点,最好是将两个图表叠加,这提出了主要差异,以下用字母 A 和 B 标注:

图 4.7 – 两种场景之间的图形差异

图 4.7 – 两种场景之间的图形差异

标记为A的 delta 显示了初始市场时间的差异。这在许多业务中都是一个重要的标准,因为客户需要真正相信你在前进。无论你与客户或内部用户的关系多么可靠——顺便说一句——很难让他们在没有展示你几个月来所做的工作的情况下离开,商业所有者知道这一点。在第一种场景中,功能 3 更早地进入市场,并可能参与整个项目的财务投资。在第二种场景中,第一个发布的功能不仅来得更晚,而且与之前的不同,这可能会根据用户最重视的内容产生很大差异。

标记为B的 delta 显示了在项目上花费的总时间差异:虽然场景 2 比场景 1 更慢地显示出初步结果,但它解决了项目开始时的更多技术债务,这将使剩余时间的发展更容易和更快。这是应该考虑的事情,因为开发团队的成本很高。根据项目的复杂性,这个 delta 可能变得非常重要(注意,在前面的图表中,刻度是完全任意的,并不代表任何有代表性的事物,因为它取决于你的项目)。

现在我们已经确定了两个基本场景,我们将深入探讨一些更现实的内容。

根据业务准则优先处理行动

这是一个常用的技巧,但提出两个极端的替代方案通常有助于让利益相关者选择一个中间的方法。有很大可能性,决策不会在之前解释的两个极端方法之间,而是在中间某个妥协点。但话又说回来,你如何调整光标?你可以使用哪些准则来进行这种调整?

对于任何有商业头脑的软件架构师来说,一些标准准则会立即浮现在脑海中,即总收入/营业额和盈利率。通常,我会让第三个准则是一个模糊的东西,利益相关者会根据对公司战略的重要性来评估它(他们对这一点比任何 IT 人员都了解得更多)。在项目这个阶段重要的是,这些准则应该快速评估,以“规划扑克”模式进行,并且数量应该有限。我为每个准则使用一到三颗星的水平,整个准则不超过三个。这通常足以找出最佳场景。

回到更现实的话题,这里有一个例子,是我为另一家我咨询过的公司创建的决策表(一些条目已被删除或重命名,因为它们可能会透露太多信息):

图 4.8 – 通过所选准则优先处理项目

图 4.8 – 通过所选准则优先处理项目

有趣的是,在这种情况下,你会注意到,准则并不是之前提出的“标准”准则。这里,我们有以下内容:

  • 改善的流程数量(在地图的第一层)

  • 对整体生产力的预估(这是一个工业生产公司)

  • 对增长预估的影响(它们在一个快速整合的市场上运营,小公司被大公司收购,因此快速增长对繁荣至关重要)

此外,还增加了一个列到传统的项目估算权重和延迟估算中,即与项目相关的冲击和风险(这是一个合理的方法,并且应该适用于任何此类项目)。为了强调这样一个项目的易于阐述比完整性和对方法的精确尊重更为重要,以下是表格在直接在白板上分析 2 小时后的初始样子。

您可以在 GitHub 上找到这张白板图片:github.com/PacktPublishing/Enterprise-Architecture-with-.NET/blob/main/Whiteboard%20image.jpeg

最后,关于时间、语义和对齐的几点话

为什么会有一个关于软件开发中时间重要性的完整章节?你可能想知道这个问题,我希望我已经说服了你,不是时间的的重要性,这在任何项目中都是不言而喻的,而是许多软件开发和信息系统设计中的问题应该通过时间管理的棱镜来观察。

经常发生的情况是,我们把它视为理所当然,因为它在我们无法采取任何行动的情况下悄然流逝,我们在设计活动中忘记了时间,没有考虑到未来的视角。有多少信息系统因为它们被设计来解决今天的问题,而没有考虑到业务将如何在未来发展而诞生不良?技术演变,当然,我们可以处理它们,有时我们做得比应该的更多,通过为下一个框架、未来的技术等做准备。但这不是问题!重要的是业务领域功能:它们也在变化,信息系统必须与业务保持一致,而不是与可能在几年后成为过时的技术演变保持一致。

当我们倾向于将概念视为稳定时,时间也常常被遗忘,仅仅是因为我们没有使用足够广泛的时间范围来解释它们。许多概念在表面上看似极其稳定的同时,实际上正在演变。在第三章中,我解释了“客户”这一概念是一个业务规则而不是一个稳定的实体:对于商业而言,客户可以是过去 12 个月内购买过东西的任何人,而对于营销而言,可以是过去 24 个月内购买过东西的任何人。也许维护部门会根据谁获得了运行保证来列出客户名单。这些规则,就像任何业务规则一样,会随着时间的推移而改变。在某个时候,也许大老板会厌倦商业和营销没有传达相同的客户数量和演变速度,并强迫他们采用一个共同的定义;也许保证期限会改变,这将影响客户维护名单。谁知道呢?有一点是肯定的:如果你没有考虑时间,你会遇到麻烦。

一个小故事——我希望你能从中发现时间的重要性——来结束这一章:我碰巧为一个组织提供建议,该组织通过与其他组织的合并,不得不更换他们的标志。由于这些组织在选举后都受到政治变化的影响,那里的人们希望使将来调整标志变得更加容易,因为这项操作非常耗时且无聊:标志必须从信息系统中产生文档的每个应用程序的每个消息模板中更改,而我们谈论的是数千个模板和数月的工作来调整标志。最初的方法是在每个地方传播服务器共享路径:这样,当文件被修改时,更改标志将自动在所有地方发生。幸运的是,我们有时间稍微思考一下,并决定将方法切换到通过内容协商公开不同格式的标志资源的 URL,并使用额外的 URL 查询参数来指示要发送标志的参考时间。这样,大多数只是创建文档的应用程序不必关心传递此参数,并将获得最新的标志位图资源;但对于少数法律约束的应用程序,它们必须能够在过去某个时间点与精确的像素完美形式融合信函,仍然可以这样做,而不会在旧信函上出现新标志,这会引起麻烦。如今,这个组织已经配备了一个具有存档功能的内容管理系统,现在以更好的方式解决了这个问题。技术进化以新的方式处理了功能需求。再次强调,这只是时间问题!

关于时间和技术债务在信息系统中的重要性,这里有一个最后的注意事项:可能存在比技术债务更糟糕的事情,我们称之为“功能债务”或更精确地说是“语义债务”。这将是第九章的主题。但就目前而言,我们将以一个非常理论性的内容结束这本书的理论部分:一个完美的信息系统!

摘要

在本章中,我们展示了时间如何影响信息系统分析中的每一件事,因为它是一个在使用过程中不断演变的活生生的实体。时间在我们的日常生活中如此普遍,以至于在构建信息系统时常常被遗忘,但它却是设计它的关键,使其能够经受时间的考验,并具有较低的 TCO。

敏捷方法是最早处理这种时间相关现实的方法之一,它彻底改变了软件的创建和处理方式。同样的方法可以应用于整个系统,即作为一个协同工作的应用程序集合,这意味着技术债务可以全局处理并保持受控。这种技术债务的概念命名并不准确,正如未来章节将展示的,语义学非常重要,因此我建议时刻牢记这一点,并在可能的情况下调整命名,正如本章所提出的。

本书剩余部分包含许多食谱,以帮助您减少这种技术债务或至少将其保持在可接受的水平。但在下一章中,我们将尝试想象一个完美的信息系统,几乎没有任何债务或耦合。

第五章:一个乌托邦式的完美 IT 系统

记得在第四章中关于由计算机、人类和狗组成的完美信息系统笑话吗?其中有些真理,因为人类一直在变化(他们的思想、他们的做事方式、他们在商业中遵循的规则、他们想购买的东西等等),而计算机则乐于重复、稳定、定义明确的任务。当然,这两者并不兼容,因此需要狗来防止人类把计算机工作搞得一团糟。

但谁首先创造了计算机?当然是人类。所以,这个笑话并不是说人类应该完全不接触计算机,而是创造它们,然后让它们在不做任何改变的情况下完成工作。当然,这只有在第一次尝试通过某种奇迹变得完美的情况下才可能实现。

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

  • 理想系统的概念

  • 理想系统中的数据管理

  • 理想系统中的规则管理

  • 理想系统中的流程管理

  • 我们能接近这个乌托邦式系统有多近?

本章将描述一个类似于 100%理想信息系统的东西,它能够适应不断变化的商业变化,同时在速度、鲁棒性和能源效率方面仍然表现出色。这样的系统是由多米尼克·沃基耶在他的基础书籍《可持续信息系统 - 使用 SOA 重铸 SI》中想象的。构成理想信息系统的三个主要实体是主数据管理MDM)、业务规则管理系统BRMS)和业务流程建模(我们将在稍后详细解释)。接下来的几节将逐一解释它们,本章将以分析如何在现实中创建这个乌托邦式系统以及什么会阻止它为结尾。

理想 IT 系统的概念

在我们详细说明这样一个理想系统的不同部分之前,我将进一步解释这样一个模型的有用性。当我们对想要达到的目标有了更清晰的认识时,我们将详细说明概念的不同部分,并尝试使它们更加具体。

理想结构的用途

“理想”或“乌托邦”是工程师们通常有一种奇怪关系的术语。尽管他们的大部分思考都是在假设的情境中进行的,以在理论上取得进步,但他们知道实践总是与理论有很大距离,并且根据他们在理论与实践之间的掌握程度,工程师们很容易陷入许多陷阱:

  • 仅停留在理论层面可能会让他们的思考从未应用于现实世界,最终导致时间的浪费以及无法真正了解他们工作的价值。

  • 缺乏足够的理论基础来在他们的领域取得进步;仅仅停留在工作的实践中可能会帮助完善某些领域,但很少带来强大、改变领域的革命性想法。

  • 最糟糕的是:在两者之间切换,但从未真正建立连接,使实践从理论中受益并从实践中证实理论。这种将两者结合在一起的能力,在我看来,是构成最佳工程结构的关键,无论是公司还是甚至国家,只要它们在最高水平上教授和组织这种能力。

正因如此,在这本书中有一个位置,否则它非常偏向于实践,并从许多现实世界的信息系统中获得了丰富的经验回报,但也有一些理论和理想化的思考。

设计的起源

我们将要描述的理想系统的结构,最初是在多米尼克·沃基耶的《可持续信息系统 - 使用 SOA 重整信息系统》中提出的,他是法国最著名的架构师之一,也是 PRAXEME 方法的倡导者。我有幸在 10 多年前接受了培训,其解释非常清晰,我立即购买了他的书籍,这些书籍很快成为我在信息系统工业化咨询工作中的参考之一。

这个想法背后的原则是,了解许多信息系统,可以描述一种元系统,它将包含所有必要的功能,只需几个通用的模块,这些模块可以定制以实现与业务相关的方面。这三个模块中的每一个都将非常通用,与任何特定的业务都没有关系,这也解释了为什么整个系统中内容如此之少。这三个领域如下:

  • 存储数据并使其可用(简而言之,这正是数据库所做的事情)

  • 执行业务规则以做出决策(这将涉及使用数据的软件应用程序)

  • 通过协调许多小任务来实现复杂过程(尽管非常近似,但这正是使应用程序协同工作的关键)

多米尼克·沃基耶确实能够将任何业务功能指认为这些三个不同技术功能的组合。在与许多小型和大型公司的工业信息系统合作后,他认为这三个模块结合起来将能够处理任何给定业务领域的所有可想象的特征。尽管现在这种结构还没有被广泛使用,但它可能只是对未来几十年将要工业化和良好控制的工业信息系统的一个非常期待的预览。至少,从我的谦逊观点来看,这是这样一个具有前瞻性结构的最佳候选人。

使用愿景来定义目标

我们为什么要讨论这样一个如此未来派且我们没有任何证据证明其能够在实践中应用的架构呢?这会不会导致过度思考或过度设计?如果你自己问自己这些问题,你完全有理由这样做,而且这一章节并不是要推动你为你的下一个信息系统架构使用这个工具。

就像任何模式或架构工具一样,它主要存在是为了给你一些想法,如果其中的一些部分适合你的系统,那么它帮助你前进并加速思考将会是非常好的。

但是,拥有一个“理想”或“乌托邦”的愿景也可以帮助你给你的实现提供一个全局的导向。记住敏捷开发的隐喻,我们不是射箭,而是带着箭走向目标,当达到目标时用手将箭射出?好吧,为了做到这一点,我们同意我们需要知道目标是什么样的,以便找到并达到它。有时候,目标形状可能很难想象。当然,一个以业务为导向的思维方式会给你最好的想法,你应该保持功能性的关注。但如果业务想法相当模糊呢?也许一个“柔软”的、可适应的信息系统会允许你在事情变得稍微清晰一些的时候开始工作。开始构建系统甚至可以帮助企业主更好地思考他们想要什么。而且如果你有一个可以非常快速适应他们构建业务想法的理想系统,那么这可能就是你需要的项目类型。

当然,完全通用的成本(在性能上,以及初始设置的时间上)将会更高,但如果你的情况是这样的,通过简单地定制信息系统的参数来适应不断变化的企业目标的能力可能会在很大程度上克服这些缺点。而且一旦企业愿景确定,就没有什么可以阻止你重新实现一些现在稳定的部分,用特定的、优化的代码来替换,如果需要的话。

既然愿景已经清晰,我们将深入探讨构成这个理想 IT 系统的三个部分,即数据管理模块、规则管理模块,以及最后的流程管理模块。

理想系统中的数据管理

MDM 是理想系统的第一部分。在讨论数据是如何处理和使用的之前,解释数据将采取什么形式以及如何管理它是合乎逻辑的。这就是为什么我们将在讨论 BRMS 和 BPM 之前解释一个乌托邦系统中的 MDM 概念。

数据是信息系统的血液

数据是任何信息系统中最重要的组成部分。有些人甚至可能认为它们完全是关于数据的,因为“数据”这个术语在过去十年中被炒作得如此之高。

虽然很难想象一个没有至少一个数据库来存储数据的信息系统,因为这意味着没有任何关于任何商业事件的知识留存。可以想象一些完全短暂且未存储的信息片段,但由这些特定案例组成的整个系统听起来几乎无法理解。

这意味着数据应该首先得到关注,这正是理想通用信息系统第一个模块的主题。这个特性被称为 MDM。在这个情况下,“主”一词指的是系统中最重要的数据,那些被系统中的大多数参与者使用的数据。但在这种乌托邦式的系统特定情况下,每一份数据都会被视为如此,并放置在一个单独的模块中,该模块负责“MDM”这一职责。

数据作为 21 世纪的石油——真的吗?

在我们更深入探讨这样一个模块可能是什么样子之前,关于数据的重要性有一个小的补充说明:你可能听说过关于数据的“21 世纪石油”这个表达。这是为了强调数据已经成为商业组织如此重要的一部分,它可以与 20 世纪带来巨大工业进步的石油相提并论。

许多工业公司使用数据来跟踪他们的机器,优化生产,并将其与销售和股票联系起来,简而言之,通过“数字孪生”虚拟化工业流程以更好地控制它。但对于某些行业来说,数据甚至可能是原材料本身,如今许多数字原生公司纯粹通过数据收集、精炼和转售来赚钱,尤其是针对广告活动。

石油开采,自 20 世纪以来就已经发生,极大地加速了几乎所有领域的工业生产(重工业、铁路、化肥、农业机械等),同时也催生了众多公司自身进行石油的勘探和生产。这种比较是公平的,尽管在保持一些重要差异的同时需要谨慎。

首先,数据的类似物——石油裂化精炼塔——尚未发明,或者至少尚未以标准形式出现。当然,一些商业智能工具和大数据方法在某些情况下可以提供帮助,但数据项目失败率(据估计在 70%到 80%之间,取决于研究)表明,我们距离石油精炼厂的工业化还有很长的路要走。到目前为止,数据对于大多数公司来说仍然部分地处于未开发状态,就像 19 世纪末土地所有者对土地的态度一样:污染。当然,数据中存在价值,但它完全未被开发,且没有标准的方法来提取这种价值。

我们也不要忘记,数据仍然基于实际的石油:数据中心消耗了大量的能源(很快,地球上 8%的能源将被用于数字用途,这超过了航空旅行,并将很快达到汽车运输)。尽管一些数据中心所有者假装使用可再生能源(但只是购买补偿活动,这并不会减少全球石油消耗),但大多数数据中心仍然严重依赖石油作为能源。此外,服务器的生产、网络硬件的使用以及它们的运营功能都是能源的大消费者,最终也是石油的大消费者。

因此,我们必须牢记,数据确实可能成为下一个世纪的石油,但我们目前正处于这种状况。本章中描述的数据管理方法可以帮助你达到这一目标,特别是通过使用治理来提高数据质量,这仍然是阻碍数据利用的首要原因。

一个真正的“无所不知”的系统

我不会在本章中过多地详细阐述,因为 MDM 将在本书后面的专门章节中讨论,并且将结合一个真实案例进行解释,这有望使一切变得更容易。现在,我只会提出将 MDM 与“无所不知”的系统进行比较。确实,一个制作精良的 MDM 不仅会知道实体当前的状态,还会知道每个实体的整个故事,并记住它们的不同状态和修改,从摇篮(创建)到坟墓(存档或删除)。正因为如此,持久性是 MDM 模块的主要责任。

然而,还有一些其他的责任:

  • 研究的便捷性:这与坚持一致,因为如果不提供数据,那么保留数据就没有太多意义。这个特定的责任可能在其实施过程中被委托出去,通常当 MDM 应用程序使用索引引擎时(全文搜索是一个复杂的问题,它证明了将责任分解成几个更原子的责任并委托给专门的模块是合理的)。

  • 报告/商业智能的来源:就像数据搜索一样,报告是 MDM 模块可以委托给其他实现的重要责任,例如数据湖。

  • 处理实体多个版本的能力:正如之前所解释的,一个真正的 MDM 应该知道数据实体的每一个版本,而不仅仅是它的“最新”状态(引号用来强调这个“最新”概念在许多系统中都是一个痛点,尤其是在分布式系统中,因为它依赖于数据的一致性,以及因此事务、乐观/悲观锁等等)。

  • 管理属性值验证的能力:尽管这可能与 BRMS 共享责任,我们将在本章后面讨论 BRMS,但 MDM 本身可以执行少量验证以确保其基本一致性。这不应与数据正确性混淆,数据正确性取决于其目的(单一数据可以适用于商业用途但不适用于另一个用途),并且可以根据数据版本以及业务规则的变化而很好地变化。

  • 业务规则:一些入门级业务规则,仅使用 MDM 系统内部的数据,也可以添加到 MDM 中,尽管如前所述,它们大多数都与验证有关。

  • 历史处理:除了存储数据的连续版本外,MDM 还必须能够根据目标时间轻松检索这些版本,浏览历史记录,找出谁对数据进行了哪种类型的更改,等等。

如果这还不清楚,请不要担心——本书后面提供的关于一个 MDM 存储个人数据的例子将帮助您理解所有这些责任是如何被使用和实施的。

与 CQRS 的关系

如果你对数据架构感兴趣,你可能已经将存储数据版本化修改和搜索其不同状态之间的关注点分离识别为命令和查询责任分离CQRS)的原则。我们确实在谈论相同的原则,即使 CQRS 远远超出了仅仅分离这两个责任,并提出了如何使它们一起工作的技术方法。

CQRS 是一个太大的主题,无法在本书中与其他所有主题一起处理,但它非常适合 MDM 模块的责任分离,我强烈建议你在创建 MDM 实现时使用这种方法。本书接下来几部分将要涵盖的例子将使用 CQRS 方法,尽管这是一个非常有限和简化的实现。再次强调,这是一个版本选择,因为添加完整的 Kafka 引擎会使我们难以专注于应用程序本身,并且会使我们远离本书的主题。

数据质量的需求

回到石油的比喻,里面含有沙子是一个真正的问题,因为石油必须被足够净化才能进入工厂。否则,不仅输出产品的质量不会很好,而且这还可能损坏这个极其昂贵的工业设备,即精炼塔。这就是为什么轻质原油是最好类型的石油之一,也是为什么来自沥青砂的天然气之所以如此昂贵进行精炼(除了对我们环境的灾难性影响)的原因。

在信息技术领域,数据与清洁数据之间有着强烈的等价性,因为清洁的数据是一个伟大的产品,它将允许对商业活动进行精确的报告,对可能出现的问题进行快速洞察,并总体上更好地控制公司。与行业内任何人讨论这个问题,每个人都会同意数据质量的重要性……然而,据估计,数据科学家的工作中超过一半的时间是用来清理数据的。我见过一些公司,这个比例危险地增加到了大约 80%。我说“危险”,有多个原因:

  • 首先,为这样的低智商任务支付高薪是一种金钱的浪费

  • 这通常也是一种浪费时间的行为,因为优秀的数据科学家通常会在接下来的 6 个月内离开工作,如果他们不得不花更多的时间在清理和组装数据上而不是“让它说话”

  • 最后,它揭示了管道中的问题,因为发送这些脏数据的 IT 系统设计不正确,这意味着这些报告问题可能不是唯一的

为什么每个人都同意数据清洁的重要性,但情况仍然如此糟糕?好吧,恰好数据管理通常不是关于技术部分(这相当简单,因为我们现在有这么多工具)而是关于组织部分:谁负责哪些数据?谁有权利收集和更新它?哪个团队决定如何切割数据以及哪些数据进入这个或那个服务责任?所有这些问题都是所谓的数据治理的一部分,尽管它非常重要,但许多公司并没有处理这个问题,尽管他们拥有所有存储数据的工具。再次,这是与往常一样的根本原因:技术从未解决过组织问题,但编辑们非常擅长让你相信这一点……而且,作为一个公司老板,你非常希望它是真的!但是,不,你将不得不做一些不有趣的工作,比如数据分类,找到数据所有者和数据管理员,实施定期的会议来跟进决定的数据流程,等等。现在创建你的 MDM 模块正是做这件事的正确时机,因为它的成功很大程度上取决于这些行动。

设计一个通用的“参考”

几乎总是会有一个内部标识符来指代存储组织主要实体的软件应用程序,例如产品。大多数时候,人们会根据技术名称来称呼它——例如,“产品数据库”或“产品 Excel 文件”——但这是一种坏习惯,因为它将功能概念与技术、软件概念耦合在一起。这不仅可能分别发展(“产品文件”变成了“产品数据库”),而且它们应该尽可能多地这样做,因此应该保持分离。

从这个角度来看,“产品列表”已经有所改进,但我的最爱(因为它与我母语法语中的一个等效词非常接近)是将其称为“产品参照物”。这个名字承载着这样一个重要概念:它假定数据内部具有参考地位,同时也可以将其视为空间中的参照物,这回到了信息系统地图的概念,就像我们会有一个地理地图,其中包含参照物内的坐标。

重要提示

命名可能会更糟糕,正如我在第三章中提到的轶事,我的一个客户将文章的参照物简单地称为“Serge 的文件”。这导致了很多混淆,因为它隐藏了保持此文件最新状态的极端重要性,而这甚至不是 Serge 的正式工作!

大多数情况下,所谓的“参照物”都是针对单一类型的实体(产品、销售、客户等)。本章所描述的理想化系统的想法是,对于任何类型的实体都使用单一款软件。在本书的其余部分,我们不会深入探讨这个理想化系统,而将保持一种更标准的方法,即针对特定的业务领域实现一个参照物。这也使我们能够为每种情况采用最佳技术(“最好的面包”方法),而不是“一刀切”的方法,尽管在理论上很有趣,但在现实世界中却极其复杂。

MDM 的实施选择

无论喜欢与否,Excel 仍然是你可以拥有的最简单的 MDM(主数据管理),在许多小型公司中,使用 Excel 进行良好的组织流程已经可以走得很远,前提是你已经消除了良好组织的重要缺陷(例如,一个集中式文件,不同用户有权限而不是到处都是副本,严格的数据质量组织等)。

当然,将需要一定程度的复杂性,这将导致专门的实现。这听起来很疯狂,但针对 MDM 的软件应用非常少。微软基于 SQL Server 提供了一项名为 Master Data Services 的解决方案,但它的使用听起来非常有限,至少在我的知识领域内是这样。在与微软讨论后,该产品确实已被放弃,其继任者是名为Profisee的合作伙伴产品,它与Microsoft Purview合作以提供数据治理功能(learn.microsoft.com/en-us/azure/architecture/reference-architectures/data/profisee-master-data-management-purview)。Semarchy (www.semarchy.com)听起来是集成 MDM 的一种新颖且有趣的方法,但我还没有尝试足够多,无法推荐它。

我看到一些公司使用无头 CMS 系统,如 Strapi、Sanity、Cockpit、Prisma 等,来创建可以作为入门级 MDM 的后端。但这通常缺乏良好的数据版本控制系统,并且治理的实施仍然是集成商的工作。总的来说,这是一个非常基于技术的方案,如前所述,MDM 远不止是简单地存储数据。

这就是现成方法的概况,诚然这并不走得很远。我至今所见到的每一个 MDM 的正确实现都是一个专门的开发。一些商业应用程序,如 ERP,有时对产品或客户有良好的参考,但大多数都缺乏业务上的对齐。

小贴士

可能听起来逻辑上 ERP 可能不与业务对齐,因为它试图适用于许多业务领域。尽管如此,当你看到前五的 ERP 系统为顾客和供应商提出两个不同的领域,忽略了大多数现有公司可能的数据重复问题时,这表明问题不仅仅是商业的多样性,还有这些 ERP 公司的非常方法,他们认为建模业务是 100%客户的责任,而它应该是基于书面、向前兼容的标准共享责任。但这将违反供应商锁定,这不符合他们的利益,这就是为什么唯一的前进方式是客户选择一个接受业务实体表示标准的实现,包括在(罕见的)它们不存在的情况下开发这些标准。

剩余的——并且代表了运行中的大多数 MDM 系统——是专门的应用程序与数据库(有时是索引引擎,甚至是一个数据湖,如前所述)相结合。它们是由内部 IT 或外部软件公司为特定的业务用途定制的开发。如果你从全球的角度来看,许多公司的需求非常相似,那么这将是时间和金钱的巨大浪费。然而,由于软件行业缺乏使用的业务标准,目前这只是一个现状。

现在是理想系统的第二部分:在讨论数据之后,我们需要讨论业务规则。

理想系统中的规则管理

业务规则是应用于数据上的谓词,以帮助实施对业务必要的现实世界决策。例如,你可能声明,只要客户没有检查他们的银行坐标,就不能向他们发送产品。这是一个业务规则,因为它可以不带有任何软件实现的迹象来表述:这可以通过工厂中的人手动检查,通过电话与会计确认规则是否得到遵守,然后再将包裹发送给客户。

规则作为信息系统中的神经系统

如果数据是信息系统的血液,那么业务规则就是其神经和肌肉网络:它们利用血液以特定方式实现某些活动。在我们的比喻中,一个业务规则可能是“如果你感觉手指有灼热感,手臂必须缩回。”这个规则在我们的脊髓反射系统中得到实现,它使用手指中的传感器通过神经发送信息/数据,导致相关手臂的肌肉无法被控制。

在 IT 领域,业务规则的实现主要是所谓的应用程序。这通常是业务规则包含的地方。在我们的例子中,ERP 系统中必须有一些代码,它会警告包装准备系统客户尚未通过已验证的支付方式条件。包装系统的反应可能是这个订单将不会处理,或者可能是它将提前处理,但实际的物理包装将在发货前保留。实际的实现是每个模块的责任,但业务规则保持不变:“只要我们没有验证我们可以收到付款,就不会有客户交付订单”,

业务规则还包含你将在信息系统中执行的所有计算,从最复杂的到最平凡的。让我回到这一点,因为关于责任正确划分的某些事情需要说:任何客户端(GUI 或调用 API 的应用程序)都不应该处理业务规则,这些规则始终必须在服务器端实现。这肯定是你所知道的事情,听起来也很合理:毕竟,显然这些重要的功能推理必须集中化,以确保它们以相同的方式应用于所有地方。但看看任何客户端几分钟,你会发现有很多在本地做出的功能决策,有时甚至是最合理的。例如,考虑从毛额和增值税率计算净额。当然,增值税率在应用程序的生命周期中不会经常变化,计算方式本身也非常稳定,因此应该没有大问题,可以信任客户端进行这种计算,而且我们通过避免往返服务器而获得更好的性能。

好吧,但你总是可以想象业务规则会发生变化的方式。正如已经想象的那样,增值税率可能会变化。此外,如果我们谈论多行订单的净额计算,你必须在一个实体中处理多个增值税率。问题归结为风险管理:如果你知道你永远不会遇到这些特定情况,那没问题。如果你怀疑它们将来出现的可能性,你应该首先向你的产品所有者询问。如果他们对此不确定,你将不得不比较不同方法的开销:

  • 您可以在所有客户端中硬编码业务规则并希望它不会演变。如果它们不多,并且您可以轻松升级它们,那就没问题。

  • 您可以通过将利率作为您使用的应用程序的参数(当它们支持时)来为可能的变化做准备;这样在调整时已经更好、更容易,而且无需巨大的前期成本。

  • 如果您预计在未来几年内业务规则将发生变化,并且您知道调整软件将会很复杂,那么您可能应该将规则放在服务器端、集中式的应用程序中。当然,它可能会稍微慢一些,但客户端缓存可以帮助,并且您将具备未来适应性。

  • 如果您处于一个将发生大量业务变化的情况,并且许多业务规则需要处理,那么您将达到一个投资于一个专门用于管理您的业务规则的集中式软件是有意义的点。

BRMS 作为处理业务规则的专用工具

现在,让我们集中关注最后一个假设,即您需要所谓的 BRMS。再次强调,这并不是您在每一个信息系统中都需要的功能模块,最简单的系统也不需要这样的安装努力。但请注意,大多数信息系统的起点都很简单,但会变得更加复杂。真正的困难在于,一旦您进入生产阶段,用 BRMS 定制替换现有规则将会更加困难,因为如果它们没有在 API 合同下统一,所有调用都必须更改。这就是为什么这个乌托邦式的信息系统是有意义的:它展示了如果您对您的信息系统认真负责,并且不想对其打任何赌,您应该做什么(并且不会隐藏投资的规模)。如果您从一开始就知道它将会增长,您的活动将会变得工业化,那么您应该从一些一开始可能看起来过于复杂和昂贵的工具开始,但它们将在未来带来巨大的回报。如果您对此不确信,只需阅读所有那些因为未投资于他们的信息系统而失败的初创公司的经验回报,尽管他们的想法得到了客户的认可,他们的定价合理,并且他们拥有市场:这是初创公司失败最常见的原因之一,也是大公司(后者通常不提供任何反馈)。

业务规则的常见实现

你会如何实施一个 BRMS?可能会让你微笑,但同样,Excel 通常是 BRMS 最简单的实现方式,大多数时候人们并不知道这一点。一个会计肯定会告诉你(如果你问的话),他们没有使用任何 BRMS 来完成工作。但与此同时,他们会吹嘘一个手工制作的 Excel 工作簿,其中包含了他们在日常工作中以及每月结算期间所需的所有计算(以及相应的业务规则,无论是监管还是公司层面的)。如果不是 BRMS,那又是什么呢?事实上,由于“真正的”专用 BRMS 相对较少使用,对于大多数小型公司来说,业务规则实际上都包含在 Excel 文件中。

在大型公司中,有更多的业务线应用,例如 ERP,或者当公司有内部软件开发能力时,甚至定制软件。在这种情况下,集成到应用中的业务规则比例上升,但这并不一定是最好的。为什么许多以业务为导向的人尽管存在缺点,却仍然喜欢 Excel 电子表格?简单来说,因为他们可以完全控制它,并且不需要依赖 IT 部门进行更改、添加功能等。在某个应用中开始实施业务规则,耦合问题立即显现:

  • 是否可以在不重新编译、测试和重新部署应用的情况下更改规则?

  • 如果规则可以定制,是否可以通过功能操作员来定制,或者你需要 IT 部门来实施更改(或者最好是在最佳情况下,对他们进行培训)?

  • 我们如何处理必须在特定日期和时间更改的规则(例如,许多与会计相关的或业务范围内的报告规则在午夜 1 月 1 日)?

  • 是否可以有两个业务规则的同时版本,或者我们是否必须停止软件在年底,以确保在新的一年规则更改之前没有任何操作(不要笑:这种情况经常发生……)?

尽管存在这些潜在问题,但在专用业务应用中实施业务规则已经是一个很大的进步,尤其是如果这是在服务器端完成的,最好是作为实现一个良好文档化的、以合同为先的 API。这种解决方案比 Excel 电子表格更不容易出错,因为规则的不同实现可能会在整个组织中传播,导致混乱。它还倾向于提供业务规则的第一级治理,因为不同的服务负责“他们”的软件应用,以及其中实施(或定制)的业务规则。

然而,这些实现并不能代表我们乌托邦式系统中的 BRMS,即一个包含公司所有业务规则的独特系统。而且,如果你想走上一条未来证明的信息系统之路,你可能会更倾向于使用像 Drools(一个开源包,由 Red Hat 的 JBoss Rules 实现)或 IBM Operational Decision Manager 这样的软件。这些软件通常会提供以下功能:

  • 业务规则的执行,这提供了所谓的决策引擎(DMN代表决策模型和符号,是 BPMN 的伴随标准)。如果你想了解更多关于 BRMS 这个重要部分的信息,我推荐 Drools 提供的优秀文档页面,可在www.drools.org/learn/dmn.html找到。

  • 这些业务规则的存储定义,听起来很显然,但当你加上存储整个业务规则版本历史的要求时,情况就不同了。

  • 决策表的使用、事件和动作之间的链接,以及在最复杂的情况下,将混合许多不同规则以找到额外结论的推理引擎(一个过于简化的例子是“所有采购都需要发票”和“发票必须在标题中包含买方的地址”,到“所有采购都需要买方的地址”)。

  • 其中一些提供了图形编辑器,让非技术人员能够自行调整规则。尽管编辑器声称任何人都可以使用,但它们仍然相当技术性,需要一些培训。

  • 与此同时,沙盒对于人们在将业务规则调整投入生产之前进行测试非常有用。

  • 根据所需的性能和调用变化的程度,缓存机制可以是一个很好的 BRMS 补充。

小贴士

Open Policy Agent 是一个专注于授权规则的 BRMS。我们将在第十二章中展示其用法。

最后,在讨论数据和业务规则之后,我们将添加第三个也是最后一个模块,以完成我们的理想 IT 系统。

理想系统中的流程管理

我们理想中的系统可以处理数据,并使用业务规则对其做出决策,但是什么让它运转呢?在第四章中,我们强调了一切都是时间的问题,但没有任何东西具有将数据和规则置于受控、基于时间的运动中的角色。这将是由流程引擎来扮演的角色。

流程作为信息系统的大脑

回到我们的人体比喻,你可能已经猜到,业务流程管理(BPM)作为协调包含业务规则的应用程序动作的模块,可以与大脑的控制塔相联系。正如我们的大脑协调每个肌肉的动作,同时接收来自我们五感的信号,以实现诸如接球、键盘打字或穿衣等复杂的结果,流程描述了信息系统每个部分必须完成的复杂任务,以实现全局目标,而 BPM 引擎协调这些任务,有效地调用负责的模块(或者更确切地说,API,这些 API 本身由技术模块实现;还记得我们在第三章中解释的重要观点吗)。

如果理想的信息系统由一个 MDM 和一个 BRMS 组成,包含每个业务领域的所有数据和所有业务规则,那么 BPM 就是使一切协同工作的模块,通过互操作所有功能。

BPM 的实现

在谈论技术实现(这现在应该是一种本能)之前,我们必须尝试找到一个我们特征的可接受标准。幸运的是,一个无可争议的标准已经存在,我们之前已经讨论过:BPMN。所以,如果你在寻找一种执行流程的演变方式,其中引擎可以完全与设计解耦,你必须使用 BPMN。我已经在一个特定的编辑器的 GUI 上测试了编辑 BPMN 2.0,并在另一个编辑器的执行引擎上执行,它运行良好,这表明了该标准的成熟度。

当然,并非所有信息系统都需要我们讨论的这种复杂性,但它们在某个时候都会使用一个中央协调系统,即使大部分可以手动操作,流程可能甚至没有在 BPMN 中设计,而只是信息系统参与者头脑中知道(再次强调,这并不意味着一切都是基于计算机的)。而且你知道吗?当我们根本不需要任何复杂性时,我们会找到我们老朋友 Excel!

小贴士

到目前为止,我知道你一定认为我是一个绝对的 Excel 粉丝。然而,我并不是,我也亲眼见证了由于使用 Excel 处理共享数据、没有控制其在组织中的传播以及更多问题而导致的巨大功能问题。我完全清楚它如何通过让几乎每个人都能拥有信息系统的一小部分信息来阻止人们共同工作。但与此同时,我也不赞成忘记电子表格为 IT 和业务专家带来的所有贡献。而且,现在大多数信息系统如此不成熟也不是我的错!所以,如果你必须使用一个粗糙的工具来完成工作……那就这样吧!Excel 将长期成为信息系统的“瑞士军刀”。拥有一个完整的工具箱会更好,但如果你只能拥有一个工具,那就拿这把刀而不是拔钳或一些其他奇特且高度专业化的工具。回到软件上,现在对于 MDM、BRMS 和 BPM,比 Excel 电子表格更好的替代方案已经存在。然而,如果你意识到 Excel 的缺点并控制它们,其多功能性和易用性将使你在信息系统的许多部分中加快速度。特别是,尽管 MDM 和 BPM 在 Excel 之外要好得多,但我经常通过在 Excel 中放入复杂的计算并将电子表格插入 API 中,建立了一些“低质量的 BRMS”,并取得了巨大的商业成功。是的,这有点像是一种黑客行为,但只要它隐藏在 API 后面,这种临时、快速和简陋的实施就没有问题……而且业务用户非常喜欢它!

我们能有多接近这个乌托邦式的系统?

到目前为止,我们已经描述了理想信息系统的内容,这是由 Dominique Vauquier 在他的开创性著作中描述的。这种 MDM + BRMS + BPM 的组装也可以在其他一些来源中找到,例如,SHIFT 项目在其向更环保的 IT 行业数字化转型的方法中描述的“可持续信息系统”,或者敏捷链管理系统ACMS)方法。

当然,这是一个理想化的愿景,所以问题不是将其用作实际信息系统都应该基于的蓝图,而是在设计新的信息系统或改进现有信息系统时,我们能够接近它有多近。

优先考虑上下文

各种上下文元素可能会促进本章讨论的方法的使用:

  • 全新的信息系统:如果你足够幸运,可以从一个新创建的组织从头开始,那么遗产就不会阻碍你,这是一个可以使理想信息系统得以实现的巨大因素。

  • 复杂程度:当然,这还远远不够,因为正如之前所述,只有当它得到商业的证明时,这样的理想系统才有其存在的空间。如果你的信息系统必须高度可进化,因为商业规则和市场经常变化,数据结构不明确,并且必须快速适应流程,那么这又是另一个可以促进之前所述架构采用的因素。

  • 投资批准:再次强调,这个因素也不够,因为商业可能需要这样一个高度复杂的信息系统,但投资者可能没有意识到这一点。对于许多首席执行官来说,IT 只是一个成本中心。但如果你理解信息系统已经成为大多数工业活动的支柱,那么你又有了一个积极的因素。

  • 实施能力:一切看似顺利,但不要低估实施这样一个信息系统所面临的困难。这种方法与大多数专业人士所知的方法大相径庭,以至于你很难找到能够部署它的人。你将不得不说服他们,培训他们,并让他们适应你的方式,而现有的反馈和经验回报非常有限。

诚然,这些因素有很多,而且你很可能现在没有它们。对于像我这样年龄的人来说,甚至有可能我们永远看不到这种完美的协调,使得这种乌托邦式的结构成为最常用的一个。但它确实存在——其优势是显而易见的,而且行业变化的加速和数字化转型将毫无疑问地使其成为一个越来越被观察到的选项。技术监控也显示,在过去 3 到 5 年中,MDM 的主题已经取得进展,一些先进的公司已经实施了它们,并伴随着一个内部开发者平台,带来了非常有趣的结果。简而言之,我们正处于趋势的初期,但波浪增长的可能性很高。

接近目标,敏捷的方式

这样的理想信息系统目前还不是主流,并且在未来也不会成为标准,这并不意味着它们没有兴趣。这就像一个在敏捷团队工作的人,他不想知道最终软件的愿景是什么,声称他们只对当前冲刺中将要完成的事情感兴趣!

同时也存在一个理想——一个愿景——来提供方向,指引我们的旅程。还记得箭的隐喻吗?我们必须知道目标的样子才能定位并达到它;否则,我们只是在漫无目的地四处走动,希望得到最好的结果。总之,请不要因为这一概念不是立即实用的就放弃它:了解它可以帮助你在未来在信息系统不同的方向之间做出选择,因为你将感觉更接近这个长期、理想的方向,这将为你带来更大的价值。可能性是,你永远无法达到理想状态,但希望,所采取的方向将为你的信息系统带来更多的价值和灵活性。如今信息系统的情况如此糟糕,拥有一些高于平均水平的东西在所有工业流程数字化时代已经是一个巨大的优势。

摘要

在本章中,我们展示了可能是一个理想的信息系统。我们讨论了在软件工业部署中可能出现的所有问题,特别是关于它们的演变,因此展示不仅限于点解决方案,还展示了如何全局性地解决这些问题。当然,这一方法的大部分仍然是乌托邦式的,但这一练习有趣之处在于它表明,如果构建得非常好,只有三个模块就可以满足任何商业需求。数据、业务规则和业务流程之间的分离是即使在您工作的信息系统远非理想的情况下也应该记住的事情。

本章相当概念化,但这本书的第一部分就此结束,这部分旨在涵盖大量的理论背景。本书的下一部分将涵盖更多关于架构的实用方法,为了完全阐述理论与实践(这是本书的主要前提),第三部分将提供源代码,展示如何实现我们讨论过的良好架构的概念和方法。特别是,这里解释的三个主要模块将在一个具有有限功能范围的示例应用程序中展示,但具有所有成长为完整信息系统的必要复杂性:

  • MDM 将基于 NoSQL 数据库,后端服务器使用 ASP.NET,并公开 API 设计的合同优先

  • 将使用一个独立的 BRMS 进行授权管理

  • 将采用 BPM 方法来展示如何使流程可调整,使用低代码实现

总结来说,在接下来的七个章节中,我们将通过使用适用于所需用例各部分的示例,运用经过实战检验的方法来设计示例应用程序。在接下来的五个章节中,我们将分模块实现这个示例应用程序。最后,最后三个章节将讨论部署和调整这个示例应用程序,就像它在生产环境中一样。随着时间的推移,需要做出调整以保持其与功能业务需求的一致性。

进一步阅读

一个可持续的信息系统 - 使用 SOA 对信息系统进行渐进式翻新,作者:皮埃尔·博内/让-米歇尔·德塔弗尼耶 - 多米尼克·瓦奎尔 - 赫尔墨斯 / 拉瓦锡 - 2007 年 11 月 - ISBN-13: 978-2746218291

第二部分:架构框架和方法

在第一部分,它相当理论化,因此即使没有技术背景的读者也能理解之后,本书的第二部分第二部分变得更加实用,并解释了如何将某些原则应用于信息系统的架构层面,以使其更加完善。这就是我们将讨论如何设计与业务良好对齐的低耦合服务的方法。将涵盖通用软件原则,但也会涉及更广泛的方法,例如授权管理和业务规则的外部化。在业务/IT 对齐的信息系统中管理数据的正确方法也将被详细阐述。

本部分包括以下章节:

  • 第六章SOLID 原则,从代码到系统

  • 第七章C4 和其他方法

  • 第八章面向服务的架构和 API

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

  • 第十章主数据管理

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

  • 第十二章业务规则的外部化

  • 第十三章授权的外部化

第六章:SOLID 原则,从代码到系统

从本章开始,我们将从理论部分转向,尽管我们还没有开始编码(这将在第十三章开始),但我们将开始将理论应用于设计由几个应用程序组成的小型信息系统。我们将分解不同的功能,展示它们如何帮助产生业务流程结果,并创建这些功能背后的软件。为此,我们将设计不同的组件和涉及服务的 API 合同,并思考数据应该如何设计和治理。在第十三章中,我们将使用所有这些设计阶段来实际实现样本信息系统。

当然,这个信息系统将在范围和复杂性上有所缩减,但这个练习已被设计成包括应该做出的大多数重要决策。你会发现严格的责任分离,过程和功能之间有良好的分离,通过 API 解耦服务,标准化合同,采用最佳实践方法,将软件堆栈适应所需的功能,软件和硬件之间的独立性,以及许多其他原则。

在本章中,我们将通过思考它应该公开的功能来开始设计我们的演示系统。为此,我们将使用SOLID 原则,将它们扩展到信息系统。SOLID 是由软件开发五个基本原则的首字母组成的缩写,这些原则如下:

  • 单一责任指出一个模块应该只做一件事

  • 开/闭区分了对进化的开放和对修改的封闭

  • Liskov 的原则解释了替换应该如何工作

  • 接口分离随后讨论了合同应该如何与业务功能紧密对齐

  • 最后,依赖倒置处理了耦合以及它应该如何进行,这与大多数情况下看似自然的方式相反

这些原则,通常应用于软件应用,实际上适用于每个软件系统,并且是设计它们不同模块的绝佳方式。因此,我们将使用它们来设计我们的样本信息系统。但首先,我们需要描述这个系统的业务需求。

描述样本信息系统需求

在进行任何分析之前,我们将想象系统所有者希望从系统中得到什么。当然,正如我们解释的那样,时间是信息系统设计中非常重要的约束条件,其生命周期很长,我们将模拟我们最初并不了解所有需求的事实。特别是,本书的最后一章将模拟信息系统的虚构公司出现新的需求,并解释系统将如何适应这些需求。这一点尤为重要,因为本书的主要目标是展示系统应该如何创建或适应,以便其随时间演变的简单性。

为了使练习尽可能真实,同时保持简单以便包含在一本书中,我们将想象公司、信息系统的用户、他们的业务、他们操作的数据等等。这就是我们将在本节中要做的事情。

公司及其业务

我们将称这家公司为DemoEditor,它将是一家与个人作者签订书籍写作合同并随后销售这些书籍的编辑公司。我们将想象这是一家相当小的公司(少于 50 人),并且其当前的信息系统极其简化,主要由一个标准的 Office 365 组织提供电子邮件功能、基本的 SharePoint 文档管理、一个外部的网站,以及大量通过 Excel 工作簿实现的内部功能。

尽管目前这种状况还算舒适,因为信息系统尚未因为长期积累的点对点互操作、遗留软件应用的退化等问题而变成一团乱麻,但它仍然显示出效率低下的迹象。Excel 工作簿的多个副本使得员工难以清晰地看到作者池和书籍写作的状态。此外,由于写作过程不统一,公司总监抱怨无法找到关于全球进度或书籍交付延误的清晰统计数据。

业务主要是寻找适合市场的主题,选择合适的作者,跟进书籍的写作,以及组织合适的销售流程。

信息系统的用户和参与者

50 人当中,大多数是图书编辑。接下来是销售团队,一点行政人员,以及总监。在这个简单的例子中,我们将考虑所有书籍的印刷和发行都外包给另一家公司,而 DemoEditor 只专注于编辑过程。

图书编辑的工作是寻找作者,寻找书籍主题,并将合适的作者与合适的书籍匹配。然后,他们跟进写作过程,确保质量达标。

然后,这取决于销售团队,他们的工作是寻找间接客户,这意味着图书馆或书店组织,因为 DemoEditor 并不直接向读者销售。这意味着商人实际上是通过数量而不是单位来销售书籍的,尽管在我们演示软件系统中我们不会过多地处理这部分,但在实际情况中这会很重要。

最后,总监需要通过销售团队和编辑提供的报告和统计数据来监控数字。公司的顺利运营在很大程度上取决于书籍的截止日期,正如写作质量、主题与作者的匹配以及读者和书店的期望一样。这意味着总监必须衡量所有这些指标,信息系统当然也预期提供这些信息。要求编辑或销售人员每周填写 Excel 表格是没有意义的,因为这会让他们失去实际工作的宝贵时间。

数据操作

如你所想,DemoEditor 的信息系统将不得不处理有关作者、书籍和销售的数据,以及从这些原始数据中提取的一些附加统计数据。作者将通过他们的身份、一些联系信息、可能还有支付版税的银行坐标以及他们的技能信息来识别。书籍将使用业务范围内的参考编号、标题、摘要以及其他关于内容的信息进行注册。销售基本上是向书店销售的书籍数量,以及相关的日期和可能的销售条件。

报告数据将是一切可以由总监用于销售、作者和书籍的业务智能的数据:每个类别销售了多少本书,销售的时间趋势如何,作者在销售放缓和新鲜感不再起作用之前可以就给定书籍完成多少版次,谁是他们的最佳销售员,哪个书店退回的书籍最少或重新订购得最快,等等。报告数据无疑是与时间相关的,这不仅是因为报告随时间演变,而且还因为报告应该显示业务在时间和地理上的动态。

信息系统对公司的重要性

DemoEditor 是一家小型公司,这意味着员工可以“填补空白”并做一点任何事情。虽然这在某些情况下是一个优势,意味着他们敏捷且适应性良好,但它也意味着他们不太倾向于以工业化和可重复的方式做事。电子表格可能会被复制并在公司不同版本中传播,而不是在网络上保持一个独特的参考版本。此外,数据销售被分散到不同的销售人员手中,因为他们往往相互竞争,因此很难统一数量折扣(价格是固定的)以及客户名单。

由于商业管道并不非常正式,一些潜在客户在没有销售人员真正能够提供关于花了多长时间和多少努力才能将潜在客户转化为客户的统计数据的情况下,就变成了客户。总监在不知道如果他们雇佣更多的销售人员公司是否会卖出更多产品时,确实很难。为合适的书籍选择作者也是一个问题。一般来说,编辑对市场有很好的把握,并且非常清楚哪些主题应该被撰写。但是,合格的作者池相当有限,而且作者们大多因他们已经写过的书而知名。大多数时候,编辑不知道这些专家还了解哪些其他技术,而且有时候,在花费了大量时间寻找作者之后,一本书被签约给了一个新作者,结果编辑几周后才发现,实际上有一位已经为 DemoEditor 写过几本书的优秀作者实际上具备新项目的正确技能。对于 DemoEditor 来说,一个更新、共享和高效的知识库对于作者能力至关重要。

负责改进系统的人看到的状况

经理要求你过来帮助处理信息系统。公司里的每个人都清楚 IT 可以更加高效并帮助他们更好地工作,但他们说,他们并不是 IT 方面的专家。由于没有内部 IT 人员,他们尽力而为,但他们意识到小型公司的“自己动手”精神只能走这么远,他们不得不找人来提供一些结构。经理也担心在完成这项工作之前增加公司规模;否则,这可能会带来更多问题而不是增长。

随着业务流程的进行和预算不可扩展,你被分配的任务是“在汽车行驶时更换轮胎”。IT 系统可能会有一些短暂的停顿,但不会持续很长时间。数据必须被清理,但作者和书籍的数据库必须在过程中保持可用,因为它们是公司大多数员工日常工具。经理并不太关心报告不可用或甚至被破坏,因为目前它并不很有用,而且大多数数字本来就不准确。

在接下来的章节中,我们将置身于一个被要求执行这项基础任务并设计更新后的 IT 系统不同组件的工程师的角色,并决定其服务应该如何运行以及应该设计哪些业务领域。之后,我们将实施所有这些,并逐步转型信息系统。但到目前为止,我们必须将前几章学到的理论转化为将指导我们前进的原则。SOLID 原则是一套非常适合的原则。

SOLID 原则及其在任意规模系统中的应用

SOLID 原则是适用于软件应用的重要原则,但它们也恰好非常适合软件系统的一般应用,因此我们可以使用它们来构建我们的项目。我们将逐一解释这五个原则,以及它们如何应用于 DemoEditor 要求的转型和其新信息系统的设计。由于这是一本关于信息系统的书,而不是关于软件开发的书,尽管我们最终会构建一些实现,但我将不会从编码的角度描述这些原则,而只是简要地介绍它们的主要思想和,更详细地,它们在系统设计中的转化。

单一职责原则

这个原则指出,一个类,或者在我们的情况下,一个信息系统模块,应该只做一件事,并且只做一件事。这个定义相当宽泛,但可以通过指出一个实体应该只有一个业务理由来改变来稍微缩小范围。如果同一个类在作者管理和书籍管理发生变化时应该升级,那么这与单一职责原则有关,并且这个类应该被分解成至少两个更小的类。

这个原则显然很容易翻译到适用于信息系统的场景中,它直接应用于模块,无论是服务、组件还是整个系统的其他部分(我们将在下一章回到粒度管理的话题)。系统中的每个实体都应该只做一件事,并且只做一件事。如果从组成系统的软件应用的角度来看,这意味着每个应用都应该负责系统的一个业务领域。由于我们管理作者、书籍、销售等等,我们确实应该为这些找到各自的应用。这个业务领域的概念现在还不够精确,但同样,我们将在未来的章节中回到这个话题,即第九章,详细阐述领域驱动设计方法和领域以及边界上下文的概念。

目前,我们只需同意一个业务领域需要其自己的应用程序。如果你在考虑微服务,是的,我们将遵循这条路线,但请耐心等待,因为这个“微”的限定条件并不总是必要的,我们更愿意谈论“服务”(在第八章中有更清晰的定义)。

这个首要原则听起来可能非常简单(它的表述确实如此),但其含义可能非常深远。为了给出我们将在示例应用程序中必须处理的复杂性的一个例子,让我们展开一个服务依赖于另一个服务的案例,比如“书籍作者”的关系。正如之前所说,作者和书籍管理是两个不同的责任。但我们应该如何处理两者之间的关系?是另一个服务吗?无论如何,当有人从服务中读取书籍实体时,应该如何检索和显示书籍的作者?我们可以反过来问相同的问题:如果有人调用特定作者的 API,我们应该如何显示这位作者参与编写的书籍列表?

深入探讨这个最后场景有助于更好地理解责任的概念。想象有两个独立的服务,每个服务都有自己的数据库,因为它们应该独立。现在,一个用户使用GET请求调用特定书籍的 API,比如说https://demoeditor.org/api/books/123456。模块确实负责发送书籍标题、ISBN/EAN 号码和书籍的一些其他属性。那么关于作者的信息呢?这就是责任原则帮助划清界限的地方。编辑们会告诉你,大多数时候,当他们获取书籍信息时,他们需要知道作者,但只需要他们的标识符和一些主要数据,比如他们的首字母和姓氏。这是书籍服务的责任。如果你再次询问你的产品所有者(编辑们,因为他们将是使用信息系统的人),并且他们需要更多数据,他们会转向/api/authors服务来获取,当然使用/api/books服务提供的初始答案中的标识符。因此,这些数据是第二个服务唯一的责任。

每个了解良好数据库设计原则的读者可能已经感到窒息了,考虑到这种方法需要数据重复。确实,由于/api/authors负责作者的全部数据,包括当然的首字母和姓氏,这意味着如果/api/books负责在请求时提供给定书籍的标识符、首字母和姓氏,那么非重复规则就被打破了!而且这正是责任概念有趣且应该深入挖掘的地方。我们考虑以下责任分配如何?

/api/authors 服务负责提供关于作者的始终是最新的数据,包括他们的名和姓。这意味着它是作者的真实参考来源:任何需要关于作者最新、最佳信息的人都应该转向这个服务,该服务将负责及时提供这些信息。由于它是这些数据的参考,该服务当然会提供带有价值日期的数据,因为数据可能会随时间变化。例如,作者结婚后可能会更改他们的姓氏;作者服务应该跟踪这一点,因为它负责作者及其数据完整性。

/api/books 服务负责为书籍提供相同的服务,这意味着在书名、标识符等方面有相同的服务水平。但当谈到一本书的作者时,这涉及到另一个参考服务的关系,因此它所负责的服务数据仅仅是指出在另一个服务中的正确实体。这提出了两个有趣的问题。

第一个问题是功能性的:链接应该是简单地指向一个特定的作者,还是应该指向作者在特定时间点的值?这需要回答一些业务规则:如果作者在第一版和第二版之间结婚,书籍上出现的作者名字应该改变吗?如果是这样,如何调整注册的版权?对于书籍原始版本的简单重印,情况是否相同?

第二个问题是更技术性的:如果书籍服务存储了作者服务的链接,而后者在需要时不可用,会发生什么?如果“通常”的数据(名和姓)已经在书籍服务中存储,那么没有问题,因为它现在与第二个服务是独立的。但这又回到了功能问题:如果作者结婚后书籍上的名字不应该改变,那么没问题,甚至更好,因为本地副本将防止在具有检索“旧”数据的日期时难以访问作者服务。如果名字应该演变,可能最好是暂时退回到旧名字,而不是仅仅提供机器可读的标识符;只有编辑者才会知道……

我希望这个例子,尽管复杂,已经向你展示了我们所说的“责任”一词的含义。诚然,这很复杂,但请记住,我们谈论的一切都与商业复杂性相关,并非偶然。确实,谈论价值日期和参考数据管理中历史的重要性可能听起来过于复杂,因为这在当前的信息系统中并不常见。但这确实是一个真实的问题,而且这种对实际功能现实的缺乏反思也是一个问题,因为它阻止了对所有业务规则的反思!这并不意味着基于这种反思构建的软件将考虑到所有这些复杂性。在真正的敏捷方式中,你肯定会从非常简单的东西开始。但这个对功能复杂性的深入理解确保了软件在未来将易于演化,而且你不会在某个实施点上卡住,因为软件与业务不匹配。

开放/封闭原则

开放/封闭原则在其表达方式上本身就包含了一个悖论,这使得它一开始看起来很奇怪:一个模块怎么可能同时是开放的和封闭的?理解这个原则对于创建将不断演化的系统非常重要,因为它指出了什么应该保持封闭,什么应该开放以改变,以便这种演化尽可能顺利地进行。

当应用于面向对象编程时,开放/封闭原则指出,一个类应该对扩展开放,但对修改封闭。封装和私有成员用于防止程序中的任何实例直接修改另一个实例的状态;否则,在执行程序时跟踪发生的事情将非常困难。如果没有办法跟踪哪个类修改了另一个类的状态,那么调试也会变得复杂。这就是为什么一个类保持其成员私有,并且只开放一些公共函数,以允许对其状态的某些更改,这种方式由其自己的代码控制,遵循其自己的规则。这是原则的封闭部分。

但通常情况下,一个类不会被标记为final以允许另一个类从它继承并专门化标记为virtual的功能。继承的类还可以添加一些数据成员,除了访问标记为protected(或者当然,public)的从继承类之外。再次强调,类控制着什么可以被覆盖,什么不能,但至少它对另一个类扩展其行为是开放的。这是原则的开放部分。

将此原则应用于信息系统并不意味着在反思上发生重大变化,因为服务替换了类,扩展或保护的技术仅从实际观点来看有所不同。如果我们继续以服务是 REST API 的例子为例,我们可以在类的成员和 API 合同实现持久化的数据之间建立平行关系:只有服务可以修改这些数据,因为只有实现可以访问持久化使用的(数据库或其他)。当然,一些 API 方法可能允许从 API 客户端传递一些特定的修改,但 API 实现控制这一点,并应用业务规则以确保修改按其意愿进行(或者,顺便说一句,可能被拒绝)。这是原则在信息系统模块中的应用的封闭部分,它在与类应用中的相似性方面非常明显。

在 API 上实现开放原则的部分更为复杂,因为 API 的行为可以通过多种方式扩展:

  • 实现这一目标的一种方法是通过创建一个将扩展初始 API 合同的 API。OpenAPI 语法中存在实现多态的机制,也可以以这种方式聚合类型,使得新的类型包含初始类型及其所有内容。

  • 另一种方法是创建一个 API,它替换了旧实现的展示,但仍然依赖于所有标准数据,并在此基础上提供自己的数据。如果做得仔细,扩展后的 API 甚至可能与初始 API 合同完全兼容,因为它只添加了新的数据(如果它只是传递初始数据而不改变任何行为,甚至符合我们很快将看到的 Liskov 替换原则)。

  • 第三种选择是使用 API 网关来公开更新的合同,并实现来自原始 API 和来自另一个服务的附加数据的混合,该服务专门用于其存储和处理。这种方法在继承原则上更接近。

这三种方法可以概括如下:

图 6.1 – API 的开放/封闭方法

图 6.1 – API 的开放/封闭方法

Liskov 替换原则

Display接口将实现一个print函数,该函数在白色纸张上打印黑色文本(通过打印机、屏幕或其他任何东西;这里不重要)。现在假设有一个名为ColorDisplay的类专门化Display类,并再次提出一个新的函数签名print,但接受一个名为color的参数,允许用户指定他们想要的任何颜色。无参数的print函数应该如何表现?继承的类肯定会指向其新的改进后的print函数。在这种情况下,应该将什么默认颜色传递给这个函数?如果你回答“当然是黑色”,你就知道 Liskov 替换原则是关于不让类的用户感到惊讶,并确保他们知道行为是预期的。

对于模块化信息系统中的服务也是如此。再次,我们将使用 API,因为这是在这种系统中分解模块的标准方式。当你得到一个 API 合约时,它声明了可以调用哪些方法,使用哪些动词,以及通过哪个 URL;它还声明了你可以发送或接收的属性的准确名称。但你完全可以尊重 API 合约的字面意思而不尊重其精神,这正是 Liskov 替换原则所涉及的。

让我们将之前面向对象编程的示例翻译成 API 服务,并想象一下,有人可以使用/api/print的 1.0 版本来处理将被发送到设备上的黑色文本。如果使用/api/print API 的 1.1 版本,并且支持/api/print?color=[HEX-VALUE],我们肯定会期望将我们的旧客户端指向新的 API 会导致黑色文本的产生。在 API 合约与其实现之间的流中,这可以描述如下:

图 6.2 – Liskov 替换原则

图 6.2 – Liskov 替换原则

接口分离原则

Rectangle类实现了IShape接口。这意味着Rectangle必须实现getSurfacegetPerimeter方法,还必须实现一些方法,如drawShape等。如果程序中唯一有趣的事情是计算形状的数学特性,那么正确的方法是将IShape拆分为IGeometricalShapeIDrawableShape(即分离接口),以便类只实现它们需要的接口。

对于 API 合约也是如此。回到我们的图书服务,最好将两个合约分开,一个用于图书特性,另一个用于图书销售特性,即使相同的实现会公开两个接口,一个位于/api/books之后,另一个位于/api/books/sales之后,而不是通过仅使用一个合约来强制实现所有函数。

虽然总是可能(并且在 API 中比在类中更可接受)以NotImplemented消息响应,但将接口分为两个也使得引入版本更容易。如果销售接口在 1.0 版本中没有正确定义,并且需要进行重大的、向后兼容的重写,那么将能够在不进行任何更改的情况下继续暴露书籍的特性(可能被大多数客户端使用)。

注意

向后兼容性是 API 新版本的质量,其中所有在先前版本中使用的调用在新版本上工作结果完全相同。

接口隔离甚至使得暴露三个 API 合约成为可能(并且很容易),即/api/books/api/books/sales/api/books/sales/v2

图 6.3 – 接口隔离

图 6.3 – 接口隔离

虽然在解释中这一点相当明显,但遗憾的是,这个原则在现有的 API 中经常被遗忘。这尤其是因为编辑倾向于在从实现生成 OpenAPI 合约的框架中提供函数,而不是遵循合约优先的方法(虽然这确实更复杂,但这是唯一能够产生正确、与业务对齐的 API 的方法)。由于合约是从整个源代码自动生成的,生成器不会区分不同的方法和资源,产生一个单一、不可分割的合约,该合约不遵守接口隔离原则。

依赖倒置方法

依赖倒置方法之所以被称为如此,是因为它与传统关于依赖的思考方式相反。想象一下,我们有一个报告模块和另一个提供用于报告目的的数据的模块。自然地,我们倾向于认为从功能角度来看,报告模块应该在技术上依赖于数据模块。这是一个罕见的情况,其中将技术设计直接与功能概念对齐并不足够好。如果我们想确保低耦合,我们必须再迈出一步,向两个服务都将依赖的共同接口添加一些间接性:

  • 就像Data类会实现IData接口一样,/api/data服务应该实现数据 OpenAPI 文件中定义的 API 合约。

  • 就像Reporting类会使用IData源(而不是直接使用Data实现)来防止硬耦合一样,/api/reporting服务不会直接调用/api/data服务,而是调用配置提供的 URL,前提是它实现了数据 OpenAPI 合约。在面向对象编程中,这通常通过注入来完成。在服务中,根据编排机制,这可以通过 API 网关、入口暴露或甚至(更复杂的)服务网格来实现。

下面的图已经用于第三章,但它在这里特别相关,因为它直观地展示了信息系统中的依赖倒置原则:不是软件模块之间的依赖,而是两个模块共同工作,指向同一个业务合同定义(纯粹是功能性的),一个来实现它,另一个来消费它:

图 6.4 – 四层耦合图

图 6.4 – 四层耦合图

让我们继续到下一节,我们将分析 SOLID 原则。

对 SOLID 原则的批判性分析

虽然我及时意识到(当然像许多人一样)SOLID 原则几乎适用于信息系统,就像适用于面向对象的类和接口一样,但有些部分值得讨论。确实,对于任何其他原则,严格的、不考虑周全的应用可能会导致问题。即使原则在特定情境中适用得很好,它们也可能有强烈的副作用,最终使它们比有帮助更有害。必要的谨慎方法导致了大量的争论,你必须小心处理这些原则。

职责分离的局限性

第一个(也许是最重要的)原则,即职责分离原则,有时很难应用——在某些情况下可能甚至不可能——应用到服务或应用的高级模块中。对于类来说,一旦编译完成,总是有可能在不影响整个应用的情况下分解一个类。服务和模块并不提供这种易于组合的特性,因为它们附带额外的约束,如暴露、端点、编码接口、文档、集成子系统等。所有这些都对分解产生了影响,这也是为什么那些不谨慎对待微服务并在系统中过度分解它们的人最终花费更多时间在它们的演变上,而不是如果他们坚持使用单体架构的话。我不会再次谈论粒度,因为这个问题已经在之前的章节中讨论过了,但这是显然需要深思的问题,可能会限制职责分离原则的适用范围,或者至少是其深度。

另一个困难与你能将责任分离得多深无关,而是与某些责任有时多么复杂有关,即使在相对较高的层面上也是如此。例如,这是“微前端”架构中的一个巨大困难,我并不是在谈论与前端组件“微”相关的那个问题。简单的事实是,视觉组件不仅呈现功能,还具有视觉影响,这在使它们独立方面是一个巨大的困难。例如,在jonhilton.net/good-blazor-components/中暴露了这一点:通过将 Blazor 组件的内容委托给另一个子组件(在文章的例子中,使用ChildContext属性包含一个Card实例),你确实将显示视觉组件内部部分的责任外部化了。但这并不意味着它不会产生影响,因为子组件的渲染必然会影响其上方的组件。事实上,容器要么为子组件固定一个大小并承担其显示的责任,要么以纯响应式设计的方式让子组件调整其显示。在这种情况下,它自己的大小将受到其子组件的影响,这也破坏了通过责任分离原则本应实现的独立性。

对于这个问题,智力解决方案是考虑每个组件对其内容的负责,但不负责其显示,而这是浏览器中显示引擎的责任。这可能并不令人非常满意,因为它回到了整个前端的单一点执行,这是我们试图使其模块化和易于演进的目标,但至少在有限的范围内实现了组件之间的低耦合。这是在设计信息系统和模块化应用程序时必须做出的许多妥协之一。

关于单体架构的争论

本章可能是讨论关于“回归单体架构”这一长期争论的好地方,这是对在软件架构社区讨论中观察到的微服务缺点的一种反应。总的来说,微服务方法被认为是有害的,有些人建议回归单体应用程序。这遗憾的是又是一个当今极端辩论的例子,因为这两种方法(以及它们之间巨大的范围)都有价值,这取决于你的需求。

没有哪个严肃的人曾经假装微服务是适合所有架构的最佳解决方案。事实上,从一开始,大多数解释这种方法的文章就坚持认为,它们只适用于某些特定的环境(高流量、频繁修改应用程序、负责不同模块的团队之间有明确的界限等等)。然而,有些人没有考虑到这一点,现在抱怨微服务并不适合他们的案例。更糟糕的是,在一种二元且不太深思熟虑的反应中,他们摒弃了微服务的整个原则,并宣传所谓的“回归单体”。在这种情况下,任何理智的工程师都会简单地发现一个众所周知的摆动运动,其中一种极端追逐另一种极端。在这种情况下,解决方案既不在其中任何一个,也不在两者之间不断平衡,而只是在它们之间达到一个美好的平衡。

在软件应用程序的情况下,我们称什么为单体?一个由单个进程组成的程序?不,否则这意味着即使是像cdexit这样的最小应用程序也应该被视为这样。这个术语正是用来描述那些负责太多业务功能、变得过于沉重以至于无法良好演变或适应其设计用途的单个应用程序。

关于所谓微服务架构的死亡和回归单体的文章中讨论的情况,根本就没有涉及到正确的话题,那就是服务的粒度。当然,微服务的非常小的粒度并没有满足他们的信息系统需求。但这仅仅意味着粒度太细。回到最粗的粒度(单体)只是显示了对于问题的理解深度不足。工程师处理这个问题的方式是寻找服务正确的粒度。正如codeopinion.com/biggest-scam-in-software-dev-best-practices/中所述,选择不仅仅是在“亚马逊在做,所以我们也做”和“我们不是亚马逊,所以我们不应该做”之间——对业务和上下文的分析知识才会告诉你你应该如何在这两者之间定位自己。

恰好存在一种称为领域驱动设计的设计方法,这是找到信息系统服务正确粒度的完美方法。它也与业务/IT 对齐的原则密切相关,因为它说明了业务领域如何被切割成内部紧密耦合、外部低耦合的模块。我们在我们的演示应用程序设计中所做的一切,都将是对这种正确粒度研究的说明,基于业务功能需求。

小心无意中的耦合

最后关于职责分离的一些建议(这个首要原则无疑引发了众多讨论!):一旦你根据业务能力图明确划分了职责,职责分离的工作并未完成,因为在功能的技术转型过程中,很容易重新创建耦合。

我将给出一个此类问题的简单例子,因为它非常典型地代表了我在描述的陷阱。想象一下,我们创建了一个演示应用程序,其中包含一个用于书籍的 API,一个用于书籍销售的 API(可能位于相同的 URL 下,但在合同层面上仍然是独立的),以及一个用于作者的 API。公司总监可能在某个时候想要一份关于作者的地域起源如何影响其销售本地化的报告(也许布列塔尼的作者在其地区销售会更好,因为他们的联系网络更密集?或者可能没有影响?无论如何,这可能值得分析)。这是微服务中常见的问题,微服务本应拥有自己的持久性。在这种情况下,我们如何建立数据之间的联系,就像我们在添加单个数据库时创建join操作一样?一个标准的方法是添加一个收集数据结构,它从所有微服务中收集、索引和汇总数据,并提议一个具有自己 API 合同的专用/api/reporting服务。

当然,这个服务对其源存在一定程度的耦合,但这可以通过保持本地缓存或混合订阅数据变化的方法与较低频率的直接收集相结合来降低耦合级别,以确保没有信号丢失,并且数据在可控的频率下重置。此外,它还提供了原子数据服务中没有的有趣功能,例如索引、动态聚合能力等。GraphQL是一个很好的协议来公开此类服务,并且它们与命令查询职责分离架构集成得非常好。

然而,如果原子服务不仅为这个报告服务提供数据,而且开始从它那里消费原子数据,那么可能会出现意外的耦合。这可能会很快发生,因为索引引擎的性能提升对开发者来说非常有吸引力。问题是这导致了循环功能依赖,这本身就是一个相当大的问题,而且耦合程度更高,因为一旦这种依赖关系建立,原子数据服务就会突然与每个其他服务耦合。当然,耦合可能仍然很低,但无论如何,已经建立了一个联系,这在大多数情况下——我所见到的——都是进入系统的一个真正的痛点。

如果你真的需要在原子服务中进行快速的聚合读取,你必须通过在专门的持久性上添加索引等方式内部处理。如果你看到这个漂亮的索引引擎就在服务墙的另一边,这可能会听起来很复杂,但这是为了保持你的系统不受耦合影响并且易于随时间进化而必须付出的代价。再次强调,这是一个妥协,如果架构师认为功能耦合不是问题,你可以通过这样做来节省时间。但这是一个有意识的(并且有文档记录的)妥协。

图 6.5 – 降低耦合强度

图 6.5 – 降低耦合强度

在回到演示之前总结

在本章中,我们学习了可以应用于信息系统(而不仅仅是面向对象编程,因为它们最初的目标是)的 SOLID 原则;单一职责原则和接口隔离原则已被用来开始定义我们演示应用程序所需的不同的 API 合约。开放/封闭原则将帮助保持这个 API 语法的可进化性,并且其进化必须遵循 Liskov 替换原则,以便系统能够令人满意地进化。最后,依赖倒置已被证明是合同优先 API 的核心原则,以及能够将软件实现与面向业务的功能对齐,这是我们在这本书中寻求的主要目标。

在下一章中,我们将进一步设计我们的演示应用程序,通过定义其不同的组件。这将帮助我们描绘出我们想要在这本书中创建的第一版的大致轮廓,同时也能更清晰地定义应用程序的不同部分,使用一种适应分解深度的方法。

第七章:C4 和其他方法

在上一章中,我们提供了一个关于我们将要构建的示例信息系统(以及应用程序软件模块)的商业目标的初步想法(作为一个实际练习来遵循本书中概述的原则)。谈到原则,在介绍演示系统时,我们还解释了面向对象编程中最重要的原则如何应用于我们系统的设计。

现在已经确定了高级原则,是时候深入理解我们的示例信息系统了,我们将利用这一点来展示专业人士使用的一些不同方法。正如我们将看到的,存在许多架构方法和软件表示,它们在许多方面都有重叠。事实上,其中一些非常接近,以至于它们实际上是作者标记他们个人方法的一种方式,而不是为架构工具带来额外的价值。这与 JavaScript 生态系统中的情况相同,那里有太多的框架。此外,每天都会变得更糟,因为每天都有新的团队认为他们可以通过提出另一个解决方案来解决问题。同样的事情也发生在架构方法上,我们最近被清洁架构六边形架构洋葱架构等等所淹没。

由于选择一种方法而不是另一种方法实际上并不重要,我们只会快速展示这些方法,以便您可以选择哪种方法更适合您的思维方式,或者简单地从许多方法中混合一些有趣的片段,因为这些只是一些众所周知的最优实践的新组合。在我们的示例信息系统的情况下,C4 方法听起来很有趣,可以提供一个如何设计它的初步方法,因此我将重点关注这种方法,以提供更多细节。C4 架构框架背后的理念是,从上下文级别到容器级别,然后是组件级别,最后是代码级别,关注 IT 系统。我们将随着在示例系统中应用它们来解释这四个级别。

就像在上一章中提到的SOLID 原则一样,我的目标不是深入解释这些方法,因为如果已经在免费和高质量的资源中提供了更详细的解释,那么写作就没有价值了。我的意图是提取这些方法如何有助于达到商业/IT 一致性,或者至少帮助表示与这个主题相关的问题。

在本章中,我们将学习以下主题:

  • C4 方法

  • 清洁架构

  • 六边形架构

  • 洋葱架构

C4 方法

C4 方法涉及设计一个系统(或应用程序),具有四个不同粒度级别,从应用程序所处的上下文级别开始,到将展示应用程序不同过程的容器级别,再到展示组成过程的各个部分的组件层,最后到代码级别,在那里我们将找到用于创建应用程序的类和接口。以下是该方法的示意图:

图 7.1 – C4 方法中的级别

图 7.1 – C4 方法中的级别

每个级别都详细说明了上一级的内容,并提供了更详细的应用视图。事实上,低级别的单元总是必须与上层的一个给定单元相关联,这也帮助正确地分担责任并保持低耦合度。

在接下来的章节中,我们将为我们的示例系统和软件模块绘制四个级别,以展示方法;同时,我们将开始设计我们的演示,比上一章允许的业务需求更深入。请注意,以下图示不一定是书中最后创建的系统所对应的精确图示;我真的很想展示完整和真实的设计过程,因此我会在设计样本应用程序的同时绘制这些图示。在我对最终结果有一个清晰和完整的看法之后再绘制这些图示——在我看来——不会那么具有教育意义,而且在某种程度上,我会感觉像是在作弊。

上下文级别

在上下文级别上,我们的系统将由之前详细说明的三个用户配置文件使用,即编辑者、销售人员以及 DemoEditor 的总监。我们将考虑用户列表已经存在于信息系统中的 Active Directory 或类似系统中。对于内容管理系统,我们将考虑它已经存在用于二进制文档管理。最后,我们还将考虑在整个系统中也提供了一个负责电子邮件发送的模块。它用于向系统中的参与者发送电子邮件,无论是验证某些数据还是通知他们某些事情。当然,作者也会在上下文中表示,即使他们没有直接使用内部信息系统。以下是上下文级别的示意图:

图 7.2 – 上下文级别图

图 7.2 – 上下文级别图

我们可以对箭头的内容进行更详细的说明,但到目前为止,这个图示是自解释的,不同角色使用的功能在上一章中已经解释过了。我们在这个图示中最感兴趣的中心是Demo Editor System框,这是我们将在 C4 架构的下一级别详细说明的。

容器级别

容器级别,我们展示了系统在构建块方面的使用情况。大多数情况下,这些块是分离的过程。在我们的案例中,由于我们决定拥有完全独立的服务,我们将拥有与 API 展示服务器一样多的容器。我稍微提前透露一下本书的技术章节,但我们将以 Docker 容器的形式部署系统,因此 C4 架构中的“容器”与 Docker 或其他类似技术中使用的部署单元的“容器”之间有一个完全匹配。

在下面的图中,虚线矩形内的框代表与 Demo Editor 系统对应的更高级别框的详细信息。我们仍然可以表示上下文级别的其他部分,但最佳实践是不详细说明它们,因为它们超出了我们的研究范围。我们可以详细说明流向它们的流(就像我们对用户目录箭头所做的那样,指向单页应用程序),但这不是强制的,如果我们还不想精确地描述这些数据流,我们也可以保留从整个系统指向的箭头(这就是邮件发送者和 CMS 子系统所做的那样)。

图 7.3 – 容器级别

图 7.3 – 容器级别

顺便说一下,在这个反思层面,我们开始考虑数据流的内容可能是什么,在这个特定的情况下,我意识到,与其使用 CMS,电子文档管理可能更为合适。如果我们考虑这是一个外部系统,而不是我们正在设计的系统的一部分,那么实际上我们可能没有能力去改变它。然而,由于它与它有交互,我们可能能够影响与其通信所使用的协议,这正是我们的分析所关注的。从实际的角度来看,我在写作的时候还不知道(记住,我希望尽可能现实,我在写章节之前还没有完成练习),是否会有一个 Alfresco 容器、一个 Azure 存储,或者一个 SharePoint 365 网站来实现这个子系统。我所知道的是,我需要传递二进制文档并检索它们,这意味着像内容管理互操作性服务CMIS)这样的流肯定会被指示(CMIS 是电子文档的互操作性标准)。

目前,我们还没有表达每个框内部的内容。我们只知道每个框都将作为软件应用拥有自己的生命周期,并代表一个过程和一个 Docker 容器(确实是一个好的实践,即每个容器只包含一个进程,以便所有事物都能很好地对齐)。如果我们稍微深入到开发细节中,那么为每个这些容器设置一个持续集成管道,以及逻辑上为每个容器设置一个 git 仓库,将是正确的。但我正在预测这本书的“动手”部分。现在,让我们深入到一个框中,即 书籍存储库,并展示它将如何使用 C4 方法中的第三级,即组件图来组成。

组件级别

组件级别,我们展示了可执行容器将使用哪些不同的模块和库来完成其业务需求。由于我们的书籍库将公开两个合同 API,我们需要一个 Web 服务器来支持控制器,一个客户端来调用持久化系统,可能还会与存储库模式一起使用,一个缓存组件来本地保存一些数据,等等。这就是以下图表所展示的内容,对应于 C4 架构的第三级:

图 7.4 – 组件级别

图 7.4 – 组件级别

一个组件可能对应于不同平台上的不同工件名称。在 .NET 中,组件可能是程序集,而在 Java 中,它们将是 JAR 存档。无论如何,这种组件分解在基本上所有编程平台中都存在,即使它们以不同的形式出现。例如,在某些平台上,组件将对应于单独的文件,而在其他平台上,它们将更加抽象,例如通过使用命名空间。无论如何,都存在将代码级别内容分组为连贯组的方法,同时仍然比更高层级的容器具有更低的粒度,这些容器对应于由多个组件组成的整个过程。

代码级别

正如 C4 方法(可在 c4model.com/ 获取)的参考文档所述,只有在它们能增加价值的情况下才应使用图表。这也是我在介绍 IDbRepository 模式将被使用,也许还会使用 UnitOfWork 模式时强调的,但类组织结构尚不明确,因此现在尝试绘制图表是没有意义的。

此外,即使是为了教学目的,绘制这样的图表也不会带来太多价值,因为 C4 方法的第四级实际上与UML统一建模语言中的类图相同。在接下来的章节中,如果我们需要,我们将使用这些详细图表,一旦我们进入实际制作系统的阶段。与此同时,我们已经完成了用于解释我们想要创建的示例系统的 C4 方法,现在我们将简要讨论其他软件架构设计方法,使用它们更好地表达我们正在分析的环境。

清洁架构、六边形架构和其他框架

在软件架构的背景下,接下来可能对你来说是个惊喜——选择哪种架构方法并不那么重要,只要你有一个方法(或者几个方法一起使用,顺便说一句,如果你能控制它们之间的交互并知道何时选择一个而不是另一个)。当然,这并不意味着在设计信息系统架构时你不需要应用某种方法。我在这个领域多年经验后的观点是,你应该找到并应用你自己的方法。

此外,如果你尝试了许多不同的现有方法,如六边形架构、清洁架构、洋葱架构以及其他以领域为中心的方法,你很快就会意识到,它们提供的不过是将相同的基本原则以不同的视觉方式呈现,这些基本原则与我们在这本书开头讨论的业务/IT 对齐以及从 SOLID 方法中提取的原则高度相关,即以下内容:

  • 商业模式(代表功能概念的数据结构转换成代码的业务规则**)应该是架构的核心。这样,它不依赖于任何东西,这允许轻松更改、快速自动化测试、缺乏外部版本控制约束,以及许多其他优势。最重要的是,它使团队能够专注于最重要的事情——业务对齐。

  • 围绕这个业务核心的一切都应该使用某种间接方法来引入低耦合,使系统的任何模块的演进都更容易。一切皆依赖于业务核心模块,而它本身不依赖于任何其他东西。此外,如果需要,这些依赖关系很容易修改。

技术架构模式

在我们深入探讨方法细节之前,还有最后一件事——在这里,我将只讨论可以应用于整个信息系统设计的架构方法。有许多方法涉及单个应用程序的技术架构,它应该如何结构化,其源代码和信息流应该如何组织,应该存在哪些技术层,甚至在某些情况下,如何命名事物的建议。

例如,N 层架构(也称为基于层的架构)将描述每一组源代码应该如何调用另一组,从 GUI 到服务再到应用程序再到数据库。MVC、MVP 和 MVVM 架构稍微不那么线性,描述了每一组源代码将只与其中的一些进行通信,以简化应用程序的演变和维护(通常侧重于它们的视觉部分)。

这些架构理论上可以在整个信息系统的层面上使用,但这并不意味着更多,因为它们并没有说很多关于应用程序应该如何协同工作。N 层架构建议软件块应该通过应用层相互通信,但由于它没有强制执行,许多旧应用程序在数据库级别进行互操作,这在大多数情况下都是灾难性的耦合,也是当今绝大多数遗留系统无法演变的主要原因之一。MVC 应该适用于比仅仅视觉界面更高的层面,但尽管如此,它并没有达到整个信息系统的层面。这可能只是一个语义问题,但我个人倾向于将这些方法归类为模式而不是架构,因为它们来自经验而不是纯粹的反思(这里绝对没有价值判断——我们需要实验和智力方法来完整)。

由于这本书是关于整个信息系统架构的,我专注于适用于这种级别的架构方法的目的。恰好领域中心的方法通常对此作出回应,因为它们传达了业务领域的概念,如果你想要得到与业务一致的结果,这必须是第一个(也是唯一的)分解信息系统的途径。所有技术方法都可以在软件和硬件层上工作,但我们需要的是在业务能力映射层上工作的方法。

洋葱架构

洋葱架构将软件单元描绘为几个同心圆,中心正是业务领域模型!围绕它,你会找到服务(持久化等)然后是展示层(GUI、外部接口、测试等):

图 7.5 – 洋葱架构

图 7.5 – 洋葱架构

在洋葱架构中,一条主要规则是所有依赖都应该从外侧流向内侧层。这样,核心业务模型就不依赖于任何东西,这使得它容易进化,而外侧圈可以改变,前提是它们不影响业务(这通常比反过来更容易 – 想象一下,如果你的整个业务模型基于专有关系型数据库过程语言,并试图切换到另一个数据库引擎)。

虽然这种方法不坚持间接层的级别,但依赖反转原则(见前一章的解释)以及刚刚提到的规则使得接口对于实现持久化等功能是必要的。当然,对于更高一层也是如此。

六边形架构

六边形架构代表一个以六边形形状的软件单元,因此得名,其中六边形的左侧包含消耗内部(如 GUI 或自动化测试)的接口,右侧包含适配器以处理功能依赖(如持久化、通知服务等),而中间部分,正如你可能猜到的,是核心,包含业务实体和业务规则!

图 7.6 – 六边形架构

图 7.6 – 六边形架构

六边形架构强调端口和适配器来组织应用程序不同部分之间的通信,但这也是洋葱架构的一条规则,即使它没有以相同的方式展示。这有助于我们记住,接口和合同应该始终应用于依赖,以防止它们演变成硬耦合,这使得系统难以进化。

就“六边形”形状而言,这只是纯粹的杂乱!我的意图是画出业务域的三个“客户端”来强调方法名称与给定的技术约束或技术优势之间绝对没有关系。实际上,方法中解释说,六边形形状只是用来保留足够的空间来显示围绕中心形状的形状。这与圆圈(如之前提到的其他方法所用的)、椭圆形或正方形有何不同?如果六边形形状在概念上没有带来任何丰富性,那么我个人认为,它不应该出现在方法名称中。

这可能听起来像是对方法的无用抱怨,但这只是为了明确,它们的真正价值在于它们共同拥有的东西,而不是它们的差异。真正重要的是核心域的中心性,与业务对齐,以及依赖的控制,但所有这些已经在 SOLID 原则中存在。

清洁架构

清洁架构基本上是将之前提出的两种方法结合起来,同时提出一些额外的规则。从图形上看,它非常类似于洋葱架构,以同心圆的形式呈现,业务实体位于中间,围绕核心组织了多层,按照依赖关系应用的方向从内向外发展。

在这种方法中,你将找到之前在另外两种方法中几乎已经解释过的所有内容,包括领域模型必须摆脱任何技术依赖性,以解决其在系统中的极端重要性,并带来各种其他好处,如可测试性。

依赖反转也是推荐的,因为它与关注点分离相似。词汇略有不同,但通过在互联网上快速搜索,你会看到很多关于三种架构之间极端相似性的博客文章。清洁架构可能比其他架构更“指导”,但在写作时,不可能找到证明其中一种比另一种更好,甚至在结果上广泛不同的文档分析。

我个人的观点是,这三种方法(洋葱、六边形和清洁架构)在它们的工作方式和推荐方面非常接近,你可以使用其中一种而不影响产生的架构质量。

能够带来质量的是对这些方法存在的原因的深刻理解,特别是创建干净分离的模块并保持对依赖关系的控制的能力。没有对它们之间关系的严格规则来切割关注点和责任,最终会导致像意大利面一样的系统。

相反,遵循严格的原则,其中业务领域(最重要的部分)没有任何依赖,并且所有技术都是围绕它接口的,中间有一个间接层,这才是真正重要的。而且三种方法都有这个共同点。其余的都是纯粹细节,如果你考虑,例如,六边形形状并没有给方法带来任何东西,就像之前解释的那样,那么存在多种方法确实给人一种印象,即其中一些方法的存在仅仅是因为它们的作者想要有自己的方法。

摘要

在本章中,我们使用了 C4 方法从四个角度详细介绍了我们的演示应用程序,即它所使用的上下文、它所使用的容器以及它由哪些组件和代码组成。遵循此方法绘制的图表帮助我们解释了我们将要创建的示例信息系统的内容。我们还了解到,没有必要创建一个完全覆盖研究领域的图表,但只有这些图表具有价值。

此外,在过去几十年中涌现了许多架构方法。尽管它们在软件应用程序的设计中,以及信息系统设计中显然具有价值,但它们在某种意义上非常相似,即最近的方法大多是领域中心的;因此,它们的价值基本上可以总结为我们已经知道的两个原则,即 SOLID 原则和业务对齐原则,即首先考虑业务功能模型,并应用一种方法来减少对依赖的耦合。这两者都使得系统的演化变得更加容易。我毫不怀疑这些方法可能对软件组织具有额外的价值,并且它们确实可以帮助你超越纯粹技术导向的架构模式。然而,就整个系统而言,我认为在第章开头探讨的 C4 方法应该首先以自上而下的方式应用,将领域中心的方法作为组织 C4 方法的第三级(组件)的一种方式。

第八章中,我们将开始更精确地定义我们的演示系统所基于的 API,因为这些实体无疑是实现自由发展的应用程序最重要的技术方面。在接下来的章节中,我们将首先解释服务导向的概念及其与 API 的关系。在第九章中,我们将采用基于语义的方法来定义我们的示例应用程序的业务领域,并将它们转化为实际的 API 合约。我们已经对业务领域进行了很多讨论——在接下来的两章中,我们将定义这些领域,以及它们在我们演示系统中的交互。

第八章:服务导向和 API

现在我们已经解释了许多原则和几种方法,我们将继续进入一些技术性更强的章节,这些章节将展示更多应用到我们的演示应用程序中的示例。在本章中,我们将从 IT 的角度解释“服务”的概念,并尝试将服务置于 IT 历史中,以便您更好地理解它们的目的以及它们为行业带来了什么。当然,仍然存在一些不足,但面向 Web 的架构和一般意义上的 Web 服务,当它们被正确设计时(遗憾的是,这远非普遍现象),为软件行业带来了巨大的价值。

在对服务的历史进行了考察之后,我们将详细阐述良好服务导向架构的特征(我之所以不使用“服务导向架构”这个表达,是有确切原因的,您很快就会明白),并解释它们当前的演变,即 REST API,如何对许多软件系统有益。

最后,我们将展示上一章中提到的架构模式如何应用于演示应用程序的服务定义。为此,我们当然会使用 REST API,因为它们是现代 IT 系统架构方法的核心理念。我们将回到在第二章中广泛讨论的标准概念,并解释哪些标准可以用于我们的演示系统。最后,我们将解释当没有标准存在或适用时我们可以做什么,这将成为下一章的过渡点。

在本章中,我们将涵盖以下内容:

  • 查看服务导向的历史

  • 服务的特征

  • 应用到我们的演示系统中

查看服务导向的历史

首先,让我们从一点历史开始。这可能是因为我是一名老程序员,过去 37 年中一直在编程,其中 25 年在工业环境中,但我认为了解我们从哪里来总是很有趣,因为这解释了今天许多技术被创造的原因,以及它们仍然缺少什么。这样,我们不仅能够预见某些技术和软件实体的不足,还可以避免不充分利用它们的风险,正如它们的创造者所期望的。回顾技术的历史还有另一个优点:在沿着这条道路漫步时,你可能会偶然发现一个古老但仍然有趣的技术,它可能比新出现的技术更适合你的环境。这种情况并不经常发生,但当它发生时,如果你可以用一个经过实战考验的、比目前普遍使用的工具更简单的技术来解决你的 IT 问题,这可以在维护时间和性能上为你提供巨大的提升。例如,基于文件的互操作性可能对每天使用 Web API 的人来说显得可笑,但在特定情况下,异步操作更好、安全性不是问题、进程的独立性是一个优势、避免部署 Web 服务器可以节省时间的情况下,它们可以是一个完美的解决方案。

因此,让我们从互操作性技术开始这段旅程,并且特别开始于为什么我们需要它们。

长期期待的可重用性

使软件实体的两个部分相互互操作是一个几乎与编程本身一样古老的概念,因为它与可重用性相关。为了使一个常用函数不需要被输入两次,需要有一种方法将其从其他不同的代码部分中分离出来,并且以某种方式使其可以被这些代码片段调用。这可以很容易地如图图 8.1所示进行图解:

图 8.1 – 重复使用通用代码

图 8.1 – 重复使用通用代码

代码重复是一个问题(尽管“不要重复自己”原则可能有其自身的不足,而且每个编程选择总是需要妥协),因此将一些代码放在公共部分通常是一种有价值的方向。有众多方法可以组织这一点,而且可重用性就像 IT 领域的格拉尔一样,多年来一直被追求,甚至几十年。

程序和避免额外打纸卡

首次尝试实现可重用性来自一个你们大多数人甚至都不太可能了解的时代,那时程序以穿孔纸卡的形式存在,这些纸卡上打有孔洞,为计算机提供指令。这实际上源于雅各德织机机制,在这些纸卡被用来控制半自动化机器中的布线,以在最终布料上形成特定的图案和编织图案。通过重复使用相同的卡片,可重复性已经实现,但在给定卡片上重复图案是通过多次以完全相同的方式打孔来完成的,这导致了一个漫长的手动过程,并涉及到出错的风险。此外,每个应用程序,由按精确顺序排列的带有穿孔纸卡的盒子组成,必须包含每一条单独的指令。由于卡片上可以容纳的指令数量是固定的,因此卡片在另一个堆栈中再次使用实际上是不可能的。即使可能,提取正确的卡片,使用它,然后将其放回堆栈的正确位置,对于涉及的两个应用程序来说都太危险了,尤其是关于复制纸卡,即使这是一个手动操作。

然后出现了使用例程并将代码发送回程序中给定地址以使其重复部分指令的想法。与臭名昭著但仍然值得尊敬的GOTO指令相关的概念诞生了,它为后续的程序节省了许多指令。但有一个问题:例程只能在单个程序内部使用。诚然,它有助于减少它们的大小。然而,当创建一个新的程序时,仍然需要输入相同的代码——我们谈论的是一个复制粘贴根本不存在的时代。因此,需要更好的东西。这就是我们将在接下来的章节中要探讨的内容。

库和程序间共享指令的能力

重复使用的下一个发展阶段是.dll文件的概念,或者说是.jar存档中的 Java 模块,它们仍然是重复使用的基石,当像微软的基类库这样的库成为所有.NET 程序员的证据时,通用性就不远了。对于这个最初在微软严格控制下开始,并逐渐发展到开源可用性的特定框架,历史是一份祝福,因为许多库只是一次性实现。另一方面,Java 最初是一个更开放的平台,但在 Oracle 收购 Sun 之后,它逐渐变得更加封闭。尽管事情开始有所统一(以牺牲更慢的进化为代价),Java 生态系统中仍然存在许多做同样事情的库。我记得在我使用.NET 五年后的第一次 Java 专业开发中感到震惊,因为没有一个 XML 解析库,而是有许多,每个库在某一件事上都是最好的:Xerces 擅长流分析;Xalan 被认为是加载 XML DOM 最快;有些其他库在处理 DTD 模式验证方面做得更好;等等。对于习惯于只有System.Xml的人来说,这真是一个巨大的惊喜和巨大的失望,因为它使得学习曲线突然比平台和语言的亲近性预期的陡峭得多。

无论如何,库无疑是编程平台中重复使用最普遍的方法,并且几乎存在于所有现代语言中,无论是 JavaScript、Python、C 还是 C++等等。尽管库也有它们的困难,不仅在于版本控制和向前兼容性领域,还在于复制文件时的便捷性,有时这会导致代码库中出现多个副本(这与重复使用的初始目标相悖),但它们仍然是必要时首选的方法。当然,就互操作性而言,它们有明显的不足,因为它们只能在它们自己的执行平台上使用:尽管可能存在桥梁,但 Java 库只能由 Java 程序使用,.NET 程序集只能由另一个.NET 程序集调用,等等。这是一个长期困扰 IT 工程师的问题,也是许多解决方案出现的地方。

尝试实现通用互操作性

解决上述限制的主要方法是创建包含编译后代码的库,这样机器代码就可以从任何调用者处使用,无论创建调用程序的编程语言是什么,只要它也以机器可读代码编译。那里的困难主要在于技术层面:调用这样的库并不像使用函数名和属性那样简单。此外,由于编译平台的原因,仍然存在限制。确实没有在 Linux 操作系统内执行 Windows 库或反之亦然的方法。

微软实际上通过引入由操作系统控制的组件的概念,在这个方向上尝试走得更远。这些可重用单元不是直接作为文件提供,而是作为操作系统本身所知的实体:它们的实际形式仍然是文件,具有.dll扩展名,但一旦注册,Windows 就会使函数对任何程序可用,即使它没有访问原始文件。除了作为应用程序定制的存储库之外,注册表还存储了组件对象模型COM)所需的信息;实际上,它甚至从 Windows 3.11 开始就主要为了这个用途而开始其职业生涯。与 COM 一起,微软的一项名为动态数据交换(Dynamic Data Exchange)的技术允许应用程序组件相互插入。这就是你今天在 Word 文档中打开 Excel 工作表时看到菜单适应的原因。

在 COM 之后,出现了 COM+等扩展,然后是分布式 COMDCOM),这是试图突破本地计算机的边界并引入组件的远程执行。这些并没有像这个组件领域最新创新——ActiveX——那样成功。ActiveX 是一种基于 COM 的技术,旨在使将图形组件集成到应用程序中变得更容易,而不仅仅是函数。甚至可以通过在浏览器中交付它们来将这些组件嵌入到 Web 应用程序中。在浏览器安全没有像今天这样扩展的时候,它提供了许多有趣的功能,但现在这项技术已经过时。

存在其他用于分布式组件的技术,例如企业 JavaBeans 和所有使用 CORBA 的平台,但它们与 DCOM 相同的局限性,并且没有展现出最初承诺的低耦合水平。版本控制留给了平台维护者,没有能力与表示层建立关系,以及其他不足之处使得这些技术如今纯粹是遗留技术,未来有限。

事实上,以组件形式实现的互操作性可能过于谦逊,以至于无法真正达到 ASCII、Unicode、HTTP 等广泛使用的技术如持久技术状态。组件最初是在单台机器的范围内开始的,并且从未找到一种方法走出这个范围。为了迈出下一步,需要一个完全通用的方法,这涉及到将互操作性和重用带到整个计算机网络中——而且为了值得这样做,这必须在所有网络中最大的网络,即互联网中。

尝试使用 Web 标准实现通用性

我们互操作历史的下一个里程碑涉及 Web 服务,在术语的一般接受意义上,意味着通过 Web 标准提供服务。由于 HTTP、TCP/IP、Unicode 和 XML 等基础已经可用,并提供了这样一个通用互操作技术所需的大部分基础,因此网络是进行这一步的明显方式。

第一次尝试实现 Web 服务(或者我们可以称之为互联网上的可重用函数)使用了诸如简单对象访问协议(SOAP)和Web 服务描述语言(WSDL)等技术,由于这些标准在领域内是独一无二的,它们简单地预占了web service这个术语,这成为了 SOAP 和 WSDL 兼容的表述所接受的行话。SOAP 旨在标准化通过 HTTP 请求和响应的 XML 内容,使它们看起来像函数调用,包括一个信封、具有类型的属性、可能的元数据等等。WSDL 是用来表达相关合同的标准,简而言之,是应该在 SOAP 消息中使用的语法。还有额外的标准,例如通用描述、发现和集成(UDDI),例如,但这些未能实现其目标,并迅速衰落。许多所谓的WS-标准也是如此——WS-Authentication、WS-Routing 以及添加到 Web 服务语法的其他语法,旨在允许补充功能。

这些解决方案在 2000 年代在行业中获得了很大的动力,并在 2010 年代初达到了顶峰。实际上,它们产生了一个整个架构,被称为面向服务的架构(SOA)。SOA 本应是一个通用术语,但它已经与特定的架构和软件相关联。此外,软件制造商大量投资于SOA 工具,使公司相信,中央中间件是他们实现互操作性的全部所需,而了解互操作性的专家知道,这仅仅是交易的一部分,语义和功能互操作性实际上比技术互操作性更关键、更复杂,而技术互操作性通常是过程中的最后一公里。

当然,这导致了对 SOA 的大量批评,2010 年代中期,许多文章宣布了 SOA 的死亡及其未能实现目标。与此同时,在行业中它的传播仍然非常广泛,许多技术因为 SOA 而得到了新生。

中间件中的步骤

特别是中间件应用在 SOA 架构中被推高,即使它们不是基于 SOAP 和 WSDL,也能适应它。企业应用集成EAI)是一个古老的梦想,并假设集中的适配器使得系统中的许多应用程序能够相互通信,EAI 平台将每个消息从一种格式转换为目标格式。当然,其集中化的方面是一个单点故障SPoF),相当大的缺点。如果你再加上每次任何应用程序更改其版本时都需要更新的 EAI 砖块,那么这些定制系统从未达到成熟也就不足为奇了。

提取、转换、加载ETL)是一组数据处理工具,但它们可以被归类为中间件应用,尤其是由于在常见的信息系统中的应用程序之间有很多互操作性流实际上是纯粹的数据传输,而不是业务功能调用。当然,这是一种粗略的中间件,但数据质量比工具的复杂性更重要,一个良好的 ETL 可以在结构化信息流方面走得很远。然而,ETL 并不完全适应数字化转型,而且很容易失去对它们的控制。我咨询的一家公司有一个非常混乱的 ETL 作业系统,每晚有超过一千个作业启动,整个系统需要一个专门的工具来精确地编排这些作业,以便在早上结束时有干净的数据。随着新作业的不断添加,在低活动期间时间开始变得稀缺,在添加更多服务器功率和并行化可以并行化的内容之后,整个系统只有在办公室开门后才能完成工作。这当然成为一个重要的问题,必须通过激进的决定来解决。更糟糕的是,作业之间相互依赖且脆弱,以至于没有一个夜晚所有作业都通过,在操作期间需要运行一些纠正作业,或者对于风险最高的作业,简单地等待下一个夜晚,希望得到干净的数据。出于信息目的(并且希望它也能起到警告的作用,因为作业的数量使得图表几乎无法阅读),以下图表显示了作业的按时间顺序执行的图形。这个图表旨在概述复杂的作业;文本可读性不是目的。

图 8.2 – 复杂作业编排

图 8.2 – 复杂作业编排

消息导向中间件(MOMs)已经存在了一段时间,但得益于 SOA 和 AMQP、ActiveMQ、MSMQ 和 RabbitMQ 的引入,它们在消息传递的可靠性方面获得了市场关注度。即使在今天不再使用 SOAP Web 服务的架构中,MOM 仍然有助于确保系统内特别重要消息的全面传输功能。一些 MOM 支持者认为所有消息都应该通过中间件传递,防止应用程序直接相互通信,但这会影响性能,我们将展示消息的功能标准允许移除中介层。

就中介层而言,消息导向中间件(MOMs)从霍普和沃尔夫(www.enterpriseintegrationpatterns.com/)定义的标准消息操作方式中受益良多,这种方式被称为企业集成模式EIP)。EIP 定义了一些处理软件消息的标准模块,例如多路复用、基于内容的路由器、丰富化等。通过组合这些基本的消息转换或路由模块,MOM 能够处理几乎所有可能的功能场景。Apache Camel 是参考开源的 EIP 实现,并被广泛应用于许多中间件中。术语砖块特别适用于这些模式,因为它们可以用实际的、具体的乐高™砖块来解释:我经常使用这些来直观地解释软件系统架构的概念,特别是如何通过引入具有可组合动作的中介层,以最小的冲击使系统架构演变,每个动作都由简单的技术乐高™砖块组装处理,如图8**.3所示。3*:

Figure 8.3 – 使用乐高™砖块模拟的企业集成模式

Figure 8.3 – 使用乐高™砖块模拟的企业集成模式

企业服务总线(ESB)是 MOM 和 SOA 的自然演变,与互联网的原则相冲突。ESB 将我们讨论过的所有技术集成到一个不再有集中的系统中:网络(在 TCP/UDP 中)是唯一保持中心化的东西,其适应交付的能力被用来提高对节点故障的鲁棒性。同时,使用存储和转发模式来确保消息几乎永远不会丢失,因为它们被持久化,并且只有在下一个目的地确认它们在其控制下已持久化后才会被删除。ESBs 几乎拥有实现互联网规模系统互操作完整功能目标所需的一切。但仍然,它们失败了,或者至少没有像预期的那样成功,因为这是解决 IT 行业如此重要问题的理想解决方案。事实上,ESBs 添加了所有必要的功能,但这正是它们的厄运。由于它们可以做到一切,它们庞大、复杂的机器需要大量的专业知识来运行和维护。

最新的演变——REST API

然后出现了 REST,这是一种创建基于 Web 的 API 的更轻量级的方式,这再次彻底改变了生态系统。表示性状态转移(REST)在 2000 年之前就已经被定义,但在 2010 年代初才真正成名。到了 2020 年代,尽管遗留软件的部分很大,SOAP Web 服务仍在被利用,但没有任何新项目是基于这些旧技术开始的,几乎每个新的 API 项目都在使用 REST,或者至少采用了某种降级的、并非真正“RESTful”的方法。

简而言之,REST 是关于回归 HTTP 的基本机制,以允许在 Web 上进行函数调用。例如,与 SOAP 将操作代码发送到信封中不同,REST 使用 HTTP 动词,如GETPOSTPUTPATCHDELETE来指示服务器应该做什么。它不是发送函数调用,而是像 HTTP 处理更知名的 HTML 页面或通过 Web 提供的图像一样处理资源;只是这些资源在功能上是有方向的,比如客户或合同。这些业务实体每个都有 URL,就像网页或资源一样。它们的表示可以是 HTML,但更适合 XML 或 JSON,后者比其前身 XML 更轻量。超媒体、格式协商和头部也被用于等效的互操作功能。授权简单地留给任何 HTTP 调用中的等效功能,使用基本身份验证、Bearer 令牌等。简而言之,REST 将基于 Web 的互操作简化到了极致,消除了每一丝冗余,以专注于现有标准的纯粹和完整使用。实际上,REST 并不需要除 HTTP、JSON 或 XML、Unicode 等现有标准之外的其他任何东西。因此,它更多的是一种实践,而不是一种新的协议。

而且它确实有效……它实际上运作得如此之好,以至于互联网上的评论者毫不犹豫地谈论 SOA 2.0,甚至正确的 SOA。有些人引入了新的架构术语,如面向 Web 的应用,以将这种方法与原始 SOA 区分开来。REST 成功的最好证明是,没有编辑利用其名声来尝试强制实施专有实现:REST 之所以运作良好,是因为它没有添加任何东西,而是将任何软件层减少到零,因为一切都已经存在,工程师只需按照其预期的方式使用它即可。

我们缺少什么才能达到实际的服务重用性

这就是我们在撰写本书时的现状,毫无疑问,情况将继续发展,但我们已经达到了一个点,即基于实际的基于 Web 的互操作性,包括两个独立实体之间的互操作性,对于许多公司来说已经成为日常现实,这本身就是一个巨大的胜利。当然,我们总是可以更进一步,但主要道路已经铺好,现在剩下的任务主要是传播这种互操作方式的好做法,而不是想象一种克服任何当前缺点的新方法。

事实上,大部分剩余的问题都是由于缺乏功能数据交换的公认格式,导致存在中介连接器。如果我们想要达到一个理想的地方,在那里全球通用的互操作性不再是问题,而是一个过去的问题,我们就需要为每个数据流拥有一个无可争议的标准。这当然是不可能的,而且我们离这样的满意状态还非常遥远,但一些精确的、非常普遍的、技术上容易的交换形式目前已被涵盖。例如,认证和识别现在由 OpenID Connect、SAML、JSON Web Tokens、SCIM 以及一些其他规范很好地实现了。当然,有很多遗留软件甚至专家工程师没有使用这些,但总体趋势是它们是未来的方向,全球各地都接受这一点,并朝着这些规范努力,这些规范将成为未来的便利标准,就像 ASCII 和 Unicode 对于文本的二进制表示一样。还有一些其他领域被涵盖,或者至少有很好的、功能齐全的规范可以解决这个问题,例如 CMIS 用于电子文档交换或 BPMN 2.0 用于工作流建模。

但绝大多数的交换都没有被一个无可争议的标准所覆盖,大量的连接器仍在开发中,以建立应用程序之间的对应关系。这在当今全球 IT 领域是一个巨大的资源浪费,因为这些中介连接器并没有为顾客和最终用户增加任何额外的价值。但现实是,制定一个标准需要花费大量的时间,正如我们在第二章中看到的。尽管如此,让我们尝试关注积极的一面:现在这个运动正在活跃,情况每年都在改善,有一个强大的互操作性基础,其中技术问题现在已经得到解决。只剩下语义和功能互操作性需要处理。这将是下一章的主题,但在讨论这一点之前,我们需要回到“服务”这个概念本身,并解释一个好的服务应该如何定义。然后我们将使用这些原则来制定我们演示系统的第一个服务,使用上一章中展示的架构原则,并将它们应用于之前展示的演示应用程序,并在本书的其余部分对其进行更详细的开发。

服务的特征

服务是一个如此模糊的术语,以至于需要一个完整的章节来给出一个良好的概念理解——而不是一个单一的定义。

如同服务所解释的

“作为服务”这个表达在许多公式中被使用:SaaS代表软件即服务PaaS代表平台即服务CaaS代表容器即服务,等等。你有没有考虑过为什么如此不同的事物使用这个共同的名称?这本身可能就是对服务最好的定义:从其他事物中受益,而不必处理通常与之相关的外部性。酒店房间是一种服务,因为你可以享受到床和屋顶的好处,而不需要购买和维护一栋房子,甚至不需要打扫房间。SaaS 是一种服务,因为你可以使用软件(操作其界面、存储数据并检索它、实现复杂的计算、导出结果),而不需要安装软件、购买长期许可证、操作它、安装新版本等等。IaaS 是一种服务,因为它提供了你对基础设施的预期(CPU 功率、RAM、I/O、存储、网络带宽和使用),而不需要你担心购买服务器的硬件方面、操作它们、租用一些房间、整理电力和冷却、物理上保护它们、在出现故障时更新硬件等等。

解释作为服务的表达式是必要的,因为“服务”这个词本身非常通用,人们可能会有些困惑,为什么我们谈论面向服务架构,然后是 SOAP 网络服务的概念,然后是网络环境中的服务,等等。当我们在这本书中谈论服务时,我们真正指的是作为软件功能向用户提供的服务,而用户无需关注其实现:用户不需要知道使用的是哪个平台,服务器在哪里等等。他们只需要知道尽可能少的信息,即一个 URL 和定义交换语法的合同,以便与该服务进行互操作。

这让你想起了什么吗?仅仅依赖于某物的功能定义,而不考虑任何与软件相关的约束?这正是书中已经提到过的内容,特别是我们讨论了四层 CIGREF 图模型的部分:

图 8.4 – 使用 CIGREF 图解耦

图 8.4 – 使用 CIGREF 图解耦

当谈论提供作为服务功能时,可以将其视为从第二层(业务能力图)获得某些内容,而无需担心它在第三层和第四层(技术层)如何实现。

完全去除中间件

作为服务的方法的一个优点是它允许我们完全去除中间件。实际上,我们真正想避免的是直接、点对点的互操作,这会导致大量的耦合,如下面的图所示:

图 8.5 – 点对点互操作

图 8.5 – 点对点互操作

但中间件,虽然引入了一个间接层,却带来了两个问题。第一个问题是它引入了额外的软件复杂性,这可能很难维护。第二个问题是我们仍然处于 CIGREF 图的软件层,这意味着,如果没有进行标准化(没有标准化消息),我们可能会遇到两步耦合而不是简化它!以下方案表达了这种潜在的危险:

图 8.6 – 通过中间件进行互操作

图 8.6 – 通过中间件进行互操作

企业服务总线(ESB)通常被提出作为避免集中实体的解决方案,但它们实际工作的方式仍然意味着存在(尽管是分布式的)可能导致耦合的软件代理:

图 8.7 – 与企业服务总线进行互操作

图 8.7 – 与企业服务总线进行互操作

避免这种耦合的一种方法是从功能角度标准化消息:

图 8.8 – 与标准化解耦功能进行互操作

图 8.8 – 与标准化解耦功能进行互操作

但如果我们达到了这样一个状态,即已经创建了一个功能标准,那么中间件实际上不再需要映射数据或转换任何格式,因为ff'函数实际上是相同的(否则它们不会被包含在单一的数据流中)。中间件唯一的功能仍然是路由、认证以及一些可以通过 HTTP 简单实现的功能,而不需要任何中间件。因此,中间件简单地消失了,我们达到了之前所表达的理想情况:

图 8.9 – 间接解耦原理

图 8.9 – 间接解耦原理

在这里,唯一剩下的困难是一个功能性的,即描述与业务相关的需求。诚然,这可能是一件非常困难的事情,但主要区别在于这是我们需要在任何情况下克服的内在复杂性(否则软件将无法正确工作),而不是偶然的技术复杂性,它进入我们的设计阶段并增加了版本控制、维护等问题。这是解耦的本质,也是使系统更容易演化的关键。

再次强调,尽管我们应该尽可能努力实现这一点,并且这确实是一种创建低耦合互操作性的方法,但这种交互并不总是容易实现。MOM 和其他中间件系统在不久的将来不会退役,因为它们仍然是互操作复杂消息、应用调解以及在无法在信息系统中对消息进行完全标准化时确保交付鲁棒性的好选择。

外部互操作性最终成为现实

所有这些都可能听起来有点理论化,但正是这种方法使我们最终达到了这样一个阶段,即前面图示中的软件A和软件B之间的互操作性(图 8.58.9)不再依赖于中间件或其他阻碍其复杂化的工件。展示这一点最好的方式是提供一些实际例子。

在我之前工作的一家公司中,两个客户(一个区域委员会和一个城镇)希望以这种方式进行互操作,即当区域委员会将其列表中的关联添加时,城市会自动接收到信息并将其存储在其自己的数据库中,前提是它是注册的城市。这种方式需要一些重要的前期工作,这是我的雇主完成的,即定义法国协会的标准格式。由于我们对主题很了解,这仅花了几天时间,我们就将此格式提交给法国政府,以便在他们的开源 forge 中发布,因为他们没有为这个现有的标准。这个格式是两个客户之间的功能合同。他们同意,无论他们可能对其软件进行何种更改,协会 JSON 的内容始终如下(此提取高度简化并翻译成英语以提高可读性):

{
    "name": "Old-time developers of Brittany",
    "registrationNumber": "FR-56-973854763",
    "organizationType": "uri:ORGANIZATIONS:ASSOCIATIONS",
    "creationDate": "2019-01-04T12:00:00Z",
    "representatives": [
        {
            "role": "accountant",
            "lastName": "Gouigoux",
            "firstName": "JP"
        }
    ],
    "legalAddress" : {
        "streetNumber": 282,
        "cityName": "Saint-Nazaire",
        "zipCode": "44600"
    }
}

事实上,区域委员会在这个项目之前已经是客户,所以他们已经在使用基于此格式的我们道德人参考软件。因此,在这一方面,我们只需要定制事件管理系统,以便在发生创建、修改或删除协会的事件时调用第二个客户的回调地址。这是通过以下语法完成的:

{
    "webhooks": [
        {
            "topic": "POST+*/api/organizations",
            "callback": "https://saint-nazaire.fr/referentiel_associations/modules/index.php?refOrga={registrationNumber},
            "method": "PUT",
            "filter": "organizationType=='uri:ORGANIZATIONS:ASSOCIATIONS' and zipCode=='44600'"
        }
        {
            "topic": "PUT+*/api/organizations/{registrationNumber}",
            "callback": "https://saint-nazaire.fr/referentiel_associations/modules/index.php?refOrga={registrationNumber},
            "method": "PUT",
            "filter": "organizationType=='uri:ORGANIZATIONS:ASSOCIATIONS' and zipCode=='44600'"
        }
        {
            "topic": "PATCH+*/api/organizations/{registrationNumber}",
            "callback": "https://saint-nazaire.fr/referentiel_associations/modules/index.php?refOrga={registrationNumber},
            "method": "PUT",
            "filter": "organizationType=='uri:ORGANIZATIONS:ASSOCIATIONS' and zipCode=='44600'"
        }
        {
            "topic": "DELETE+*/api/organizations/{registrationNumber}",
            "callback": "https://saint-nazaire.fr/referentiel_associations/modules/index.php?refOrga={registrationNumber}&setActive=false,
            "method": "PUT",
            "filter": "organizationType=='uri:ORGANIZATIONS:ASSOCIATIONS' and zipCode=='44600'"
        }
    ]
}

为了稍作解释,webhooks 是外部系统对由给定应用程序发出的事件的注册。在我们的案例中,当区域委员会的参考服务接收到新组织或现有数据变更的数据时,通过参考服务的 API 方法,会引发相关事件,上述定制文件提取将这些事件与提供的 URL 的调用相关联。这个 URL 是由第二个客户(圣纳泽尔市)使用 PHP(但具体技术不重要)公开的。例如,当我们对一个新组织应用POST操作时,回调 URL 会调用创建实体的标识符以及PUT动词。这也是我们引入事实的地方,即该城市只对关联(不是所有组织)感兴趣,特别是那些在其领土上的组织,使用filter属性。

随后,URL 实现可以自由地按其意愿工作,而不依赖于发射器。在某些操作中,给定标识符上发生事件的现实就足够了(例如,在区域议会信息系统中收到DELETE命令时,可以取消关联)。在其他情况下,例如创建关联时,JSON 内容——其精确语法由双方达成一致——将通过回调中获得的标识符通过GET操作检索(因为关联的所有信息都有用)或简单地读取回调调用体(因为最重要的数据被发送到那里,当然使用相同的合同语法)。

这个例子证明是一个成功的实验,因为每个客户都可以自由地以他们想要的方式发展他们的系统,改变技术或其他参数,而他们的合作伙伴甚至不需要知道这一点。在某个时候,城市可能会对其邮政编码区域外的关联感兴趣,只需简单地注册一个新的带有更新过滤器的 webhook 内容。这不会影响事件发射器,甚至不会影响其授权方案:如果城市要求在部门(法国的一个介于地区和城市之间的地理单位)之外被调用,事件就会被发送,但通过接收到的标识符读取信息最终只会导致403 Forbidden HTTP 状态码。这种特定的机制最初让我们决定在回调请求中永远不发送任何数据,以简化授权机制。但是,在某个时候,有人决定强制被调用实体始终以GET调用回复以获取新关联的名称和基本信息是一种带宽浪费。性能并不是问题,但在这种情况下,简单性比授权失误的风险更重要,因为这项数据在法国是公开的,而且很容易获得。

标准化实现互操作性

上述例子演示了一个特定数据模式(我们称之为关键格式,但我们将在本章末尾和下一章中更详细地讨论这一点)必须被设计出来以自由和松耦合的方式交换数据的情况。但更好的情况是,这种合同在行业中已经存在。这是另一个我有幸处理的实际案例,特别是,因为当时我工作的那家小公司迫使一家更大的公司遵守我们的工作方式,仅仅因为我们使用了公认的标准。让我更好地解释一下情况……

我们的旗舰应用程序,一种类型的 ERP,生成 PDF 文档和其他二进制文件,这些文件应该被存储。在相当长的一段时间里,这些文件会存储在数据库旁边的网络共享中,有时也会存储在通过 UNC 链接访问的专用服务器上。电子文档管理系统在几年后开始成为主流,我们需要调整我们的应用程序,使其能够使用这些系统来存储文档。对于这一点,自然的选择是内容管理互操作性服务规范,因为 OASIS 发布了一个功能齐全的 1.1 版本,支持多个元数据模式、分类、版本控制以及许多我们甚至不需要的功能。而且,这也恰好是这个功能领域唯一使用的标准,这使得架构决策变得非常简单。

因此,我们最终使用了标准操作中的几个操作(在第一步中,我们只需要创建文档,向它们添加元数据和二进制内容,然后通过对其元数据内容的查询检索文档),这花费了我们几周时间才将其添加到我们的应用程序中。客户非常满意,因为对软件的简单定制就能使文档出现在他们的 Alfresco 或 Nuxeo EDM 系统中,因为这些应用程序是原生 CMIS 1.1 兼容的。但真正证明这种规范性方法重要性的,是我们第一次遇到一个配备了专有 EDM 的客户:编辑,一家相当大的公司,在我们的共同客户的信息系统中占有重要地位,希望我们修改我们的应用程序以支持他们的专有网络服务,以便发送文档和元数据。在我们最初拒绝后,情况变得有些紧张,但我们很幸运,信息系统所有者是一个聪明的人,她完全理解低耦合的价值。她明智地询问,如果她必须选择另一个供应商来提供这项服务,一个合作伙伴需要付出多少努力。EDM 提供商表示,如果我们的公司被另一家公司取代,他们就不需要做任何事情。就我们公司而言,我解释说——在相反的假设中——我们可能需要重写一些代码以适应另一个专有协议。这对客户来说已经足够了,即使她不是技术专家,也能意识到这种操作方式有问题,并要求使用基于标准的、合同式的通信渠道。由于客户的反对,EDM 提供商别无选择,只能在自己的成本下在其产品中实现对 CMIS 标准的支持。

这在许多方面都证明是一次非常令人满意的经历:

  • 首先,我必须承认,重新演绎大卫对抗歌利亚是我职业生涯中最令人自豪的时刻之一。

  • 其次,我们没有在会议中添加任何东西到我们的软件中,因为它已经准备好支持 CMIS。

  • 第三,客户赞赏我们在帮助他们达到更好、更具进化性的系统方面的专业知识,而不是像其他合作伙伴那样试图将他们推入供应商锁定的情况。

  • 第四,互操作性项目在技术上非常容易领导,因为我们只需向合作伙伴提供我们需要的 API 调用 Postman 集合,他们就能从 CMIS 规范的角度验证它们。互操作性调用中没有“隐藏参数”,一切都是明确的,并且严格通过 OASIS 标准进行规范。我们只需在认证的情况下进行一次调整。

  • 最后,即使最初不愿意的合作伙伴也承认,在项目结束时,这种方法有助于避免项目中的乒乓效应,即双方都拒绝对对方的非工作调用承担责任,最终导致时间全球损失,客户不满意。我真正相信 CMIS 支持将为他们的产品在未来的某个时候开辟新的机会。

保持完全兼容性

所有这些听起来像是一个美丽的梦想,到处都是粉红色的独角兽和彩虹,但使用国际标准和规范构建的伟大 API 并不能防止互操作性中的最后一种危险。实际上,情况正好相反,API 越干净、越易用,这种危险就越大。听起来很奇怪,不是吗?欢迎来到 Hyrum 定律(www.hyrumslaw.com/),它陈述了以下内容:

在一个 API 有足够多的用户的情况下,

无论你在合同中承诺了什么:

您系统的所有可观察行为

将会有人依赖。

您的 API 越成功,向前兼容性就越重要,因为不可能打破许多客户端的使用。但毕竟,这只是成功的一面,如果你的 API 在你的环境中是最常用的,这确保了很大的市场份额和显著的收入,这并不是一个坏价格。Hyrum 定律更为严厉,因为即使是你没有正式承诺的 API 的一些部分,也可能成为让你陷入麻烦的事情。例如,性能的突然变化可能会使你的最大客户无法继续使用你的 API。即使是较小的、非合同的修改也可能让你陷入这种麻烦。你知道吗?即使是移除一个错误,也可能让一些 API 用户不满意,因为——以一种扭曲的方式——他们的系统依赖于这种特定的行为来运行。这听起来可能很荒谬,但它比你想象的要普遍得多。毕竟,一些 API 用户按照它们的顺序而不是它们的标识符来消费响应属性是非常常见的。

在一定程度上,Hyrum 定律可以被认为是面向对象编程中 Liskov 替换原则的 API 等价物:即使一个类可以通过实现相同的接口来替换另一个类,但如果在函数调用具有相同的参数值时其行为不同,那么实际的兼容性(以及因此的可替换性)并未实现。

管理 API

即使这更多的是一个操作问题,管理大量的 API,包括所有授权访问问题、日志记录,以及可能对 API 消费进行计费、版本跟踪等问题,可能会构成一个严峻的挑战。一些专门的软件产品存在,通常被称为API 网关。它们通常以反向代理的形式实现,充当前端服务器,隐藏实际的 API 暴露。

根据您是否需要一个低耦合的系统或一个非常集成的系统,您可以使用 WSO²或 Ocelot(如果您使用 ASP.NET 实现 API 系统)等系统。

服务依赖反转

如果你记得上一章中的以下架构,你会回想起为了使卫星模块依赖于实现业务领域模型的主体模块,即使调用来自后者并前往前者,使用了端口和适配器模式:

图 8.10 – 六角架构

图 8.10 – 六角架构

这仅仅是依赖倒置原则在架构中的应用,描述一个传统接口被一个模块调用,而无需知道这个接口背后使用了什么实现。在面向对象编程(OOP)的代码中,这通常是通过对象注入来实现的。

在面向服务的系统中,尤其是在使用 Web API 时,间接层是通过调用者使用的 URL 来完成的,而无需知道其背后是什么。如果对这个模块的依赖不是问题,那么调用可以是直接的。但如果业务领域模块调用 API,直接的依赖对于演化和找到反转依赖的方法来说并不是一个好的主意。

这通常是通过使用某种回调机制来完成的,其中领域模型模块从外部(在我们的例子中是依赖)被指示调用它应该调用的 URL,可能在其定制中,也可能在运行时初始化步骤中。在前面的 webhooks 的第一种解释中,这就是当城镇需要更改区域委员会应考虑通知城镇的事件过滤器时发生的情况:区域委员会依赖于城镇是不正常的,因为城镇是信息功能请求者。这就是为什么城镇向区域委员会提供回调 URL 的最佳方式是通过注册事件,可能通过/subscribe API 来实现。

这样,我们就达到了一个很好的责任分离,因为地区议会负责以下事项:

  • 暴露一个 API,允许客户端从数据参照服务的持久化机制中创建、修改和删除组织

  • 暴露一个 API,允许客户端(可能是其他客户端,也可能是相同的客户端)在组织中注册事件

  • 当代码中出现事件时,在注册时调用这些客户端提供的 URL

  • 在注册时应用提供的过滤器,仅发射请求的事件

在另一端,城镇负责以下事项:

  • 在组织参照系上注册它需要观察的事件

  • 提供一个可访问的回调 URL,并指向必要的实现

当这种基于事件机制的机制用于每次交互以提供非常低的耦合度时,术语是事件驱动架构EDA)。在其最先进的形式中,EDA 添加了许多非常精确定义的责任,以允许以下操作:

  • 不同的注册和发射机制认证和授权方法

  • 通过在必要时重新应用调用并,如果需要,警告管理员,在尝试了一定次数后,事件已被存储以供稍后向某些已注册客户端发射

  • 处理大量事件

  • 处理大量已注册客户端

  • 服务级别协议管理,以及其他许多功能

在正确的实现中,基于 EDA 的系统是软件系统中解耦的最成功成果,允许不同模块完全透明的演变和线性性能。但尽管它在理论上有很长的存在,实际实现却非常少。

现在已经从不同的角度介绍了并研究了“服务”的概念,我们将回到我们的示例信息系统,并将这些新知识应用到它上面。

应用到我们的演示系统中

现在您应该对“服务”的概念不再有任何秘密,是时候看看我们在演示系统中涵盖的一些实际应用,以加强本章的收获。鉴于我们追求的是现代的,选择显然是示例系统的不同模块将通过 REST API 相互交互。尽可能保持中间件尽可能透明。在某些情况下,我们可能需要一些连接器来进行调解,但除此之外,应用程序将与其他集中式 API 进行通信,这些 API 将单独实现(这将通过容器编排器中将要实施的服务概念来完成)。

需要分析的接口

首先,我们将从六角架构图开始,列出所有业务域模型及其依赖关系。上一章中使用的 C4 方法表明,我们需要至少三个业务域,即书籍、作者和销售。

例如,如果我们专注于书籍,那么依赖关系包括持久化机制、作者缓存模块、书籍的 GUI 系统、书籍的 API 控制器,以及一些技术卫星,如日志记录、身份和授权管理。这可以概括如下:

图 8.11 – 六角架构的示例

图 8.11 – 六角架构的示例

在敏捷方法方面,我并不是说这包含了我们旅程结束时将出现的所有接口。但为了使这个练习尽可能真实,我在写书的同时创建了这个示例信息系统,这样就不会有任何东西被隐藏起来,并且你可以跟随我推荐的精确设计方法,当然我也尽力遵循。

因此,现在我们已经列出了第一个接口,我们需要比仅仅一个名称更精确一些。它们将要做什么?它们将如何设计以提供干净、未来兼容的使用?最重要的是,这些选择将如何反映我从第一章开始就推动的业务/IT 对齐原则?

使用规范和标准

由于我已经谈了很多关于规范和标准至关重要的内容,如果不从它们开始,就无法精确定义接口,那将是一个可怕的信号。而且,当我们使用标准时,精确描述我们在上一节中讨论的接口是非常容易的,因为我们只需要命名它(并且可能引用将要使用的版本)并且所有标准的功能、格式、语义和其他操作都通过标准的文档立即明确定义。

例如,让我们从认证和识别服务开始。对于这个特定的接口,我们将使用基于 OAuth 2.0(RFC 6749)和 JSON Web Tokens(RFC 7519)的 OpenID Connect 协议,OAuth 2.0 本身的 JWT 配置文件是标准化的(RFC 7523)。再次强调,规范和标准的好处是它们极大地简化了我们的工作。如果我要用同样的精确度描述没有标准的接口使用,那么这一章将会更长。对于这个服务,引用几个 RFC(当然,在下一章中,使用这些规范的优秀实现)就足以使一切变得明确。

那么数据库接口呢,或者更准确地说,持久化接口呢?决定使用一个 NoSQL 文档型方法,因为它听起来最适应我们讨论的业务实体和我们要处理的数据量。关于 MongoDB 可能不是一个非常知名的事实,但大多数使用的协议都是开放标准,实际上也被许多其他 NoSQL 数据库实现所使用。如果你想要改进你本地的 MongoDB 数据库,你只需要更改连接字符串即可切换到 Atlas 服务或 Azure CosmosDB 实例,因为一切工作方式都是相同的。MongoDB Wire Protocol 规范是在 Creative Commons Attribution-NonCommercial-ShareAlike 3.0许可下发布的。使用的 BSON 格式(bsonspec.org/#/specification)是公开文档化的,并且可以被任何软件实现。还有更多。除了对软件进行适当的适应以满足我们的需求,以及它易于创建免费数据库的事实之外,标准化方面是使 MongoDB 成为我们示例应用程序的一个合理选择的关键。

好的,现在转到授权问题!在软件授权管理方面存在两个主要规范,即admin角色拥有所有权限,operator可以根据投资组合读取和写入实体,而reader角色只能读取数据(例如)。然后,使用 OPA 可能不是正确的选择,因为它会添加很多开销。当然,真正的疑问,再次,是考虑时间。当然,到本书结束时,我们的示例应用程序将非常简单,使用 OPA 将是过度设计。然而,这个练习的目标是展示如果我们旨在一个真实、工业、自由发展的信息系统,我们应该如何工作。鉴于我们假设权利管理将会更复杂,我们将立即开始使用适应的接口,在我们的情况下意味着 OPA 1.0。

记录功能的情况有些不同,因为这并不是一个直接的功能,而是一个技术特性。然而,这并不意味着不应该使用相同的标准化方法。唯一的区别是,这种间接级别不会像其他规范那样在国际层面上标准化,而是将在平台上本地化。我们的示例应用程序主要使用.NET Core,因此我们将使用该技术的标准,而恰好有一个标准全局接口在Microsoft.Extensions.Logging中,称为ILogger,它也存在一个泛型类ILogger<T>。我们将在技术章节中返回来看如何使用它,也许我们甚至会通过使用像 Serilog 这样的语义日志系统来增加一些趣味。但就目前而言,只需说日志机制也将得到标准化即可。

值得注意的是,该领域的某些参与者目前正在努力实现第一级标准化,例如 Elastic 的 ECS 规范(详情请见www.elastic.co/guide/en/ecs/current/ecs-reference.html)。由于 Elastic 是观察平台的主要出版商之一,且该规范是开源的,我们可以对它作为标准的传播抱有一定的希望,尽管只有时间才能证明一切。

我在哪里可以找到规范和标准?

当我教授或咨询关于业务/IT 一致性,特别是关于需要参考规范和标准的问题时,这个问题总是在某个时候出现:我们该如何搜索规范? 我必须说,我对这个问题感到非常惊讶,原因有几个:

  • 寻找它们就像任何互联网搜索一样简单,几乎所有这些规范都是公开的,为了实现它们的目标,需要尽可能的可见,因此找到它们在技术上绝对没有任何困难。这表明,大多数在 IT 行业工作的人(根据我培训或教授的人数,我确实有一些显著的统计数据)对自己的行业标准一无所知,这相当令人烦恼。我可以理解,不是很多人知道 BPMN 2.0,因为流程是一个特定的用例,并不是所有应用程序都需要工作流引擎。但为什么一些架构师不知道 OAuth 2.0,因为这在互联网上几乎无处不在,几乎所有软件应用程序在某个时候都需要某种形式的身份验证?

  • 即使一些非专业人士也知道一些最著名的规范和标准提供者,例如 ISO 或 IETF。甚至请求评论(RFC)这个术语也被很多人所理解。当然,一些生产规范的 IT 特定组织,如 OASIS,可能不太为人所知。但另一方面,万维网联盟(W3C)是一个非常活跃且被广泛认可的机构。那么,为什么问这个问题的人没有本能地从这些组织开始,搜索他们所需的内容呢?

对于一些客户,我在某个时候甚至创建了一份白皮书,其中包含了我在当时工作的业务环境中(公共和政府机构)使用的近一百个规范和标准,因为这个问题总是反复出现,我希望有一个快速的答案,不仅告诉他们在哪里可以找到他们需要的东西,还提供给他们已经找到的答案。这有一个简单的原因:因为我发现真正的问题不是这些人不知道“在哪里可以找到规范”,而是这表明了他们对使用这些规范的能力存在疑虑。规范和标准可能因为数百页解释所有可能情况而显得有些令人畏惧。即使是简单的 RFC 也确实不易阅读。

但这个问题也有其他答案:

  • 首先,找到合适的规范并开始使用它并不需要你阅读规范说明。实际上,只有当你需要实现其中大部分内容时,阅读它才会对你有益。

  • 在大多数情况下,你会使用实现规范的组件,你所要做的就是确认它们是被认可、已经建立起来的模块。

例如,为了在我们的示例信息系统中使用 OpenID Connect,我们基本上不需要了解该协议本身,因为我们将依赖 Apache Keycloak,它以透明的方式为我们实现它。我们唯一需要处理的是选择身份提供者以及 Keycloak GUI 简化的一些自定义设置。

即使你必须深入研究规范的细节,大多数时候,你只需要理解其中的一小部分。例如,在我们的示例应用程序中,我们将在某个时候肯定需要实现某种对作者合同二进制文档的支持;这意味着我们当然会使用 CMIS 1.1,因为这是这个用例的公认标准。但因为我们只会发送文档,添加二进制和元数据,并查询文档作为回报,我们可能只使用整个规范中的 10%。

最后,一个好的规范通常已经相当广泛地传播并在国际上使用。所以,阅读完整的规范说明总是很有趣的,但让我们说实话:你在最初接触标准的方式主要是通过模仿在参考网站上找到的一些示例调用,并根据你的需求进行调整。只有当你达到一定程度的复杂性时,在 RFC 的全文中找到实施细节的精确细节才会变得容易一些。

对于其他接口的关键格式

对于这个主题的最后一部分,接下来出现的逻辑问题是:当我们的环境中没有规范或标准时,我们该怎么办?

对于这个问题,我总是首先本能地回答,“你愿意打赌确实没有我可以向你展示的规范吗?” 大多数时候,这个问题会回到之前的问题,只是表明提问者对规范感到不舒服,或者他们害怕这会很难(但实际上,规范反而让你摆脱了所有困难的设计方面)。因为,让我们面对现实,今天我们几乎为所有事情都有规范。好吧,在 IT 领域可能比机械领域少一些规范。但是,对于每个常见功能都有标准。你有一套通用的技术规范,用于国际数据传输中使用的每个实体,以及包括银行、保险、旅行等在内的每个常见的人类活动。甚至还有一个 ISO-Gender 规范(ISO/CEI 5218),用于以数字格式表示人类性别。

答案的第二个部分涉及当确实没有适用于你所在环境的规范时我们应该做什么。对这个问题的答案已经在本章中给出了一部分:然后你创建一个被称为关键格式的东西,它具有与真实规范相同的标准化目标,但仅限于你自己的环境。当然,目标是追求普遍性总是更好的。不仅因为,你永远不知道,但如果你投入足够的努力,并且其他人对此感兴趣,你的格式可能会成为规范(这是规范出现的方式:它总是从对业务领域极其了解的个人开始,他们努力将他们的知识转化为技术性的东西,然后其他参与者将其作为交换的合理基础达成一致)。而且因为追求普遍性将使你的关键格式尽可能接近规范,并带来尽可能多的优势。

对于这一点,规则是尽可能快地回归到现有的规范。当然,似乎不存在一个关于作者概念的国际化规范(尽管都柏林核心的creator属性允许我们从一个资源到创建该资源的个人或组织之间建立联系),但因为它指向个人,所以许多其他相关的规范会迅速适用,例如社会保险号码用于唯一标识,ISO 8601 用于创作日期,等等。同样的情况也适用于书籍:当然,我们可能找不到一个完美的标准来精确地满足我们样本应用的需求,特别是其持久化系统,但尽管如此,还是有关于语言的规范(ISO 639),以及用于注册书籍识别的国际公认标准代码,如国际标准书号(ISBNs),以及我们将在系统中记录的书籍描述中几乎一切事物的标准。

现在,真正的问题是什么应该放入书中以及作者的关键格式?这是一个如此重大的问题,以至于需要单独用一章来阐述。好消息是,下一章将解释如何回答这个问题。

摘要

在本章中,我采用了简短的历史方法(详细的方法本身就可以写成一本书)来解释在面向服务中涉及的风险是什么,以及这个看似简单却难以定义的“服务”一词在过去几十年是如何被实施的。我们肯定还没有到达故事的结尾,但如今,似乎最好的方法就是使用带有中间件的 REST API,尽可能通过使用规范和标准来减少。这不仅避免了昂贵的转换连接器,因为互动中的每个人都使用同一种语言,而且还帮助我们了解我们的设计是否正确,因为联盟和专家已经对这个业务领域进行了很多思考。

标准化的 API 使得今天在不破坏它们的情况下改变重要信息系统的一些部分变得容易。它们允许进行国际银行业务、更高效的保险系统、简化出国旅行,以及许多工业 IT 世界的成就。

我们讨论了规范,还讨论了兼容性、服务的演变、服务将通过接口如何集成,以及更多内容。到本章结束时,我们回到了我们的示例应用程序,并展示了将用于实现它将公开的一些服务的规范。现在,一个难题仍然存在:当没有针对商业需求的标准化格式,而我们又需要创建一个关键的格式(当然,尽可能使用规范来定义其内部属性)时,我们如何确定这个格式的内容?我给出的最佳答案是使用领域驱动设计DDD)。这正是下一章的主题。

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

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

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

如果你这样做,你很快就会对业务领域中使用的词汇有所疑问。技术方法有一个巨大的缺点,那就是限制了方法。但我们应该至少认识到,它迫使我们非常精确,因为计算机就像一箱石头一样愚蠢,它们强迫我们在信息指定上明确表达。考虑语义并使用称为领域驱动设计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的数据表,这可能会引起很多麻烦。

图 9.1 – 错误的语义

图 9.1 – 错误的语义

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

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

图 9.2 – 正确的语义

图 9.2 – 正确的语义

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

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

DDD

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

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

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

  • 探索与实验

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

  • 明确的上下文边界

  • 专注于核心领域

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

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

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

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

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

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

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

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

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

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

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

在指代应用中使用实体

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

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

图 9.4 – 指代的花的隐喻

图 9.4 – 指代的花的隐喻

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

管理实体的生命周期

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

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

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

图 9.5 – 书籍的生命周期

图 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 集群。这里的目的是不详细说明这些方法,只是解释它们将收集源数据中的所有事件,然后智能地计算聚合数据中的后果(智能之处在于它们知道后果,只计算所需的,并且可以在集群的许多机器上平衡这些计算,并在最后分组结果)。它们甚至可以进一步到在聚合数据上生成最终报告,并将其提供给最终用户,实现完整的“推送”范式。

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

图 10.1 – 报告模式

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

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

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

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

克隆架构

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

重要注意事项

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

集成和分布式架构

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

图 10.7 – 集成引用架构

图 10.7 – 集成引用架构

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

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

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

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

图 10.8 – 分布式引用架构

图 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属性以获得更快的结果。

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

其他类型的参照架构

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

图 10.9 – 虚拟参照架构

图 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 步 – 相同的基础设施但没有链接

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

第 7 步 – 统一访问

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

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

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

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

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

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

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

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

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

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

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

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

图 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. 团队继续使用现有的流程工作,现实中没有观察到任何改进。

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

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

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

业务流程建模

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

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

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

图 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 图开始:

图 11.2 – 最简单的 BPMN 图

图 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 标准中,可以表示一个业务流程,其中活动以非常通用的方式描述,如下所示:

图 11.3 – 收缩子流程的表示

图 11.3 – 收缩子流程的表示

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

图 11.4 – 子流程的扩展表示

图 11.4 – 子流程的扩展表示

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

图 11.5 – 不同类型的任务

图 11.5 – 不同类型的任务

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

图 11.6 – 专门化事件的示例

图 11.6 – 专门化事件的示例

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

图 11.7 – 在流程中使用泳道

图 11.7 – 在流程中使用泳道

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

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

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

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

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

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

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

图 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业务流程的第二个例子添加计数器:

图 11.11 – BAM 的示例

图 11.11 – BAM 的示例

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

  • 100 个流程实例已被启动

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

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

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

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

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

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

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

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

业务流程模拟和优化

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

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

图 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 的三个级别旁边,为它们提供技术覆盖:

图 11.13 – API 的三个级别

图 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 在这里也非常有用。

图 11.14 – Zapier 示例

图 11.14 – Zapier 示例

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

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

舞台编排而非编排

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

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

图 11.15 – 自动化流程示例

图 11.15 – 自动化流程示例

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

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

图 11.16 – 通过编排自动化

图 11.16 – 通过编排自动化

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

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

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

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

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

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

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

图 11.18 – 低耦合编排

图 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 中存在一种与业务规则直接相关的任务类型:

图 12.1 – 业务规则任务

图 12.1 – 业务规则任务

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

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

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

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

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

图 12.3 – 决策表

图 12.3 – 决策表

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

图 12.4 – 简化的 BPMN 流程

图 12.4 – 简化的 BPMN 流程

注意

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

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

图 12.5 – 扩展的决策表

图 12.5 – 扩展的决策表

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

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

图 12.6 – 决策图的示例

图 12.6 – 决策图的示例

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

图 12.7 – 扩展的决策图

图 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(之前引用的文档为具有不同授权配置文件的用户提供了其他凭据,以及如何设置生产就绪的授权)进行连接。

图 12.8 – JPBM 登录页面

图 12.8 – JPBM 登录页面

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

图 12.9 – JPBM 欢迎页面

图 12.9 – JPBM 欢迎页面

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

图 12.10 – JPBM 空间的列表

图 12.10 – JPBM 空间的列表

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

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

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

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

图 12.12 – 选择示例项目

图 12.12 – 选择示例项目

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

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

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

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

图 12.14 – 一个示例 DMN 决策图

图 12.14 – 一个示例 DMN 决策图

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

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

图 12.15 – 一个示例 DMN 决策表

图 12.15 – 一个示例 DMN 决策表

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

图 12.16 – JBPM 构建和部署菜单

图 12.16 – JBPM 构建和部署菜单

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

图 12.17 – JBPM 部署成功通知

图 12.17 – JBPM 部署成功通知

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

图 12.18 – JBPM 服务器管理界面

图 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表示应该提供给规则引擎以执行其值的值。界面应如下所示:

图 12.19 – 一个示例 Postman 调用

图 12.19 – 一个示例 Postman 调用

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

图 12.20 – Postman 认证设置

图 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 进行测试,但如果你点击违规场景资产,你将被带到这个很棒的界面,在那里你可以执行初步测试,以确保在部署之前一切正常:

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

图 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)的不同子域以及我们在其中如何命名事物是很重要的。让我们从与识别(你是谁)和认证(你如何证明你的身份)相关的概念开始:

图 13.1 – 识别和认证语义

图 13.1 – 识别和认证语义

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

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

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

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

图 13.2 – 授权语义

图 13.2 – 授权语义

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

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

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

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

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

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

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

IAM 实施的偏离

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

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

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

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

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

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

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

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

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

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

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

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

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

图 13.6 – IAM 责任的正确映射

图 13.6 – IAM 责任的正确映射

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

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

图 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 部门永远不要成为应用程序的功能管理员,而只是它们安装的机器的管理员(这并不阻止他们间接地查看或操作数据,但这又是另一个应该通过合同标准和行政行为的完全可追溯性来解决的问题)。

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

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

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

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

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

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

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

图 13.10 – 记录的 RBAC 方法

图 13.10 – 记录的 RBAC 方法

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

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

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

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

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

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

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

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

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

图 13.13 – ABAC 方法

图 13.13 – ABAC 方法

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

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

图 13.14 – 带有 BRMS 的 ABAC 实现

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

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

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

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

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

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

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

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

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

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

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

图 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 仅仅是代码库本身。

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

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

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

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

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

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

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

图 13.19 – 服务粒度优缺点

图 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 作为底层测试框架,但当然,您可以根据自己的喜好进行修改:

图 13.20 – 创建 SpecFlow 项目

图 13.20 – 创建 SpecFlow 项目

创建的项目结构将基于一个名为 Calculator 的示例,并且第一个动作是将名称更改为符合我们自己的目的,即测试 OPA:

图 13.21 – SpecFlow 项目结构

图 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 快捷键来显示测试资源管理器。测试可能最初不会显示,您可能需要运行解决方案生成来使它们出现。一旦它们显示出来,您可以逐个或同时运行场景,如果一切正常,它们应该确认规则按预期工作,并在圆圈上显示勾选标记:

图 13.22 – SpecFlow 测试的结果

图 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 等标准,当然是一个重要步骤,但我们还将以标准方式展示电子文档管理系统,并讨论许多其他外部服务。最后,将展示业务流程的外部执行,包括编排和协奏范式。

本部分包含以下章节:

  • 第十四章, 分解功能职责

  • 第十五章, 插入标准外部模块

  • 第十六章, 创建只写数据引用服务

  • 第十七章, 向数据引用服务添加查询

  • 第十八章, 部署数据引用服务

  • 第十九章, 设计第二个数据引用服务

  • 第二十章, 创建图形用户界面

  • 第二十一章, 扩展接口

  • 第二十二章, 集成业务流程

posted @ 2025-10-23 15:08  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报