buy the book ribbon

尾声:尾声

现在怎么办?

哎哟!我们在这本书中涵盖了很多内容,对于我们的大多数读者来说,所有这些想法都是新的。考虑到这一点,我们不能指望让您成为这些技术的专家。我们真正能做的只是向您展示大致的想法,以及足够的代码,让您继续从头开始编写一些东西。

本书中展示的代码不是经过实战考验的生产代码:它是一组乐高积木,您可以用来搭建您的第一栋房子、宇宙飞船和摩天大楼

这给我们留下了两个重要的任务。我们想谈谈如何在现有系统中真正开始应用这些想法,并且我们需要警告您一些我们不得不跳过的事情。我们给了您一套全新的自掘坟墓的方法,所以我们应该讨论一些基本的枪支安全知识。

我如何从这里到达那里?

很可能你们中的很多人都在想这样的事情

“好的,Bob 和 Harry,这一切都很好,如果我被聘用从事一个全新的绿地服务,我知道该怎么做。但与此同时,我在这里处理我那团巨大的 Django 泥潭,我看不出任何方法可以到达你们那种漂亮、干净、完美、未受污染、简单的模型。从这里不可能。”

我们听到了您的声音。一旦您已经构建了一团巨大的泥潭,就很难知道如何开始改进。实际上,我们需要逐步解决问题。

首先要弄清楚:您要解决什么问题?软件是否太难更改?性能是否无法接受?您是否有奇怪、莫名其妙的错误?

心中有一个明确的目标将有助于您确定需要完成的工作的优先级,更重要的是,将这样做的理由传达给团队的其他成员。企业往往对技术债务和重构采取务实的态度,只要工程师能够为修复问题提出合理的论据。

提示
如果您将其与功能工作联系起来,则对系统进行复杂更改通常更容易推销。也许您正在推出新产品或向新市场开放您的服务?现在是花费工程资源来修复基础架构的正确时机。对于一个为期六个月的交付项目,更容易为三周的清理工作提出论据。Bob 将此称为架构税

分离纠缠的职责

在本书的开头,我们说过,一团巨大的泥潭的主要特征是同质性:系统的每个部分看起来都一样,因为我们没有明确每个组件的职责。为了解决这个问题,我们需要开始分离职责并引入清晰的边界。我们可以做的第一件事是开始构建服务层(协作系统的领域)。

apwp ep01
图 1. 协作系统的领域
[plantuml, apwp_ep01, config=plantuml.cfg]
@startuml
scale 4
hide empty members

Workspace *- Folder : contains
Account *- Workspace : owns
Account *-- Package : has
User *-- Account : manages
Workspace *-- User : has members
User *-- Document : owns
Folder *-- Document : contains
Document *- Version: has
User *-- Version: authors
@enduml

这是 Bob 第一次学习如何分解泥潭的系统,而且它确实是一个难题。到处都是逻辑——在网页中、在管理器对象中、在助手程序中、在我们编写的用于抽象管理器和助手程序的胖服务类中,以及在我们编写的用于分解服务的复杂命令对象中。

如果您正在一个已经达到这种程度的系统中工作,情况可能会让人感到绝望,但开始清理杂草丛生的花园永远不会太晚。最终,我们聘请了一位懂得如何操作的架构师,他帮助我们重新控制了局面。

首先弄清楚您系统的用例。如果您有用户界面,它会执行哪些操作?如果您有后端处理组件,那么每个 cron 作业或 Celery 作业可能都是一个单独的用例。您的每个用例都需要有一个祈使式的名称:应用计费费用、清理废弃帐户或开具采购订单,例如。

在我们的例子中,我们的大多数用例都是管理器类的一部分,并且具有诸如创建工作区或删除文档版本之类的名称。每个用例都是从 Web 前端调用的。

我们的目标是为每个受支持的操作创建一个单独的函数或类,该函数或类负责协调要完成的工作。每个用例都应执行以下操作

  • 如果需要,启动自己的数据库事务

  • 获取任何所需数据

  • 检查任何先决条件(请参阅 [appendix_validation] 中的 Ensure 模式)

  • 更新领域模型

  • 持久化任何更改

每个用例都应该作为一个原子单元成功或失败。您可能需要从另一个用例调用一个用例。这没关系;只需记下它,并尽量避免长时间运行的数据库事务。

注意
我们遇到的最大问题之一是管理器方法调用其他管理器方法,并且数据访问可能发生在模型对象本身。如果不进行遍布代码库的寻宝,就很难理解每个操作的作用。将所有逻辑拉到一个单独的方法中,并使用 UoW 来控制我们的事务,使系统更容易理解。
案例研究:为杂草丛生的系统分层

多年前,Bob 曾为一家软件公司工作,该公司将其应用程序的第一个版本(一个用于共享和处理文件的在线协作平台)外包出去。

当公司将开发工作转移到内部时,它经历了多代开发人员之手,每一波新的开发人员都为代码的结构增加了更多的复杂性。

该系统的核心是一个使用 NHibernate ORM 构建的 ASP.NET Web Forms 应用程序。用户可以将文档上传到工作区,在工作区中,他们可以邀请其他工作区成员查看、评论或修改他们的工作。

应用程序的大部分复杂性都在权限模型中,因为每个文档都包含在一个文件夹中,并且文件夹允许读取、写入和编辑权限,很像 Linux 文件系统。

此外,每个工作区都属于一个帐户,并且该帐户通过计费包附加了配额。

因此,针对文档的每个读取或写入操作都必须从数据库加载大量对象,以便测试权限和配额。创建一个新的工作区涉及到数百个数据库查询,因为我们设置了权限结构、邀请了用户并设置了示例内容。

一些操作的代码在 Web 处理程序中,这些处理程序在用户单击按钮或提交表单时运行;一些代码在管理器对象中,这些对象保存用于协调工作的代码;还有一些代码在领域模型中。模型对象会进行数据库调用或复制磁盘上的文件,并且测试覆盖率非常糟糕。

为了解决这个问题,我们首先引入了一个服务层,以便所有用于创建文档或工作区的代码都集中在一个位置,并且可以被理解。这涉及到将数据访问代码从领域模型中拉出并放入命令处理程序中。同样,我们将协调代码从管理器和 Web 处理程序中拉出,并将其推送到处理程序中。

生成的命令处理程序很长且混乱,但我们已经开始为混乱引入秩序。

提示
如果用例函数中存在重复,那也没关系。我们不是要编写完美的代码;我们只是试图提取一些有意义的层。在几个地方复制一些代码比让用例函数在一个长链中相互调用要好。

这是一个将任何数据访问或协调代码从领域模型中拉出并放入用例的好机会。我们还应该尝试将 I/O 问题(例如,发送电子邮件、写入文件)从领域模型中拉出并放入用例函数中。我们应用了 [chapter_03_abstractions] 中关于抽象的技术,以使我们的处理程序即使在执行 I/O 时也保持单元可测试性。

这些用例函数主要关于日志记录、数据访问和错误处理。一旦您完成了这一步,您将掌握您的程序实际做什么,以及一种确保每个操作都有明确定义的开始和结束的方法。我们将朝着构建纯领域模型迈出一步。

阅读 Michael C. Feathers 的 Working Effectively with Legacy Code (Prentice Hall),以获得关于测试遗留代码和开始分离职责的指导。

识别聚合和有界上下文

我们的案例研究中代码库的问题之一是对象图高度连接。每个帐户都有许多工作区,每个工作区都有许多成员,所有成员都有自己的帐户。每个工作区包含许多文档,这些文档有许多版本。

您无法在类图中表达事物的完整恐怖程度。首先,实际上没有与用户相关的单个帐户。相反,有一个奇怪的规则,要求您枚举通过工作区与用户关联的所有帐户,并采用创建日期最早的帐户。

系统中的每个对象都是继承层次结构的一部分,该层次结构包括 SecureObjectVersion。此继承层次结构直接反映在数据库架构中,因此每个查询都必须跨 10 个不同的表进行连接,并查看鉴别符列,才能知道您正在处理的对象类型。

代码库使您可以轻松地像这样“点”遍这些对象

user.account.workspaces[0].documents.versions[1].owner.account.settings[0];

使用 Django ORM 或 SQLAlchemy 以这种方式构建系统很容易,但要避免。虽然它很方便,但它使性能推理变得非常困难,因为每个属性都可能触发对数据库的查找。

提示
聚合是一个一致性边界。一般来说,每个用例应该一次更新一个聚合。一个处理程序从仓库中获取一个聚合,修改其状态,并引发由此产生的任何事件。如果您需要来自系统另一部分的数据,使用读取模型完全没问题,但要避免在单个事务中更新多个聚合。当我们选择将代码分离到不同的聚合中时,我们明确选择使它们彼此之间最终一致

许多操作需要我们以这种方式循环遍历对象——例如

# Lock a user's workspaces for nonpayment

def lock_account(user):
    for workspace in user.account.workspaces:
        workspace.archive()

甚至递归遍历文件夹和文档的集合

def lock_documents_in_folder(folder):

    for doc in folder.documents:
         doc.archive()

     for child in folder.children:
         lock_documents_in_folder(child)

这些操作扼杀了性能,但修复它们意味着放弃我们的单个对象图。相反,我们开始识别聚合并打破对象之间的直接链接。

注意
我们在 [chapter_12_cqrs] 中讨论了臭名昭著的 SELECT N+1 问题,以及当读取用于查询的数据与读取用于命令的数据时,我们可能选择使用不同的技术。

我们主要通过用标识符替换直接引用来做到这一点。

聚合前

apwp ep02
[plantuml, apwp_ep02, config=plantuml.cfg]
@startuml
scale 4
hide empty members

together {
    class Document {
      add_version()
      workspace: Workspace
      parent: Folder
      versions: List[DocumentVersion]

    }

    class DocumentVersion {
      title : str
      version_number: int
      document: Document

    }
    class Folder {
      parent: Workspace
      children: List[Folder]
      copy_to(target: Folder)
      add_document(document: Document)
    }
}

together {
    class User {
      account: Account
    }


    class Account {
      add_package()
      owner : User
      packages : List[BillingPackage]
      workspaces: List[Workspace]
    }
}


class BillingPackage {
}

class Workspace {
  add_member(member: User)
  account: Account
  owner: User
  members: List[User]
}



Account --> Workspace
Account -left-> BillingPackage
Account -right-> User
Workspace --> User
Workspace --> Folder
Workspace --> Account
Folder --> Folder
Folder --> Document
Folder --> Workspace
Folder --> User
Document -right-> DocumentVersion
Document --> Folder
Document --> User
DocumentVersion -right-> Document
DocumentVersion --> User
User -left-> Account

@enduml

使用聚合建模后

apwp ep03
[plantuml, apwp_ep03, config=plantuml.cfg]
@startuml
scale 4
hide empty members

frame Document {

  class Document {

    add_version()

    workspace_id: int
    parent_folder: int

    versions: List[DocumentVersion]

  }

  class DocumentVersion {

    title : str
    version_number: int

  }
}

frame Account {

  class Account {
    add_package()

    owner : int
    packages : List[BillingPackage]
  }


  class BillingPackage {
  }

}

frame Workspace {
   class Workspace {

     add_member(member: int)

     account_id: int
     owner: int
     members: List[int]

   }
}

frame Folder {

  class Folder {
    workspace_id : int
    children: List[int]

    copy_to(target: int)
  }

}

Document o-- DocumentVersion
Account o-- BillingPackage

@enduml
提示
双向链接通常是您的聚合不正确的标志。在我们原始代码中,Document 知道其包含的 Folder,而 Folder 具有 Documents 的集合。这使得遍历对象图变得容易,但阻止我们正确思考我们所需的一致性边界。我们通过使用引用来分解聚合。在新模型中,Document 引用了其 parent_folder,但无法直接访问 Folder

如果我们需要读取数据,我们避免编写复杂的循环和转换,并尝试用直接 SQL 替换它们。例如,我们的一个屏幕是文件夹和文档的树视图。

这个屏幕对数据库来说非常繁重,因为它依赖于嵌套的 for 循环,这些循环触发了延迟加载的 ORM。

提示
我们在 [chapter_12_cqrs] 中使用了相同的技术,我们在其中用简单的 SQL 查询替换了 ORM 对象上的嵌套循环。这是 CQRS 方法的第一步。

经过大量的绞尽脑汁,我们用一个又大又丑陋的存储过程替换了 ORM 代码。代码看起来很糟糕,但速度更快,并有助于打破 FolderDocument 之间的链接。

当我们需要写入数据时,我们一次更改一个聚合,并引入消息总线来处理事件。例如,在新模型中,当我们锁定一个帐户时,我们可以首先通过 SELECT id FROM workspace WHERE account_id = ? 查询所有受影响的工作区。

然后我们可以为每个工作区引发一个新命令

for workspace_id in workspaces:
    bus.handle(LockWorkspace(workspace_id))

通过绞杀者模式转向微服务的事件驱动方法

绞杀榕模式涉及在旧系统边缘周围创建一个新系统,同时保持旧系统运行。旧功能的一部分逐渐被拦截和替换,直到旧系统完全不做任何事情并且可以关闭。

在构建可用性服务时,我们使用了一种称为事件拦截的技术,将功能从一个地方移动到另一个地方。这是一个三步过程

  1. 引发事件以表示您要替换的系统中发生的更改。

  2. 构建第二个系统,该系统使用这些事件并使用它们来构建自己的领域模型。

  3. 用新的系统替换旧的系统。

我们使用事件拦截从 之前:基于 XML-RPC 的强双向耦合…​

apwp ep04
图 2. 之前:基于 XML-RPC 的强双向耦合
[plantuml, apwp_ep04, config=plantuml.cfg]
@startuml Ecommerce Context
!include images/C4_Context.puml

LAYOUT_LEFT_RIGHT
scale 2

Person_Ext(customer, "Customer", "Wants to buy furniture")

System(fulfillment, "Fulfillment System", "Manages order fulfillment and logistics")
System(ecom, "Ecommerce website", "Allows customers to buy furniture")

Rel(customer, ecom, "Uses")
Rel(fulfillment, ecom, "Updates stock and orders", "xml-rpc")
Rel(ecom, fulfillment, "Sends orders", "xml-rpc")

@enduml
apwp ep05
图 3. 之后:使用异步事件的松耦合(您可以在 cosmicpython.com 上找到此图的高分辨率版本)
[plantuml, apwp_ep05, config=plantuml.cfg]
@startuml Ecommerce Context
!include images/C4_Context.puml

LAYOUT_LEFT_RIGHT
scale 2

Person_Ext(customer, "Customer", "Wants to buy furniture")

System(av, "Availability Service", "Calculates stock availability")
System(fulfillment, "Fulfillment System", "Manages order fulfillment and logistics")
System(ecom, "Ecommerce website", "Allows customers to buy furniture")

Rel(customer, ecom, "Uses")
Rel(customer, av, "Uses")
Rel(fulfillment, av, "Publishes batch_created", "events")
Rel(av, ecom, "Publishes out_of_stock", "events")
Rel(ecom, fulfillment, "Sends orders", "xml-rpc")

@enduml

实际上,这是一个为期数月的项目。我们的第一步是编写一个可以表示批次、发货和产品的领域模型。我们使用 TDD 构建了一个玩具系统,该系统可以回答一个简单的问题:“如果我想要 N 个单位的 HAZARDOUS_RUG,它们需要多长时间才能交付?”

提示
在部署事件驱动系统时,从“可行骨架”开始。部署一个仅记录其输入的系统迫使我们解决所有基础设施问题,并开始在生产环境中工作。
案例研究:划分微服务以替换域

MADE.com 最初有两个单体应用:一个用于前端电子商务应用程序,一个用于后端履行系统。

这两个系统通过 XML-RPC 进行通信。后端系统会定期唤醒并查询前端系统,以了解新订单。当它导入所有新订单后,它会发送 RPC 命令来更新库存水平。

随着时间的推移,这种同步过程变得越来越慢,直到圣诞节,导入一天的订单花费的时间超过了 24 小时。Bob 被聘请来将系统分解为一组事件驱动的服务。

首先,我们确定流程中最慢的部分是计算和同步可用库存。我们需要的是一个可以监听外部事件并保持可用库存总量持续更新的系统。

我们通过 API 公开了该信息,以便用户的浏览器可以询问每个产品的可用库存量以及交付到其地址需要多长时间。

每当某个产品完全缺货时,我们都会引发一个新事件,电子商务平台可以使用该事件将产品下架。因为我们不知道我们需要处理多少负载,所以我们使用 CQRS 模式编写了该系统。每当库存量发生变化时,我们都会使用缓存的视图模型更新 Redis 数据库。我们的 Flask API 查询这些视图模型,而不是运行复杂的领域模型。

因此,我们可以在 2 到 3 毫秒内回答“有多少库存可用?”这个问题,现在 API 经常在持续一段时间内每秒处理数百个请求。

如果这一切听起来有点熟悉,那么,现在您知道我们的示例应用程序来自哪里了!

一旦我们有了一个可用的领域模型,我们就转向构建一些基础设施组件。我们的第一个生产部署是一个微型系统,它可以接收 batch_created 事件并记录其 JSON 表示形式。这是事件驱动架构的“Hello World”。它迫使我们部署消息总线、连接生产者和消费者、构建部署管道并编写简单的消息处理程序。

有了部署管道、我们所需的基础设施和基本的领域模型,我们就开始了。几个月后,我们投入生产并为真实客户提供服务。

说服您的利益相关者尝试新事物

如果您正在考虑从一团巨大的泥潭中划分出一个新系统,您可能同时遇到可靠性、性能、可维护性或所有这三个方面的问题。根深蒂固、棘手的问题需要采取激烈的措施!

我们建议将领域建模作为第一步。在许多杂草丛生的系统中,工程师、产品负责人和客户不再使用相同的语言。业务利益相关者以抽象的、以流程为中心的术语谈论系统,而开发人员则被迫以系统在其狂野而混乱的状态下实际存在的形式谈论系统。

案例研究:用户模型

我们之前提到过,我们第一个系统中的帐户和用户模型通过“奇怪的规则”绑定在一起。这是一个工程和业务利益相关者如何分道扬镳的完美例子。

在这个系统中,帐户工作区 的父级,而用户是 工作区成员。工作区是应用权限和配额的基本单元。如果用户加入工作区并且尚未拥有帐户,我们会将他们与拥有该工作区的帐户关联起来。

这很混乱且临时,但在产品负责人要求一个新功能之前,它运行良好

当用户加入公司时,我们希望将他们添加到公司的一些默认工作区,例如 HR 工作区或公司公告工作区。

我们不得不向他们解释,没有公司这种东西,并且用户加入帐户没有任何意义。此外,“公司”可能拥有由不同用户拥有的多个帐户,并且新用户可能会被邀请加入其中任何一个帐户。

多年来,对破损模型添加黑客行为和解决方法最终赶上了我们,我们不得不将整个用户管理功能重写为一个全新的系统。

弄清楚如何建模您的领域是一项复杂的任务,这是许多优秀书籍的主题。我们喜欢使用交互式技术,如事件风暴和 CRC 建模,因为人类擅长通过游戏进行协作。事件建模是另一种将工程师和产品负责人聚集在一起,以命令、查询和事件的形式理解系统的技术。

提示
查看 www.eventmodeling.orgwww.eventstorming.com,以获取一些关于使用事件对系统进行可视化建模的优秀指南。

目标是能够使用相同的通用语言来谈论系统,以便您可以就复杂性所在达成一致。

我们发现将领域问题视为 TDD kata 非常有价值。例如,我们为可用性服务编写的第一个代码是批次和订单行模型。您可以将其视为午餐时间研讨会,或作为项目开始时的快速原型。一旦您可以证明建模的价值,就更容易为构建项目以优化建模提出论据。

案例研究:David Seddon 谈论采取小步骤

大家好,我是 David,本书的技术审阅者之一。我曾在多个复杂的 Django 单体应用上工作过,所以我知道 Bob 和 Harry 所做的各种关于缓解痛苦的宏伟承诺的痛苦。

当我第一次接触到这里描述的模式时,我非常兴奋。我已经成功地在较小的项目中使用了一些技术,但这是一个针对更大的、数据库支持的系统(例如我在日常工作中使用的系统)的蓝图。所以我开始尝试弄清楚如何在我当前组织中实施该蓝图。

我选择解决代码库中一直困扰我的一个问题领域。我首先将其实现为一个用例。但我发现自己遇到了意想不到的问题。有些事情是我在阅读时没有考虑过的,现在让我很难看出该怎么做。如果我的用例与两个不同的聚合交互,这有问题吗?一个用例可以调用另一个用例吗?它将如何在遵循不同架构原则的系统中存在,而不会导致可怕的混乱?

那个如此有前景的蓝图怎么了?我真的足够理解这些想法,可以付诸实践吗?它甚至适合我的应用程序吗?即使适合,我的同事会同意如此重大的改变吗?这些只是让我幻想一下的好想法,而我却在继续真实的生活吗?

我花了一段时间才意识到我可以从小处着手。我不需要成为一个纯粹主义者,也不需要第一次就“做对”:我可以进行实验,找到适合我的方法。

所以这就是我所做的。我已经在一些地方应用了 一些 想法。我已经构建了新的功能,其业务逻辑可以在没有数据库或模拟的情况下进行测试。作为一个团队,我们引入了一个服务层,以帮助定义系统所做的工作。

如果您开始尝试在您的工作中应用这些模式,您最初可能会经历类似的感觉。当书本上美好的理论与您的代码库的现实相遇时,可能会令人沮丧。

我的建议是专注于一个具体的问题,并问问自己如何使用相关的想法,也许是以最初有限且不完美的方式。您可能会发现,就像我一样,您选择的第一个问题可能有点太难了;如果是这样,请转到其他问题。不要试图煮沸海洋,也不要害怕犯错误。这将是一次学习经历,您可以确信您大致朝着其他人发现有用的方向前进。

因此,如果您也感到痛苦,请尝试这些想法。不要觉得您需要获得重新架构一切的许可。只需寻找一个可以从小处开始的地方。最重要的是,这样做是为了解决一个具体的问题。如果您成功地解决了它,您就会知道您做对了什么——其他人也会知道。

我们的技术审阅者提出的我们无法融入散文中的问题

以下是我们起草期间听到的一些问题,我们找不到一个好的地方在本书的其他地方解决这些问题

我需要一次完成所有这些吗?我可以一次只做一点吗?

不,您绝对可以逐步采用这些技术。如果您有一个现有系统,我们建议构建一个服务层,以尝试将协调放在一个地方。一旦您有了它,就更容易将逻辑推送到模型中,并将边缘问题(如验证或错误处理)推送到入口点。

即使您仍然有一个庞大而混乱的 Django ORM,也值得拥有一个服务层,因为这是一种开始理解操作边界的方法。

提取用例会破坏我的许多现有代码;它太混乱了

只需复制和粘贴。短期内造成更多重复是可以的。将此视为一个多步骤过程。您的代码现在处于糟糕的状态,因此请将其复制并粘贴到新位置,然后使新代码干净整洁。

完成此操作后,您可以将旧代码的使用替换为对新代码的调用,最后删除混乱的代码。修复大型代码库是一个混乱而痛苦的过程。不要期望事情会立即好转,也不要担心您的应用程序的某些部分仍然很混乱。

我需要做 CQRS 吗?这听起来很奇怪。我不能只使用仓库吗?

当然可以!我们在本书中介绍的技术旨在使您的生活更轻松。它们不是某种惩罚自己的苦行纪律。

在工作区/文档案例研究系统中,我们有很多 View Builder 对象,这些对象使用仓库来获取数据,然后执行一些转换以返回哑读取模型。这样做的好处是,当您遇到性能问题时,可以轻松地重写视图构建器以使用自定义查询或原始 SQL。

用例应该如何在更大的系统中交互?一个用例调用另一个用例有问题吗?

这可能是一个过渡步骤。同样,在文档案例研究中,我们有需要调用其他处理程序的处理程序。但这变得非常混乱,最好是使用消息总线来分离这些问题。

通常,您的系统将具有一个单一的消息总线实现和许多子域,这些子域以特定的聚合或一组聚合为中心。当您的用例完成时,它可以引发一个事件,并且其他地方的处理程序可以运行。

用例使用多个仓库/聚合是否是代码异味?如果是,为什么?

聚合是一个一致性边界,因此,如果您的用例需要原子地(在同一事务中)更新两个聚合,那么严格来说,您的一致性边界是错误的。理想情况下,您应该考虑移动到一个新的聚合,该聚合包装了您想要同时更改的所有内容。

如果您实际上只更新一个聚合,并将另一个聚合用于只读访问,那么这没问题,尽管您可以考虑构建一个读取/视图模型来获取该数据——如果每个用例只有一个聚合,则会使事情更简洁。

如果您确实需要修改两个聚合,但这两个操作不必在同一事务/UoW 中,请考虑将工作拆分为两个不同的处理程序,并使用领域事件在两者之间传递信息。您可以在 Vaughn Vernon 的 关于聚合设计的论文 中阅读更多内容。

如果我有一个只读但业务逻辑繁重的系统怎么办?

视图模型可以包含复杂的逻辑。在本书中,我们鼓励您分离读取和写入模型,因为它们具有不同的 一致性和吞吐量要求。大多数情况下,我们可以对读取使用更简单的逻辑,但这并非总是如此。特别是,权限和授权模型可能会为我们的读取端增加很多复杂性。

我们编写的系统中,视图模型需要广泛的单元测试。在这些系统中,我们从 视图获取器 中分离出一个 视图构建器,如 视图构建器和视图获取器(您可以在 cosmicpython.com 上找到此图的高分辨率版本) 中所示。

apwp ep06
图 4. 视图构建器和视图获取器(您可以在 cosmicpython.com 上找到此图的高分辨率版本)
[plantuml, apwp_ep06, config=plantuml.cfg]
@startuml View Fetcher Component Diagram
!include images/C4_Component.puml

ComponentDb(db, "Database", "RDBMS")
Component(fetch, "View Fetcher", "Reads data from db, returning list of tuples or dicts")
Component(build, "View Builder", "Filters and maps tuples")
Component(api, "API", "Handles HTTP and serialization concerns")

Rel(api, build, "Invokes")
Rel_R(build, fetch, "Invokes")
Rel_D(fetch, db, "Reads data from")

@enduml

+ 这使得通过为其提供模拟数据(例如,字典列表)来轻松测试视图构建器。“花哨的 CQRS”与事件处理程序实际上是一种在每次写入时运行我们复杂的视图逻辑的方式,以便我们可以避免在读取时运行它。

我需要构建微服务来完成这些工作吗?

天哪,不需要!这些技术比微服务早了十年左右。聚合、领域事件和依赖倒置是控制大型系统复杂性的方法。只是当您为业务流程构建了一组用例和模型时,将其移动到自己的服务相对容易,但这并不是必需的。

我正在使用 Django。我还能做到这一点吗?

我们有一个专门为您准备的附录:[appendix_django]

自毁装置

好的,所以我们给了您一大堆新玩具来玩。这是细则。Harry 和 Bob 不建议您将我们的代码复制并粘贴到生产系统中,并在 Redis pub/sub 上重建您的自动化交易平台。出于简洁和简单的原因,我们回避了很多棘手的主题。以下是我们认为您在真正尝试之前应该知道的一些事项列表。

可靠的消息传递很难

Redis pub/sub 不可靠,不应将其用作通用消息传递工具。我们选择它是因为它既熟悉又易于运行。在 MADE,我们运行 Event Store 作为我们的消息传递工具,但我们也有使用 RabbitMQ 和 Amazon EventBridge 的经验。

Tyler Treat 在他的网站 bravenewgeek.com 上发表了一些优秀的博客文章;您至少应该阅读 “您无法实现精确一次交付”“您想要的是您不想要的:理解分布式消息传递中的权衡”

我们明确选择可以独立失败的小型、集中的事务

[chapter_08_events_and_message_bus] 中,我们更新了我们的流程,以便取消分配订单行和重新分配订单行发生在两个独立的工作单元中。您将需要监控来了解这些事务何时失败,以及用于重放事件的工具。使用事务日志作为您的消息代理(例如,Kafka 或 EventStore)可以使其中一些操作更容易。您还可以查看 发件箱模式

我们没有讨论幂等性

我们还没有真正考虑过当处理程序被重试时会发生什么。在实践中,你会希望使处理程序具有幂等性,这样用相同的消息重复调用它们不会对状态进行重复更改。这是构建可靠性的关键技术,因为它使我们能够在事件失败时安全地重试事件。

有很多关于幂等消息处理的优秀资料,可以尝试从 “如何在最终一致的 DDD/CQRS 应用程序中确保幂等性”“(消息传递中的)不可靠性” 开始阅读。

你的事件模式会随着时间推移需要改变

你需要找到某种方法来记录你的事件,并与消费者分享模式。我们喜欢使用 JSON schema 和 markdown,因为它很简单,但也有其他的现有技术。Greg Young 写了一整本书关于随着时间推移管理事件驱动系统:《事件溯源系统中的版本控制》(Leanpub)。

更多必读资料

还有一些我们想推荐的书籍,以帮助你入门

  • Leonardo Giordani 撰写的《Python 整洁架构》(Leanpub) 于 2019 年出版,是为数不多的关于 Python 应用程序架构的早期书籍之一。

  • Gregor Hohpe 和 Bobby Woolf 合著的《企业集成模式》(Addison-Wesley Professional) 是学习消息传递模式的一个很好的起点。

  • Sam Newman 撰写的《从单体到微服务》(O’Reilly),以及 Newman 的第一本书《构建微服务》(O’Reilly)。书中提到了 Strangler Fig 模式(绞杀榕模式)等多种模式,并将其列为最受欢迎的模式之一。如果你正在考虑迁移到微服务,或者想了解集成模式以及基于异步消息传递的集成的注意事项,这些书都值得一看。

总结

呼!有很多警告和阅读建议;我们希望没有完全吓到你。我们写这本书的目标是给你足够的知识和直觉,让你开始自己构建一些东西。我们很想了解你的进展,以及你在自己的系统中使用这些技术时遇到的问题,所以为什么不通过 www.cosmicpython.com 与我们联系呢?