From 582042d474caf25ad070d350338c9c56a0d5317b Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Wed, 29 Jan 2025 20:31:03 +0800
Subject: [PATCH 01/75] update Readme.md
---
.gitignore | 1 +
Readme.md | 50 +++++++++++++++++++++++++-------------------------
2 files changed, 26 insertions(+), 25 deletions(-)
diff --git a/.gitignore b/.gitignore
index 2d069b63..324cdcdc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ acknowledgements.html
epilogue_1_how_to_get_there_from_here.html
epilogue_2_footguns.html
images/*.html
+.idea/
diff --git a/Readme.md b/Readme.md
index 37b7dba3..7f0c662c 100644
--- a/Readme.md
+++ b/Readme.md
@@ -10,31 +10,31 @@
O'Reilly have generously said that we will be able to publish this book under a [CC license](license.txt),
In the meantime, pull requests, typofixes, and more substantial feedback + suggestions are enthusiastically solicited.
-| Chapter | |
-| ------- | ----- |
-| [Preface](preface.asciidoc) | |
-| [Introduction: Why do our designs go wrong?](introduction.asciidoc)| ||
-| [**Part 1 Intro**](part1.asciidoc) | |
-| [Chapter 1: Domain Model](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 2: Repository](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 3: Interlude: Abstractions](chapter_03_abstractions.asciidoc) | |
-| [Chapter 4: Service Layer (and Flask API)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 5: TDD in High Gear and Low Gear](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 6: Unit of Work](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 7: Aggregates](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [**Part 2 Intro**](part2.asciidoc) | |
-| [Chapter 8: Domain Events and a Simple Message Bus](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 9: Going to Town on the MessageBus](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 10: Commands](chapter_10_commands.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 11: External Events for Integration](chapter_11_external_events.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 12: CQRS](chapter_12_cqrs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 13: Dependency Injection](chapter_13_dependency_injection.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Epilogue: How do I get there from here?](epilogue_1_how_to_get_there_from_here.asciidoc) | |
-| [Appendix A: Recap table](appendix_ds1_table.asciidoc) | |
-| [Appendix B: Project Structure](appendix_project_structure.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Appendix C: A major infrastructure change, made easy](appendix_csvs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Appendix D: Django](appendix_django.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Appendix F: Validation](appendix_validation.asciidoc) | |
+| Chapter | |
+|---------------------------------------------------------------------------------------------------------------------| ----- |
+| [Preface
前言(翻译中...)](preface.asciidoc) | |
+| [Introduction: Why do our designs go wrong?
引言:为什么我们的设计会出错?(未翻译)](introduction.asciidoc) | ||
+| [**Part 1 Intro**](part1.asciidoc) | |
+| [Chapter 1: Domain Model
第一章:领域模型(未翻译)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 2: Repository
第二章:仓储(未翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(未翻译)](chapter_03_abstractions.asciidoc) | |
+| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(未翻译)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(未翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 6: Unit of Work
第六章:工作单元(未翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 7: Aggregates
第七章:聚合(未翻译)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [**Part 2 Intro**](part2.asciidoc) | |
+| [Chapter 8: Domain Events and a Simple Message Bus
第八章:领域事件与简单消息总线(未翻译)](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 9: Going to Town on the MessageBus
第九章:深入探讨消息总线(未翻译)](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 10: Commands
第十章:命令(未翻译)](chapter_10_commands.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 11: External Events for Integration
第十一章:集成外部事件(未翻译)](chapter_11_external_events.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 12: CQRS
第十二章:命令查询责任分离(未翻译)](chapter_12_cqrs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 13: Dependency Injection
第十三章:依赖注入(未翻译)](chapter_13_dependency_injection.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Epilogue: How do I get there from here?
尾声:我该如何从这里开始?(未翻译)](epilogue_1_how_to_get_there_from_here.asciidoc) | |
+| [Appendix A: Recap table
附录A:总结表格(未翻译)](appendix_ds1_table.asciidoc) | |
+| [Appendix B: Project Structure
附录B:项目结构(未翻译)](appendix_project_structure.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Appendix C: A major infrastructure change, made easy
附录C:轻松实现重大基础设施更改(未翻译)](appendix_csvs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Appendix D: Django
附录D:Django(未翻译)](appendix_django.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Appendix F: Validation
附录F:验证(未翻译)](appendix_validation.asciidoc) | |
From e9421e8d49db124e4210b098f7e543d6d04141c0 Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Wed, 29 Jan 2025 21:20:01 +0800
Subject: [PATCH 02/75] update preface.asciidoc
---
preface.asciidoc | 179 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 179 insertions(+)
diff --git a/preface.asciidoc b/preface.asciidoc
index 3c740cd8..6c79d1bc 100644
--- a/preface.asciidoc
+++ b/preface.asciidoc
@@ -1,9 +1,12 @@
[[preface]]
[preface]
== Preface
+前言
You may be wondering who we are and why we wrote this book.
+您可能会好奇我们是谁,以及为什么要编写这本书。
+
At the end of Harry's last book,
http://www.obeythetestinggoat.com[_Test-Driven Development with Python_] (O'Reilly),
he found himself asking a bunch of questions about architecture, such as,
@@ -14,14 +17,26 @@ He made vague references to "Hexagonal Architecture" and "Ports and Adapters"
and "Functional Core, Imperative Shell," but if he was honest, he'd have to
admit that these weren't things he really understood or had done in practice.
+在 Harry 的上一本书
+http://www.obeythetestinggoat.com[_使用 Python 的测试驱动开发_](O'Reilly)
+的结尾,他发现自己对架构提出了一堆问题,例如:如何构建应用程序结构才能更容易进行测试?
+更具体地说,如何确保核心业务逻辑可以被单元测试覆盖,并尽可能减少集成测试和端到端测试的数量?
+他模糊地提到了“六边形架构”“端口与适配器”“功能核心,命令式外壳”等概念,但如果要诚实地说,
+他得承认这些不是他真正理解或在实践中实现过的东西。
+
And then he was lucky enough to run into Bob, who has the answers to all these
questions.
+然后他很幸运地遇到了鲍勃,而鲍勃对这些问题都有答案。
+
Bob ended up as a software architect because nobody else on his team was
doing it. He turned out to be pretty bad at it, but _he_ was lucky enough to run
into Ian Cooper, who taught him new ways of writing and thinking about code.
+他起初对此并不擅长,不过 _他_ 很幸运,遇到了 Ian Cooper,是他教会了 Bob 一些关于编写代码和思考代码的新方法。
+
=== Managing Complexity, Solving Business Problems
+管理复杂性,解决业务问题
We both work for MADE.com, a European ecommerce company that sells furniture
online; there, we apply the techniques in this book to build distributed systems
@@ -29,10 +44,17 @@ that model real-world business problems. Our example domain is the first system
Bob built for MADE, and this book is an attempt to write down all the _stuff_ we
have to teach new programmers when they join one of our teams.
+我们都供职于 MADE.com,这是一家欧洲的电子商务公司,在线销售家具。在那里,我们应用本书中的技术来构建分布式系统,
+以模拟现实世界中的业务问题。我们的示例领域是 Bob 为 MADE 构建的第一个系统,
+而本书则是我们将所有需要教授新程序员的 _内容_ 记录下来的尝试,当他们加入我们的团队时,这些内容都会用到。
+
MADE.com operates a global supply chain of freight partners and manufacturers.
To keep costs low, we try to optimize the delivery of stock to our
warehouses so that we don't have unsold goods lying around the place.
+MADE.com运营着由货运合作伙伴和制造商组成的全球供应链。为了保持低成本,
+我们努力优化库存商品送达仓库的方式,以避免未售出的商品堆积在各处。
+
Ideally, the sofa that you want to buy will arrive in port on the very day
that you decide to buy it, and we'll ship it straight to your house without
ever storing it. [.keep-together]#Getting# the timing right is a tricky balancing act when goods take
@@ -41,11 +63,20 @@ damaged, storms cause unexpected delays, logistics partners mishandle goods,
paperwork goes missing, customers change their minds and amend their orders,
and so on.
+理想情况下,当你决定购买沙发的那一天,它刚好到达港口,我们可以直接将它运送到你家,
+而不需要存储。[.keep-together]#掌握#这一时机是一个微妙的平衡过程,
+因为货物需要三个月的时间通过集装箱船运到。在此过程中,物品可能损坏或受到水害,
+风暴可能导致意外延误,物流合作伙伴可能会处理不当,
+文件可能会丢失,客户可能会改变主意并修改订单,等等。
+
We solve those problems by building intelligent software representing the
kinds of operations taking place in the real world so that we can automate as
much of the business as possible.
+我们通过构建能够反映现实世界中各种操作的智能软件来解决这些问题,从而尽可能多地实现业务自动化。
+
=== Why Python?
+为什么选择Python?
If you're reading this book, we probably don't need to convince you that Python
is great, so the real question is "Why does the _Python_ community need a book
@@ -56,12 +87,21 @@ of problems that the C# and Java world has been working on for years.
Startups become real businesses; web apps and scripted automations are becoming
(whisper it) _enterprise_ [.keep-together]#_software_#.
+如果你正在阅读这本书,我们大概不需要说服你Python有多棒,所以真正的问题是:“为什么 _Python_ 社区需要这样一本书?”
+答案与Python的流行程度和成熟度有关:尽管Python可能是全球增长最快的编程语言,
+并且正逐步接近绝对流行度的顶峰,它才刚刚开始处理C#和Java领域多年来一直关注的那类问题。
+初创公司正在成长为真正的企业;网络应用和脚本自动化正在逐渐(悄悄说一句)变成 _企业级_ [.keep-together]#_软件_#。
+
In the Python world, we often quote the Zen of Python:
"There should be one--and preferably only one--obvious way to do it."footnote:[`python -c "import this"`]
Unfortunately, as project size grows, the most obvious way of doing things
isn't always the way that helps you manage complexity and evolving
requirements.
+在 _Python_ 世界中,我们经常引用 _Python_ 之禅:
+“应当有一种——最好只有一种——明显的方式来实现它。”footnote:[`python -c "import this"`]
+不幸的是,随着项目规模的增长,最明显的实现方式并不总是能够帮助你管理复杂性和不断演变的需求的最佳方法。
+
None of the techniques and patterns we discuss in this book are
new, but they are mostly new to the Python world. And this book isn't
a replacement for the classics in the field such as Eric Evans's
@@ -70,16 +110,29 @@ or Martin Fowler's _Patterns of
Enterprise Application Architecture_ (both published by Addison-Wesley [.keep-together]#Professional#)—which we often refer to and
encourage you to go and read.
+我们在本书中讨论的技术和模式都不是全新的,但它们对 _Python_ 世界来说大多是全新的。
+而且,本书并不能取代该领域的一些经典著作,例如 Eric Evans 的《_领域驱动设计_》
+(_Domain-Driven Design_)或 Martin Fowler 的《_企业应用架构模式_》
+(_Patterns of Enterprise Application Architecture_)(两者均由 Addison-Wesley [.keep-together]#Professional# 出版)
+——我们经常提到这些书,并鼓励你去阅读它们。
+
But all the classic code examples in the literature do tend to be written in
Java or pass:[C++/#], and if you're a Python person and haven't used either of
those languages in a long time (or indeed ever), those code listings can be
quite...trying. There's a reason the latest edition of that other classic text, Fowler's
_Refactoring_ (Addison-Wesley Professional), is in JavaScript.
+但是,文献中的所有经典代码示例往往都是用 Java 或 pass:[C++/#] 编写的,
+如果你是一个 _Python_ 程序员,并且已经很久没有使用这些语言(或者根本从未使用过),
+那么这些代码示例可能会让人感觉相当……吃力。正因如此,Fowler 的另一部经典著作《_重构_》(_Refactoring_,Addison-Wesley Professional)
+最新版才使用了 JavaScript。
+
[role="pagebreak-before less_space"]
=== TDD, DDD, and Event-Driven Architecture
+TDD、DDD 和事件驱动架构
In order of notoriety, we know of three tools for managing complexity:
+按知名度排序,我们知道有三种用于管理复杂性的方法:
1. _Test-driven development_ (TDD) helps us to build code that is correct
and enables us to refactor or add new features, without fear of regression.
@@ -87,39 +140,58 @@ In order of notoriety, we know of three tools for managing complexity:
that they run as fast as possible? That we get as much coverage and feedback
from fast, dependency-free unit tests and have the minimum number of slower,
flaky end-to-end tests?
+_测试驱动开发_(_Test-driven development_,TDD)帮助我们编写正确的代码,
+并使我们能够在无需担心引入回归的情况下进行重构或添加新功能。但要充分利用我们的测试可能并不容易:
+我们如何确保测试运行得尽可能快?如何确保通过快速、无依赖的单元测试获得尽可能多的覆盖率和反馈,
+同时将较慢且不稳定的端到端测试数量降到最低?
2. _Domain-driven design_ (DDD) asks us to focus our efforts on building a good
model of the business domain, but how do we make sure that our models aren't
encumbered with infrastructure concerns and don't become hard to change?
+_领域驱动设计_(_Domain-driven design_,DDD)要求我们将精力集中在构建一个良好的业务领域模型上,
+但我们如何确保我们的模型不会被基础设施相关的问题所困扰,并且不会变得难以修改?
+
3. Loosely coupled (micro)services integrated via messages (sometimes called
_reactive microservices_) are a well-established answer to managing complexity
across multiple applications or business domains. But it's not always
obvious how to make them fit with the established tools of
the Python world--Flask, Django, Celery, and so on.
+通过消息集成的松耦合(微)服务(有时称为 _响应式微服务_)是管理多个应用程序或业务领域复杂性的成熟解决方案。
+但如何让它们与 _Python_ 世界中的现有工具——如 Flask、Django、Celery 等——很好地结合起来并不总是显而易见的。
+
NOTE: Don't be put off if you're not working with (or interested in) microservices.
The vast majority of the patterns we discuss,
including much of the event-driven architecture material,
is absolutely applicable in a monolithic architecture.
+如果你没有使用(或对)微服务(感兴趣),也不要感到却步。我们讨论的绝大多数模式,包括大量与事件驱动架构相关的内容,
+完全可以应用于单体架构。
Our aim with this book is to introduce several classic architectural patterns
and show how they support TDD, DDD, and event-driven services. We hope
it will serve as a reference for implementing them in a Pythonic way, and that
people can use it as a first step toward further research in this field.
+本书的目标是介绍几种经典的架构模式,并展示它们如何支持 TDD、DDD 和事件驱动服务。
+我们希望这本书能作为以 _Pythonic_ 方式实现这些模式的参考,同时也希望人们能够将其作为在这一领域进行深入研究的第一步。
=== Who Should Read This Book
+谁应该阅读本书
Here are a few things we assume about you, dear reader:
+亲爱的读者,我们对你有以下一些假设:
* You've been close to some reasonably complex Python applications.
+你接触过一些相对复杂的 _Python_ 应用程序。
* You've seen some of the pain that comes with trying to manage
that complexity.
+你已经体会过试图管理这些复杂性所带来的一些痛苦。
* You don't necessarily know anything about DDD or any of the
classic application architecture patterns.
+你未必了解 DDD 或任何经典的应用架构模式。
We structure our explorations of architectural patterns around an example app,
building it up chapter by chapter. We use TDD at
@@ -128,20 +200,32 @@ If you're not used to working test-first, it may feel a little strange at
the beginning, but we hope you'll soon get used to seeing code "being used"
(i.e., from the outside) before you see how it's built on the inside.
+我们围绕一个示例应用程序来组织对架构模式的探索,逐章构建它。由于我们在工作中使用 TDD,因此我们倾向于先展示测试的代码清单,
+然后再展示实现代码。如果你不习惯以测试为先的方式工作,起初可能会感到有些奇怪,
+但我们希望你很快就能适应先看到代码“被使用”(即从外部看代码),然后再看到它是如何在内部构建的。
+
We use some specific Python frameworks and technologies, including Flask,
SQLAlchemy, and pytest, as well as Docker and Redis. If you're already
familiar with them, that won't hurt, but we don't think it's required. One of
our main aims with this book is to build an architecture for which specific
technology choices become minor implementation details.
+我们使用了一些特定的 _Python_ 框架和技术,包括 Flask、SQLAlchemy 和 pytest,
+以及 Docker 和 Redis。如果你已经熟悉它们,那当然很好,但我们认为这并不是必须的。
+本书的主要目标之一是构建一种架构,使具体的技术选择仅成为次要的实现细节。
+
=== A Brief Overview of What You'll Learn
+您将学到的内容的简要概述
The book is divided into two parts; here's a look at the topics we'll cover
and the chapters they live in.
+本书分为两部分;以下是我们将要讨论的主题及其所在的章节。
+
==== pass:[#part1]
Domain modeling and DDD (Chapters <>, <> and <>)::
+领域建模与 DDD(第 <>、<> 和 <> 章)::
At some level, everyone has learned the lesson that complex business
problems need to be reflected in code, in the form of a model of the domain.
But why does it always seem to be so hard to do without getting tangled
@@ -151,8 +235,13 @@ Domain modeling and DDD (Chapters <>, <>, <>, and <>)::
+仓储(Repository)、服务层(Service Layer)和工作单元(Unit of Work)模式(第 <>、<> 和 <> 章)::
In these three chapters we present three closely related and
mutually reinforcing patterns that support our ambition to keep
the model free of extraneous dependencies. We build a layer of
@@ -160,22 +249,29 @@ Repository, Service Layer, and Unit of Work patterns (Chapters <> and <>)::
+关于测试和抽象的一些思考(第 <> 和 <> 章)::
After presenting the first abstraction (the Repository pattern), we take the
opportunity for a general discussion of how to choose abstractions, and
what their role is in choosing how our software is coupled together. After
we introduce the Service Layer pattern, we talk a bit about achieving a _test pyramid_
and writing unit tests at the highest possible level of abstraction.
+ 在介绍第一个抽象(仓储模式)之后,我们借此机会对如何选择抽象以及抽象在决定软件组合方式中的作用进行了总体讨论。
+ 在引入服务层模式后,我们还会谈论一些关于实现 _测试金字塔_ 和在尽可能高的抽象层级编写单元测试的内容。
==== pass:[#part2]
Event-driven architecture (Chapters <>-<>)::
+事件驱动架构(第 <> 至第 <> 章)::
We introduce three more mutually reinforcing patterns:
the Domain Events, Message Bus, and Handler patterns.
_Domain events_ are a vehicle for capturing the idea that
@@ -186,72 +282,110 @@ Event-driven architecture (Chapters <>-<>)::
+命令查询责任分离(Command-Query Responsibility Segregation,CQRS,第 <> 章)::
We present an example of _command-query responsibility segregation_,
with and without events.
+ 我们展示了一个关于 _命令查询责任分离_(CQRS)的示例,包括使用事件和不使用事件的情况。
Dependency injection (<>)::
+依赖注入(Dependency Injection,第 <> 章)::
We tidy up our explicit and implicit dependencies and implement a
simple dependency injection framework.
+ 我们整理了显式和隐式依赖,并实现了一个简单的依赖注入框架。
==== Additional Content
+附加内容
How do I get there from here? (<>)::
+我如何从这里到达那里? (<>)::
Implementing architectural patterns always looks easy when you show a simple
example, starting from scratch, but many of you will probably be wondering how
to apply these principles to existing software. We'll provide a
few pointers in the epilogue and some links to further reading.
+ 实现架构模式在从头开始并展示一个简单示例时总是看起来很容易,但很多人可能会想知道如何将这些原则应用到现有的软件中。
+ 我们将在尾声中提供一些指导,并附上一些进一步阅读的链接。
=== Example Code and Coding Along
+示例代码和编码
You're reading a book, but you'll probably agree with us when we say that
the best way to learn about code is to code. We learned most of what we know
from pairing with people, writing code with them, and learning by doing, and
we'd like to re-create that experience as much as possible for you in this book.
+您正在阅读一本书,但您可能会同意我们的观点:了解代码的最佳方式就是编写代码。
+我们所知道的大部分内容都是通过与他人结对编程、共同编写代码并在实践中学习获得的,
+我们希望在本书中尽可能为您重现这种体验。
+
As a result, we've structured the book around a single example project
(although we do sometimes throw in other examples). We'll build up this project as the chapters progress, as if you've paired with us and
we're explaining what we're doing and why at each step.
+因此,我们围绕一个示例项目构建了这本书(尽管有时也会插入其他示例)。我们将随着章节的推进逐步构建这个项目,
+就像您在与我们结对编程一样,我们会在每一步中解释我们正在做什么以及为什么这样做。
+
But to really get to grips with these patterns, you need to mess about with the
code and get a feel for how it works. You'll find all the code on
GitHub; each chapter has its own branch. You can find https://github.com/cosmicpython/code/branches/all[a list] of the branches on GitHub as well.
+但是,要真正掌握这些模式,您需要亲自摆弄代码,体会它是如何工作的。您可以在 GitHub 上找到所有代码;每一章都有自己的分支。此外,
+您还可以在 GitHub 上找到 https://github.com/cosmicpython/code/branches/all[分支列表]。
+
[role="pagebreak-before"]
Here are three ways you might code along with the book:
+以下是您可以跟随本书进行编程的三种方式:
* Start your own repo and try to build up the app as we do, following the
examples from listings in the book, and occasionally looking to our repo
for hints. A word of warning, however: if you've read Harry's previous book
and coded along with that, you'll find that this book requires you to figure out more on
your own; you may need to lean pretty heavily on the working versions on GitHub.
+创建您自己的代码库,并按照书中的示例列表一步步构建应用,有时可以查看我们的代码库以获得提示。
+不过,有一点需要提醒:如果您读过 Harry 的前一本书并跟着一起编写过代码,那么您会发现这本书需要您更多地自行探索;
+您可能需要非常依赖 GitHub 上的工作版本。
* Try to apply each pattern, chapter by chapter, to your own (preferably
small/toy) project, and see if you can make it work for your use case. This
is high risk/high reward (and high effort besides!). It may take quite some
work to get things working for the specifics of your project, but on the other
hand, you're likely to learn the most.
+尝试将每个模式一章一章地应用到您自己的项目中(最好是一个小型或实验性的项目),
+看看它是否适用于您的用例。这种方法风险高、回报高(同时也需要投入更多的努力!)。
+要让这些模式适配于您的具体项目,可能需要相当多的工作,但另一方面,这种方式可能会让您收获最多。
* For less effort, in each chapter we outline an "Exercise for the Reader,"
and point you to a GitHub location where you can download some partially finished
code for the chapter with a few missing parts to write yourself.
+如果您希望少花些精力,每一章我们都会概述一个“读者练习”,并提供一个 GitHub 链接,
+您可以在其中下载该章节的部分完成代码,其中包含一些需要您自己补充的部分。
Particularly if you're intending to apply some of these patterns in your own
projects, working through a simple example is a great way to
safely practice.
+特别是如果您打算在自己的项目中应用这些模式,通过一个简单的示例来实践是一个安全且有效的练习方式。
TIP: At the very least, do a `git checkout` of the code from our repo as you
read each chapter. Being able to jump in and see the code in the context of
an actual working app will help answer a lot of questions as you go, and
makes everything more real. You'll find instructions for how to do that
at the beginning of each chapter.
+ 至少,在阅读每一章时,从我们的代码库中执行一次 `git checkout` 。能够深入查看实际工作应用中代码的上下文,
+ 有助于在学习过程中解答许多问题,并使所有内容更加直观。在每一章的开头,您都会找到如何执行此操作的说明。
=== License
+许可协议
The code (and the online version of the book) is licensed under a Creative
Commons CC BY-NC-ND license, which means you are free to copy and share it with
@@ -260,51 +394,76 @@ If you want to re-use any of the content from this book and you have any
worries about the license, contact O'Reilly at pass:[permissions@oreilly.com].
+代码(以及本书的在线版本)采用了 Creative Commons CC BY-NC-ND 许可协议,这意味着您可以自由复制并与任何人分享,
+但须用于非商业目的,同时需注明出处。如果您想重用本书中的任何内容并对许可协议有任何疑问,请联系 O'Reilly,
+邮箱为 pass:[permissions@oreilly.com]。
+
The print edition is licensed differently; please see the copyright page.
+印刷版的许可有所不同;请参阅版权页。
+
=== Conventions Used in This Book
+本书中使用的约定
The following typographical conventions are used in this book:
+本书中使用了以下排版约定:
_Italic_:: Indicates new terms, URLs, email addresses, filenames, and file extensions.
+_斜体_:: 表示新术语、URL、电子邮件地址、文件名和文件扩展名。
+
+Constant width+:: Used for program listings, as well as within paragraphs to refer to program elements such as variable or function names, databases, data types, environment variables, statements, and keywords.
++等宽字体+:: 用于程序清单,以及在段落中引用程序元素,例如变量名、函数名、数据库、数据类型、环境变量、语句和关键字。
+
**`Constant width bold`**:: Shows commands or other text that should be typed literally by the user.
+**`等宽加粗`**:: 表示用户需要按字面输入的命令或其他文本。
+
_++Constant width italic++_:: Shows text that should be replaced with user-supplied values or by values determined by context.
+_++等宽斜体++_:: 表示需要用户提供的值或根据上下文确定的值来替换的文本。
+
[TIP]
====
This element signifies a tip or suggestion.
+该元素表示一个提示或建议。
====
[NOTE]
====
This element signifies a general note.
+该元素表示一般说明。
====
[WARNING]
====
This element indicates a warning or caution.
+该元素表示警告或注意事项。
====
=== O'Reilly Online Learning
+O'Reilly 在线学习
[role = "ormenabled"]
[NOTE]
====
For more than 40 years, pass:[O’Reilly Media] has provided technology and business training, knowledge, and insight to help companies succeed.
+超过 40 年以来,pass:[O’Reilly Media] 一直提供技术与商业培训、知识和洞见,以帮助企业取得成功。
====
Our unique network of experts and innovators share their knowledge and expertise through books, articles, conferences, and our online learning platform. O’Reilly’s online learning platform gives you on-demand access to live training courses, in-depth learning paths, interactive coding environments, and a vast collection of text and video from O'Reilly and 200+ other publishers. For more information, please visit pass:[http://oreilly.com].
+我们独特的专家和创新者网络,通过图书、文章、会议以及我们的在线学习平台分享他们的知识与专业技能。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程、深入的学习路径、交互式编码环境,以及来自 O'Reilly 和其他 200 多家出版商的大量文本与视频资源。欲了解更多信息,请访问 pass:[http://oreilly.com]。
=== How to Contact O'Reilly
+如何联系 O'Reilly
Please address comments and questions concerning this book to the publisher:
+如对本书有任何意见或问题,请联系出版社:
+
++++
- O’Reilly Media, Inc.
@@ -317,6 +476,7 @@ Please address comments and questions concerning this book to the publisher:
++++
We have a web page for this book, where we list errata, examples, and any additional information. You can access this page at https://oreil.ly/architecture-patterns-python[].
+我们为本书建立了一个网页,在那里列出了勘误、示例以及任何附加信息。您可以通过以下链接访问该页面:https://oreil.ly/architecture-patterns-python[]。
++++
@@ -333,6 +493,7 @@ Follow us on Twitter: link:$$http://twitter.com/oreillymedia$$[]
Watch us on YouTube: link:$$http://www.youtube.com/oreillymedia$$[]
=== Acknowledgments
+致谢
To our tech reviewers, David Seddon, Ed Jung, and Hynek Schlawack: we absolutely
do not deserve you. You are all incredibly dedicated, conscientious, and
@@ -340,6 +501,9 @@ rigorous. Each one of you is immensely smart, and your different points of
view were both useful and complementary to each other. Thank you from the
bottom of our hearts.
+致我们的技术审阅者 David Seddon、Ed Jung 和 Hynek Schlawack:我们完全不敢奢望得到你们的帮助。
+你们都无比敬业、认真且一丝不苟。你们每个人都非常聪明,而你们不同的观点既有用又相辅相成。我们由衷地感谢你们。
+
Gigantic thanks also to all our readers so far for their comments and
suggestions:
Ian Cooper, Abdullah Ariff, Jonathan Meier, Gil Gonçalves, Matthieu Choplin,
@@ -350,12 +514,27 @@ Leira, Brandon Rhodes, Jazeps Basko, simkimsia, Adrien Brunet, Sergey Nosko,
Dmitry Bychkov, dayres2, programmer-ke, asjhita,
and many more; our apologies if we missed you on this list.
+对于所有迄今为止为我们提供意见和建议的读者,我们也表示由衷的感谢:
+Ian Cooper、Abdullah Ariff、Jonathan Meier、Gil Gonçalves、Matthieu Choplin、
+Ben Judson、James Gregory、Łukasz Lechowicz、Clinton Roy、Vitorino Araújo、
+Susan Goodbody、Josh Harwood、Daniel Butler、Liu Haibin、Jimmy Davies、
+Ignacio Vergara Kausel、Gaia Canestrani、Renne Rocha、pedroabi、Ashia Zawaduk、
+Jostein Leira、Brandon Rhodes、Jazeps Basko、simkimsia、Adrien Brunet、
+Sergey Nosko、Dmitry Bychkov、dayres2、programmer-ke、asjhita,
+还有更多人;如果遗漏了您的名字,我们深表歉意。
+
Super-mega-thanks to our editor Corbin Collins for his gentle chivvying, and
for being a tireless advocate of the reader. Similarly-superlative thanks to
the production staff, Katherine Tozer, Sharon Wilkey, Ellen Troutman-Zaig, and
Rebecca Demarest, for your dedication, professionalism, and attention to
detail. This book is immeasurably improved thanks to you.
+特别感谢我们的编辑 Corbin Collins,他温和地推动我们前进,并始终不懈地为读者着想。
+同样特别感谢制作团队 Katherine Tozer、Sharon Wilkey、Ellen Troutman-Zaig 和 Rebecca Demarest,
+感谢你们的奉献、专业精神以及对细节的关注。因为有你们,这本书得到了极大的提升。
+
// TODO thanks to rest of OR team.
Any errors remaining in the book are our own, naturally.
+
+书中若仍有任何错误,自然由我们自行承担。
From d66f78cbd2a8a3199986c0d055983ef459cfe931 Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Wed, 29 Jan 2025 21:41:03 +0800
Subject: [PATCH 03/75] update Readme.md
---
Readme.md | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/Readme.md b/Readme.md
index 7f0c662c..d7be9e99 100644
--- a/Readme.md
+++ b/Readme.md
@@ -6,15 +6,19 @@
## Table of Contents
+目录
O'Reilly have generously said that we will be able to publish this book under a [CC license](license.txt),
In the meantime, pull requests, typofixes, and more substantial feedback + suggestions are enthusiastically solicited.
-| Chapter | |
-|---------------------------------------------------------------------------------------------------------------------| ----- |
-| [Preface
前言(翻译中...)](preface.asciidoc) | |
-| [Introduction: Why do our designs go wrong?
引言:为什么我们的设计会出错?(未翻译)](introduction.asciidoc) | ||
-| [**Part 1 Intro**](part1.asciidoc) | |
+O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布本书。
+与此同时,我们热情欢迎有关拉取请求、错别字修正以及更深入的反馈与建议。
+
+| Chapter
章节 | |
+|--------------------------------------------------------------------------------------------------------------------------| ----- |
+| [Preface
前言(已翻译)](preface.asciidoc) | |
+| [Introduction: Why do our designs go wrong?
引言:为什么我们的设计会出错?(翻译中...)](introduction.asciidoc) | ||
+| [**Part 1 Intro**](part1.asciidoc) | |
| [Chapter 1: Domain Model
第一章:领域模型(未翻译)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 2: Repository
第二章:仓储(未翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(未翻译)](chapter_03_abstractions.asciidoc) | |
@@ -22,7 +26,7 @@ In the meantime, pull requests, typofixes, and more substantial feedback + sugge
| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(未翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 6: Unit of Work
第六章:工作单元(未翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 7: Aggregates
第七章:聚合(未翻译)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [**Part 2 Intro**](part2.asciidoc) | |
+| [**Part 2 Intro**](part2.asciidoc) | |
| [Chapter 8: Domain Events and a Simple Message Bus
第八章:领域事件与简单消息总线(未翻译)](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 9: Going to Town on the MessageBus
第九章:深入探讨消息总线(未翻译)](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 10: Commands
第十章:命令(未翻译)](chapter_10_commands.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
From b76cf9a2bbc9ccadb61f9fea506459defc37b90e Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Wed, 29 Jan 2025 21:59:56 +0800
Subject: [PATCH 04/75] update Readme.md introduction.asciidoc
---
Readme.md | 4 +-
introduction.asciidoc | 107 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 109 insertions(+), 2 deletions(-)
diff --git a/Readme.md b/Readme.md
index d7be9e99..8752020a 100644
--- a/Readme.md
+++ b/Readme.md
@@ -17,9 +17,9 @@ O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布
| Chapter
章节 | |
|--------------------------------------------------------------------------------------------------------------------------| ----- |
| [Preface
前言(已翻译)](preface.asciidoc) | |
-| [Introduction: Why do our designs go wrong?
引言:为什么我们的设计会出错?(翻译中...)](introduction.asciidoc) | ||
+| [Introduction: Why do our designs go wrong?
引言:为什么我们的设计会出问题?(已翻译)](introduction.asciidoc) | ||
| [**Part 1 Intro**](part1.asciidoc) | |
-| [Chapter 1: Domain Model
第一章:领域模型(未翻译)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 1: Domain Model
第一章:领域模型(翻译中...)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 2: Repository
第二章:仓储(未翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(未翻译)](chapter_03_abstractions.asciidoc) | |
| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(未翻译)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
diff --git a/introduction.asciidoc b/introduction.asciidoc
index f8829603..9d93234a 100644
--- a/introduction.asciidoc
+++ b/introduction.asciidoc
@@ -1,11 +1,13 @@
[[introduction]]
[preface]
== Introduction
+引言
// TODO (CC): remove "preface" marker from this chapter and check if they renumber correctly
// with this as zero. figures in this chapter should be "Figure 0-1 etc"
=== Why Do Our Designs Go Wrong?
+为什么我们的设计会出问题?
What comes to mind when you hear the word _chaos?_ Perhaps you think of a noisy
stock exchange, or your kitchen in the morning--everything confused and
@@ -13,6 +15,10 @@ jumbled. When you think of the word _order_, perhaps you think of an empty room,
serene and calm. For scientists, though, chaos is characterized by homogeneity
(sameness), and order by complexity (difference).
+当你听到 _混乱_ 这个词时,会联想到什么?也许你会想到喧闹的股票交易所,或者清晨混乱不堪的厨房——一切都显得迷乱和杂乱。
+而当你想到 _秩序_ 这个词时,也许会联想到一个空荡荡的房间,祥和而平静。然而,对于科学家来说,混乱的特征是同质性(相同性),
+而秩序的特征则是复杂性(差异性)。
+
////
IDEA [SG] Found previous paragraph a bit confusing. It seems to suggest that a
scientist would say that a noisy stock exchange is ordered. I feel like you
@@ -26,6 +32,10 @@ deliberate effort, the garden will run wild. Weeds and grasses will choke out
other plants, covering over the paths, until eventually every part looks the
same again--wild and unmanaged.
+例如,一个精心打理的花园是一个高度有序的系统。园丁用小路和篱笆划定边界,并设计出花坛或菜圃。
+随着时间的推移,花园会演变得更加丰富茂盛;但如果没有刻意的维护,花园就会变得杂乱无章。
+杂草和野草会覆盖其他植物,掩盖小路,直到最终每个部分都变得一样——野蛮生长且无人管理。
+
Software systems, too, tend toward chaos. When we first start building a new
system, we have grand ideas that our code will be clean and well ordered, but
over time we find that it gathers cruft and edge cases and ends up a confusing
@@ -38,6 +48,13 @@ to everything else so that changing any part of the system becomes fraught with
danger. This is so common that software engineers have their own term for
chaos: the Big Ball of Mud antipattern (<>).
+软件系统同样也倾向于走向混乱。一开始,当我们构建一个新系统时,我们满怀壮志,认为代码会保持干净且有序。
+然而,随着时间推移,我们发现代码积累了杂乱无章的内容与边缘案例,最终变成了一团混乱的管理类和工具模块的沼泽。
+原本合理分层的架构也塌陷了,如同一盘过于湿软的松糕。混乱的软件系统以功能的同质性为特征:比如,
+API处理程序既包含领域知识,又发送电子邮件并执行日志记录;所谓的“业务逻辑”类不进行计算,却执行输入/输出操作;
+每个组件都与其他组件紧密耦合,以至于修改系统的任何部分都会变得充满风险。
+这种情况非常常见,以至于软件工程师用自己的术语来描述这种混乱:*大泥球反模式*(Big Ball of Mud)(<>)。
+
[[bbom_image]]
.A real-life dependency diagram (source: https://oreil.ly/dbGTW["Enterprise Dependency: Big Ball of Yarn"] by Alex Papadimoulis)
image::images/apwp_0001.png[]
@@ -45,27 +62,39 @@ image::images/apwp_0001.png[]
TIP: A big ball of mud is the natural state of software in the same way that wilderness
is the natural state of your garden. It takes energy and direction to
prevent the collapse.
+大泥球是软件的自然状态,就像荒野是你花园的自然状态一样。需要付出精力和明确的指导才能防止其崩溃。
Fortunately, the techniques to avoid creating a big ball of mud aren't complex.
+幸运的是,避免形成大泥球的技术并不复杂。
+
// IDEA: talk about how architecture enables TDD and DDD (ie callback to book
// subtitle)
=== Encapsulation and Abstractions
+封装与抽象
Encapsulation and abstraction are tools that we all instinctively reach for
as programmers, even if we don't all use these exact words. Allow us to dwell
on them for a moment, since they are a recurring background theme of the book.
+封装和抽象是我们作为程序员本能地会使用的工具,即使我们并不总是使用这些确切的术语。
+请允许我们稍作停留来讨论它们,因为它们是本书反复出现的背景主题。
+
The term _encapsulation_ covers two closely related ideas: simplifying
behavior and hiding data. In this discussion, we're using the first sense. We
encapsulate behavior by identifying a task that needs to be done in our code
and giving that task to a well-defined object or function. We call that object or function an
_abstraction_.
+术语 _封装_ 涵盖了两个密切相关的概念:简化行为和隐藏数据。在此讨论中,我们采用第一种含义。
+通过识别代码中需要完成的任务并将其交给一个定义良好的对象或函数,我们实现了对行为的封装。
+我们将这个对象或函数称为一个 _抽象_。
+
//DS: not sure I agree with this definition. more about establishing boundaries?
Take a look at the following two snippets of Python code:
+来看以下两个 _Python_ 代码片段:
[[urllib_example]]
@@ -110,10 +139,15 @@ Both code listings do the same thing: they submit form-encoded values
to a URL in order to use a search engine API. But the second is simpler to read
and understand because it operates at a higher level of abstraction.
+两个代码示例实现的功能相同:它们将表单编码的值提交到一个 URL 以使用搜索引擎 API。
+但第二个示例更易于阅读和理解,因为它是在更高层次的抽象上操作的。
+
We can take this one step further still by identifying and naming the task we
want the code to perform for us and using an even higher-level abstraction to make
it explicit:
+我们还可以更进一步,通过明确识别并命名我们希望代码为我们执行的任务,并使用一个更高层次的抽象来使其更清晰:
+
[[ddg_example]]
.Do a search with the duckduckgo client library
====
@@ -128,6 +162,8 @@ for r in duckduckpy.query('Sausages').related_topics:
Encapsulating behavior by using abstractions is a powerful tool for making
code more expressive, more testable, and easier to maintain.
+通过使用抽象来封装行为是一种强大的工具,可以使代码更具表达力、更易于测试并更易于维护。
+
NOTE: In the literature of the object-oriented (OO) world, one of the classic
characterizations of this approach is called
http://www.wirfs-brock.com/Design.html[_responsibility-driven design_];
@@ -136,15 +172,24 @@ NOTE: In the literature of the object-oriented (OO) world, one of the classic
in terms of data or algorithms.footnote:[If you've come across
class-responsibility-collaborator (CRC) cards, they're
driving at the same thing: thinking about _responsibilities_ helps you decide how to split things up.]
+在面向对象(OO)领域的相关文献中,这种方法的一个经典定义被称为 [责任驱动设计](http://www.wirfs-brock.com/Design.html)(_responsibility-driven design_);
+它使用 _角色_ 和 _责任_ 这些术语,而不是 _任务_。核心思想是以行为的角度思考代码,而不是以数据或算法为中心。
+脚注:[如果你接触过类-责任-协作(CRC)卡片,它们的目标是一样的:通过思考 _责任_,帮助你决定如何划分代码。]
.Abstractions and ABCs
+抽象与抽象基类(ABCs)
*******************************************************************************
In a traditional OO language like Java or C#, you might use an abstract base
class (ABC) or an interface to define an abstraction. In Python you can (and we
sometimes do) use ABCs, but you can also happily rely on duck typing.
+在像 Java 或 C# 这样的传统面向对象语言中,你可能会使用抽象基类(ABC)或接口来定义一个抽象。
+在 _Python_ 中,你可以(我们有时也确实会)使用抽象基类,但也完全可以愉快地依赖于鸭子类型。
+
The abstraction can just mean "the public API of the thing you're using"—a
function name plus some arguments, for example.
+
+抽象可以仅仅表示“你正在使用的事物的公共 API”——例如,一个函数名加上一些参数。
*******************************************************************************
Most of the patterns in this book involve choosing an abstraction, so you'll
@@ -152,8 +197,11 @@ see plenty of examples in each chapter. In addition,
<> specifically discusses some general heuristics
for choosing abstractions.
+本书中的大多数模式都涉及选择抽象,因此你将在每一章中看到大量的示例。
+此外,<> 专门讨论了一些关于选择抽象的一般性启发法。
=== Layering
+分层
Encapsulation and abstraction help us by hiding details and protecting the
consistency of our data, but we also need to pay attention to the interactions
@@ -161,6 +209,9 @@ between our objects and functions. When one function, module, or object uses
another, we say that the one _depends on_ the other. These dependencies form a
kind of network or graph.
+封装和抽象通过隐藏细节和保护数据的一致性来帮助我们,但我们还需要关注对象和函数之间的交互。
+当一个函数、模块或对象使用另一个时,我们称前者 _依赖于_ 后者。这些依赖关系构成了一种网络或图。
+
In a big ball of mud, the dependencies are out of control (as you saw in
<>). Changing one node of the graph becomes difficult because it
has the potential to affect many other parts of the system. Layered
@@ -168,6 +219,10 @@ architectures are one way of tackling this problem. In a layered architecture,
we divide our code into discrete categories or roles, and we introduce rules
about which categories of code can call each other.
+在一个大泥球系统中,依赖关系是失控的(如你在 <> 中所见)。修改图中的一个节点变得困难,
+因为它可能会影响系统的许多其他部分。分层架构是应对这一问题的一种方法。在分层架构中,
+我们将代码划分为不同的类别或角色,并引入关于哪些类别的代码可以相互调用的规则。
+
One of the most common examples is the _three-layered architecture_ shown in
<>.
@@ -201,12 +256,17 @@ with a business logic layer that contains our business rules and our workflows;
and finally, we have a database layer that's responsible for storing and retrieving
data.
+分层架构可能是构建业务软件中最常见的模式。在这种模型中,我们有用户界面组件,可以是网页、API 或命令行;
+这些用户界面组件与包含业务规则和工作流程的业务逻辑层通信;最后,我们有一个数据库层,负责数据的存储和检索。
+
For the rest of this book, we're going to be systematically turning this
model inside out by obeying one simple principle.
+在本书的其余部分,我们将通过遵守一个简单的原则,系统性地将这种模型翻转过来。
[[dip]]
=== The Dependency Inversion Principle
+依赖倒置原则
You might be familiar with the _dependency inversion principle_ (DIP) already, because
it's the _D_ in SOLID.footnote:[SOLID is an acronym for Robert C. Martin's five principles of object-oriented
@@ -214,23 +274,37 @@ design: single responsibility, open for extension but
closed for modification, Liskov substitution, interface segregation, and
dependency inversion. See https://oreil.ly/UFM7U["S.O.L.I.D: The First 5 Principles of Object-Oriented Design"] by Samuel Oloruntoba.]
+你可能已经熟悉 _依赖倒置原则_(DIP),因为它是 SOLID 原则中的 _D_。脚注:[SOLID 是 Robert C. Martin 提出的五大面向对象设计原则的首字母缩写:
+单一责任原则(Single responsibility)、开放封闭原则(Open for extension but closed for modification)、
+里氏替换原则(Liskov substitution)、接口隔离原则(Interface segregation)
+以及依赖倒置原则(Dependency inversion)。
+参见 Samuel Oloruntoba 的文章 [“S.O.L.I.D: The First 5 Principles of Object-Oriented Design”](https://oreil.ly/UFM7U)。]
+
Unfortunately, we can't illustrate the DIP by using three tiny code listings as
we did for encapsulation. However, the whole of <> is essentially a worked
example of implementing the DIP throughout an application, so you'll get
your fill of concrete examples.
+遗憾的是,我们无法像讲解封装那样通过三个小代码示例来说明依赖倒置原则(DIP)。然而,
+<> 的全部内容本质上就是一个在整个应用程序中实现 DIP 的完整示例,因此你会看到大量具体的示例。
+
In the meantime, we can talk about DIP's formal definition:
+与此同时,我们可以讨论一下依赖倒置原则(DIP)的正式定义:
// [SG] reference?
1. High-level modules should not depend on low-level modules. Both should
depend on abstractions.
+高层模块不应该依赖于低层模块。两者都应该依赖于抽象。
2. Abstractions should not depend on details. Instead, details should depend on
abstractions.
+抽象不应该依赖于细节。相反,细节应该依赖于抽象。
But what does this mean? Let's take it bit by bit.
+但这是什么意思呢?让我们一点一点地解析。
+
_High-level modules_ are the code that your organization really cares about.
Perhaps you work for a pharmaceutical company, and your high-level modules deal
with patients and trials. Perhaps you work for a bank, and your high-level
@@ -238,6 +312,9 @@ modules manage trades and exchanges. The high-level modules of a software
system are the functions, classes, and packages that deal with our real-world
concepts.
+_高层模块_ 是你的组织真正关心的代码。也许你为一家制药公司工作,高层模块处理患者和试验。
+也许你为一家银行工作,高层模块负责管理交易和兑换。软件系统的高层模块是那些处理现实世界概念的函数、类和包。
+
By contrast, _low-level modules_ are the code that your organization doesn't
care about. It's unlikely that your HR department gets excited about filesystems or network sockets. It's not often that you discuss SMTP, HTTP,
or AMQP with your finance team. For our nontechnical stakeholders, these
@@ -246,22 +323,35 @@ whether the high-level concepts work correctly. If payroll runs on time, your
business is unlikely to care whether that's a cron job or a transient function
running on Kubernetes.
+相比之下,_低层模块_ 是你的组织并不关心的代码。你的 HR 部门不太可能对文件系统或网络套接字感到兴奋。
+你也不太会与财务团队讨论 SMTP、HTTP 或 AMQP 等技术细节。对于非技术型利益相关者来说,
+这些低层次的概念既不有趣也无关紧要。他们关心的只是高层次的概念是否能够正常运行。
+如果工资按时发放,你的企业大概率不会在意这背后是使用 cron 任务还是运行在 Kubernetes 上的某个临时函数。
+
_Depends on_ doesn't mean _imports_ or _calls_, necessarily, but rather a more
general idea that one module _knows about_ or _needs_ another module.
+_依赖于_ 并不一定意味着 _导入_ 或 _调用_,而是更为广泛的概念,指一个模块 _了解_ 或 _需要_ 另一个模块。
+
And we've mentioned _abstractions_ already: they're simplified interfaces that
encapsulate behavior, in the way that our duckduckgo module encapsulated a
search engine's API.
+我们已经提到过 _抽象_:它们是封装行为的简化接口,就像我们的 duckduckgo 模块封装了一个搜索引擎的 API 一样。
+
[quote,David Wheeler]
____
All problems in computer science can be solved by adding another level of
indirection.
+
+计算机科学中的所有问题都可以通过增加一个间接层来解决。
____
So the first part of the DIP says that our business code shouldn't depend on
technical details; instead, both should use abstractions.
+因此,依赖倒置原则(DIP)的第一部分表明,我们的业务代码不应该依赖于技术细节;相反,两者都应该使用抽象。
+
Why? Broadly, because we want to be able to change them independently of each
other. High-level modules should be easy to change in response to business
needs. Low-level modules (details) are often, in practice, harder to
@@ -275,6 +365,12 @@ business layer. Adding an abstraction between them (the famous extra
layer of indirection) allows the two to change (more) independently of each
other.
+为什么呢?总的来说,是因为我们希望能够让它们彼此独立地进行更改。高层模块应该能够轻松地根据业务需求进行修改。
+而低层模块(细节)在实践中通常更难更改:例如,重构一个函数名相对简单,而定义、测试并部署一个用于修改数据库列名的迁移却要复杂得多。
+我们不希望因为业务逻辑与底层基础设施的细节紧密耦合而导致业务逻辑的变更变得缓慢。
+但同样重要的是,当需要时,我们必须能够更改你的基础设施细节(例如,分片数据库),而无需对业务层进行修改。
+在它们之间添加一个抽象层(著名的额外间接层)可以让两者(更)独立地进行变更。
+
The second part is even more mysterious. "Abstractions should not depend on
details" seems clear enough, but "Details should depend on abstractions" is
hard to imagine. How can we have an abstraction that doesn't depend on the
@@ -282,7 +378,12 @@ details it's abstracting? By the time we get to <>,
we'll have a concrete example that should make this all a bit clearer.
+第二部分就更加神秘了。“抽象不应该依赖于细节”似乎很容易理解,但“细节应该依赖于抽象”却难以想象。
+我们如何能有一个抽象而不依赖于它所抽象的那些细节呢?等我们到了 <> 时,
+将会有一个具体的例子,可以让这一切变得更清晰一些。
+
=== A Place for All Our Business Logic: The Domain Model
+为我们的业务逻辑提供一个归宿:领域模型
But before we can turn our three-layered architecture inside out, we need to
talk more about that middle layer: the high-level modules or business
@@ -290,7 +391,13 @@ logic. One of the most common reasons that our designs go wrong is that
business logic becomes spread throughout the layers of our application,
making it hard to identify, understand, and change.
+但是,在我们将三层架构翻转之前,我们需要深入讨论中间层:高级模块或业务逻辑。我们的设计出错的一个最常见原因是,
+业务逻辑分散在应用程序的各个层中,这使得辨识、理解和更改变得困难。
+
<> shows how to build a business
layer with a _Domain Model_ pattern. The rest of the patterns in <> show
how we can keep the domain model easy to change and free of low-level concerns
by choosing the right abstractions and continuously applying the DIP.
+
+<> 展示了如何使用 _Domain Model_ 模式构建业务层。
+<> 中的其余模式则展示了如何通过选择合适的抽象并持续应用DIP(依赖倒置原则),使领域模型易于更改并避免低层次的关注点。
From 462034dafb087bac392a1a9cb343d930a74359a2 Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Wed, 29 Jan 2025 22:59:22 +0800
Subject: [PATCH 05/75] update Readme.md chapter_01_domain_model.asciidoc
---
Readme.md | 4 +-
chapter_01_domain_model.asciidoc | 275 ++++++++++++++++++++++++++++++-
2 files changed, 268 insertions(+), 11 deletions(-)
diff --git a/Readme.md b/Readme.md
index 8752020a..07152bc4 100644
--- a/Readme.md
+++ b/Readme.md
@@ -19,8 +19,8 @@ O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布
| [Preface
前言(已翻译)](preface.asciidoc) | |
| [Introduction: Why do our designs go wrong?
引言:为什么我们的设计会出问题?(已翻译)](introduction.asciidoc) | ||
| [**Part 1 Intro**](part1.asciidoc) | |
-| [Chapter 1: Domain Model
第一章:领域模型(翻译中...)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 2: Repository
第二章:仓储(未翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 1: Domain Model
第一章:领域模型(已翻译)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 2: Repository
第二章:仓储(翻译中...)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(未翻译)](chapter_03_abstractions.asciidoc) | |
| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(未翻译)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(未翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
diff --git a/chapter_01_domain_model.asciidoc b/chapter_01_domain_model.asciidoc
index 194a8526..0de01368 100644
--- a/chapter_01_domain_model.asciidoc
+++ b/chapter_01_domain_model.asciidoc
@@ -1,5 +1,6 @@
[[chapter_01_domain_model]]
== Domain Modeling
+领域模型
((("domain modeling", id="ix_dommod")))
((("domain driven design (DDD)", seealso="domain model; domain modeling")))
@@ -8,17 +9,25 @@ that's highly compatible with TDD. We'll discuss _why_ domain modeling
matters, and we'll look at a few key patterns for modeling domains: Entity,
Value Object, and Domain Service.
+本章将探讨如何通过代码对业务流程进行建模,并使其与TDD高度兼容。
+我们将讨论领域建模的重要性(_why_),并研究一些领域建模的关键模式:实体(Entity)、值对象(Value Object)和领域服务(Domain Service)。
+
<> is a simple visual placeholder for our Domain
Model pattern. We'll fill in some details in this chapter, and as we move on to
other chapters, we'll build things around the domain model, but you should
always be able to find these little shapes at the core.
+<> 是我们领域模型模式的一个简单视觉占位符。
+在本章中我们会填充一些细节,随着进入其他章节,我们会围绕领域模型构建内容,
+但你始终应该能够在核心找到这些小形状。
+
[[maps_chapter_01_notext]]
.A placeholder illustration of our domain model
image::images/apwp_0101.png[]
[role="pagebreak-before less_space"]
=== What Is a Domain Model?
+啥是领域模型?
((("business logic layer")))
In the <>, we used the term _business logic layer_
@@ -27,6 +36,10 @@ the book, we're going to use the term _domain model_ instead. This is a term
from the DDD community that does a better job of capturing our intended meaning
(see the next sidebar for more on DDD).
+在 <> 中,我们使用了术语 _business logic layer_(业务逻辑层)来描述三层架构的中心层。
+在本书的其余部分,我们将改用术语 _domain model_(领域模型)。这是DDD(领域驱动设计)社区的一个术语,
+它更能准确表达我们的意图(有关DDD的更多信息,请参阅下一个边栏)。
+
((("domain driven design (DDD)", "domain, defined")))
The _domain_ is a fancy way of saying _the problem you're trying to solve._
Your authors currently work for an online retailer of furniture. Depending on
@@ -35,6 +48,10 @@ procurement, or product design, or logistics and delivery. Most programmers
spend their days trying to improve or automate business processes; the domain
is the set of activities that those processes support.
+“_Domain_”(领域)是一个较为花哨的说法,意思是“_你试图解决的问题_”。本书的作者目前为一家在线家具零售商工作。
+根据你所讨论的系统不同,领域可能是采购与供应、产品设计,或者物流与交付。大多数程序员每天的工作是试图改进或自动化业务流程;
+领域就是这些流程所支持的一组活动。
+
((("model (domain)")))
A _model_ is a map of a process or phenomenon that captures a useful property.
Humans are exceptionally good at producing models of things in their heads. For
@@ -45,22 +62,38 @@ intuitions about how objects behave at near-light speeds or in a vacuum because
our model was never designed to cover those cases. That doesn't mean the model
is wrong, but it does mean that some predictions fall outside of its domain.
+“_Model_”(模型)是对某个过程或现象的映射,其目的是捕捉其中一个有用的特性。人类在头脑中构建事物模型的能力尤为出色。
+例如,当有人向你扔一个球时,你几乎是下意识地预测出球的运动轨迹,因为你头脑中有一个关于物体在空间中如何运动的模型。
+当然,这个模型绝对称不上完美。比如,人类对物体在接近光速或真空中的行为直觉是非常糟糕的,
+因为我们的模型从未被设计用来涵盖这些情况。但这并不意味着模型是错误的,
+而是说明有些预测超出了它的领域范围。
+
The domain model is the mental map that business owners have of their
businesses. All business people have these mental maps--they're how humans think
about complex processes.
+领域模型是业务所有者对其业务的心智地图。所有的业务人士都有这样的心智地图——这是人类思考复杂流程的方式。
+
You can tell when they're navigating these maps because they use business speak.
Jargon arises naturally among people who are collaborating on complex systems.
+当他们在运用这些心智地图时,你可以通过他们使用的商业语言察觉到。行话(术语)是在人们共同协作处理复杂系统时自然产生的。
+
Imagine that you, our unfortunate reader, were suddenly transported light years
away from Earth aboard an alien spaceship with your friends and family and had
to figure out, from first principles, how to navigate home.
+想象一下,作为我们“不幸”的读者,你突然和你的朋友和家人一起被传送到一艘外星飞船上,飞离地球数光年远,
+并且不得不从基本原理开始,推导出如何导航回家。
+
In your first few days, you might just push buttons randomly, but soon you'd
learn which buttons did what, so that you could give one another instructions.
"Press the red button near the flashing doohickey and then throw that big
lever over by the radar gizmo," you might say.
+在最初的几天里,你可能会随意按下各种按钮,但很快你就会学会每个按钮的功能,这样你们就可以相互传递指令。
+你可能会说:“按下闪烁装置旁边的那个红色按钮,然后拉下雷达装置旁边的那个大杠杆。”
+
Within a couple of weeks, you'd become more precise as you adopted words to
describe the ship's functions: "Increase oxygen levels in cargo bay three"
or "turn on the little thrusters." After a few months, you'd have adopted
@@ -68,6 +101,10 @@ language for entire complex processes: "Start landing sequence" or "prepare
for warp." This process would happen quite naturally, without any formal effort
to build a shared glossary.
+几周之内,随着你们采用新的词汇来描述飞船的功能,你们的表达会变得更加精确:“增加三号货舱的氧气水平”或“启动小型推进器”。
+再过几个月,你们可能已经为整个复杂的流程采用了新的语言:“启动着陆程序”或“准备跳跃”。
+这一过程会非常自然地发生,而无需正式构建一个共享术语表的努力。
+
[role="nobreakinside less_space"]
.This Is Not a DDD Book. You Should Read a DDD Book.
*****************************************************************
@@ -85,22 +122,42 @@ architecture patterns that we cover in this book—including Entity, Aggregate,
Value Object (see <>), and Repository (in
<>)—come from the DDD tradition.
+领域驱动设计(Domain-Driven Design,简称DDD)推广了领域建模的概念,脚注:[
+DDD 并非领域建模的起源。Eric Evans 提及了 Rebecca Wirfs-Brock 和 Alan McKean
+所著的 2002 年出版的《_Object Design_》(Addison-Wesley Professional),
+该书引入了责任驱动设计(Responsibility-Driven Design),而DDD是其一个专注于领域的特殊案例。
+但即便如此,时间点仍然显得较晚,面向对象(OO)的爱好者会告诉你可以更早回溯到 Ivar Jacobson 和 Grady Booch;
+这一术语自上世纪80年代中期就已存在。((("domain driven design (DDD)")))]
+通过专注于核心业务领域,DDD 在彻底改变人们的软件设计方式方面取得了巨大的成功。
+本书中涵盖的许多架构模式——包括实体(Entity)、聚合(Aggregate)、值对象(Value Object,
+详见 <>)以及仓储(Repository,详见 <>)——都源于DDD的传统。
+
In a nutshell, DDD says that the most important thing about software is that it
provides a useful model of a problem. If we get that model right, our
software delivers value and makes new things possible.
+简而言之,DDD 认为软件最重要的事情是它能够提供一个问题的有用模型。如果我们把这个模型设计正确,软件就能够创造价值,并使新的事物成为可能。
+
If we get the model wrong, it becomes an obstacle to be worked around. In this book,
we can show the basics of building a domain model, and building an architecture
around it that leaves the model as free as possible from external constraints,
so that it's easy to evolve and change.
+如果我们把模型设计错了,它就会成为需要绕开的障碍。在本书中,我们会展示构建领域模型的基础知识,以及围绕领域模型构建的架构,
+尽可能让模型不受外部约束的影响,以便它能够轻松演化和变更。
+
But there's a lot more to DDD and to the processes, tools, and techniques for
developing a domain model. We hope to give you a taste of it, though,
and cannot encourage you enough to go on and read a proper DDD book:
+但是,DDD 及其用于开发领域模型的流程、工具和技术还有更多内容可以探讨。我们希望能够让你初步了解这些内容,
+并强烈鼓励你进一步阅读一本真正的DDD专著:
+
* The original "blue book," _Domain-Driven Design_ by Eric Evans (Addison-Wesley Professional)
+原版的“蓝皮书”,Eric Evans 所著的《_Domain-Driven Design_》(Addison-Wesley Professional)。
* The "red book," _Implementing Domain-Driven Design_
by Vaughn Vernon (Addison-Wesley Professional)
+“红皮书”,Vaughn Vernon 所著的《_Implementing Domain-Driven Design_》(Addison-Wesley Professional)。
*****************************************************************
@@ -108,23 +165,34 @@ So it is in the mundane world of business. The terminology used by business
stakeholders represents a distilled understanding of the domain model, where
complex ideas and processes are boiled down to a single word or phrase.
+在平凡的商业世界中也是如此。业务利益相关者使用的术语代表了对领域模型的提炼理解,其中复杂的理念和流程被简化为一个词或短语。
+
When we hear our business stakeholders using unfamiliar words, or using terms
in a specific way, we should listen to understand the deeper meaning and encode
their hard-won experience into our software.
+当我们听到业务利益相关者使用不熟悉的词汇,或以特定方式使用术语时,我们应该仔细倾听,去理解其更深层次的含义,并将他们来之不易的经验融入到我们的软件中。
+
We're going to use a real-world domain model throughout this book, specifically
a model from our current employment. MADE.com is a successful furniture
retailer. We source our furniture from manufacturers all over the world and
sell it across Europe.
+在本书中,我们将使用一个真实世界的领域模型,具体来说,是来自我们当前工作的一个模型。MADE.com 是一家成功的家具零售商。我们从世界各地的制造商采购家具,并将其销往整个欧洲。
+
When you buy a sofa or a coffee table, we have to figure out how best
to get your goods from Poland or China or Vietnam and into your living room.
+当你购买一张沙发或一张咖啡桌时,我们需要解决如何将你的商品从波兰、中国或越南高效地送到你的客厅。
+
At a high level, we have separate systems that are responsible for buying
stock, selling stock to customers, and shipping goods to customers. A
system in the middle needs to coordinate the process by allocating stock
to a customer's orders; see <>.
+从宏观上看,我们有独立的系统分别负责采购库存、向客户销售库存以及向客户运输商品。
+而中间的一个系统需要通过将库存分配给客户的订单来协调整个流程;详见 <>。
+
[[allocation_context_diagram]]
.Context diagram for the allocation service
image::images/apwp_0102.png[]
@@ -161,18 +229,28 @@ business has been presenting stock and lead times based on what is physically
available in the warehouse. If and when the warehouse runs out, a product is
listed as "out of stock" until the next shipment arrives from the manufacturer.
+为了本书的目的,我们假设业务决定实施一种令人兴奋的新方法来分配库存。到目前为止,
+业务一直是根据仓库中实际可用的库存和交货时间来展示商品的。如果仓库的库存耗尽,产品会被标记为“缺货”,
+直到下一批货物从制造商处到达为止。
+
Here's the innovation: if we have a system that can keep track of all our shipments
and when they're due to arrive, we can treat the goods on those ships as
real stock and part of our inventory, just with slightly longer lead times.
Fewer goods will appear to be out of stock, we'll sell more, and the business
can save money by keeping lower inventory in the domestic warehouse.
+创新之处在于:如果我们有一个系统可以追踪所有发货信息以及到货时间,我们就可以将那些在途货物视为真实库存并作为库存的一部分,
+只是交货时间稍长一些。这样一来,缺货的商品会减少,我们会卖出更多商品,同时业务也可以通过降低国内仓库的库存量来节省成本。
+
But allocating orders is no longer a trivial matter of decrementing a single
quantity in the warehouse system. We need a more complex allocation mechanism.
Time for some domain modeling.
+但是,分配订单不再是简单地减少仓库系统中的某个数量这么简单了。我们需要一个更复杂的分配机制。是时候进行领域建模了。
+
=== Exploring the Domain Language
+探索领域语言
((("domain language")))
((("domain modeling", "domain language")))
@@ -181,10 +259,15 @@ have an initial conversation with our business experts and agree on a glossary
and some rules for the first minimal version of the domain model. Wherever
possible, we ask for concrete examples to illustrate each rule.
+理解领域模型需要时间、耐心以及便利贴。我们与业务专家进行初步讨论,并为领域模型的第一个最小版本确定一个词汇表和一些规则。
+在可能的情况下,我们会要求提供具体的示例来说明每条规则。
+
We make sure to express those rules in the business jargon (the _ubiquitous
language_ in DDD terminology). We choose memorable identifiers for our objects
so that the examples are easier to talk about.
+我们确保使用业务术语(在 DDD 术语中称为_通用语言_)来表达这些规则。我们为对象选择易于记忆的标识符,这样可以更方便地讨论这些示例。
+
<> shows some notes we might have taken while having a
conversation with our domain experts about allocation.
@@ -194,49 +277,77 @@ conversation with our domain experts about allocation.
A _product_ is identified by a _SKU_, pronounced "skew," which is short for _stock-keeping unit_. _Customers_ place _orders_. An order is identified by an _order reference_
and comprises multiple _order lines_, where each line has a _SKU_ and a _quantity_. For example:
+_产品_通过_SKU_(读作“skew”,是库存管理单元的缩写)进行标识。_客户_会下达_订单_。一个订单通过一个_订单引用_来标识,并包含多个_订单行_,每个订单行都有一个_SKU_和_数量_。例如:
+
- 10 units of RED-CHAIR
+10 件 RED-CHAIR
- 1 unit of TASTELESS-LAMP
+1 件 TASTELESS-LAMP
The purchasing department orders small _batches_ of stock. A _batch_ of stock has a unique ID called a _reference_, a _SKU_, and a _quantity_.
+采购部门会订购小的_批次_库存。一个_批次_库存具备一个名为_引用_的唯一 ID、一个_SKU_和一个_数量_。
+
We need to _allocate_ _order lines_ to _batches_. When we've allocated an
order line to a batch, we will send stock from that specific batch to the
customer's delivery address. When we allocate _x_ units of stock to a batch, the _available quantity_ is reduced by _x_. For example:
+我们需要将_订单行_分配(_allocate_)到_批次_。当我们将某条订单行分配到某个批次时,我们会从该特定批次发送库存到客户的配送地址。当我们将_x_单位的库存分配到一个批次时,该批次的_可用数量_会减少_x_。例如:
+
- We have a batch of 20 SMALL-TABLE, and we allocate an order line for 2 SMALL-TABLE.
+我们有一个包含 20 件 SMALL-TABLE 的批次,我们将一条订单行分配了 2 件 SMALL-TABLE。
- The batch should have 18 SMALL-TABLE remaining.
+该批次应剩余 18 件 SMALL-TABLE。
We can't allocate to a batch if the available quantity is less than the quantity of the order line. For example:
+如果批次的可用数量小于订单行的数量,我们就无法分配。例如:
+
- We have a batch of 1 BLUE-CUSHION, and an order line for 2 BLUE-CUSHION.
+我们有一个包含 1 件 BLUE-CUSHION 的批次,而订单行需要 2 件 BLUE-CUSHION。
- We should not be able to allocate the line to the batch.
+我们不应该能够将该订单行分配到该批次。
We can't allocate the same line twice. For example:
+我们不能两次分配同一行。例如:
+
- We have a batch of 10 BLUE-VASE, and we allocate an order line for 2 BLUE-VASE.
+我们有一批包含10个BLUE-VASE,然后我们为一个订单分配了2个BLUE-VASE。
- If we allocate the order line again to the same batch, the batch should still
have an available quantity of 8.
+如果我们再次将订单行分配到同一批次,该批次的可用数量仍应为8。
Batches have an _ETA_ if they are currently shipping, or they may be in _warehouse stock_. We allocate to warehouse stock in preference to shipment batches. We allocate to shipment batches in order of which has the earliest ETA.
+
+批次如果正在运输会有 _ETA_ ,或者可能处于 _仓库库存_ 状态。我们优先分配给仓库库存,而不是运输批次。对于运输批次,我们按照最早 _ETA_ 的顺序进行分配。
****
=== Unit Testing Domain Models
+领域模型的单元测试
((("unit testing", "of domain models", id="ix_UTDM")))
((("domain modeling", "unit testing domain models", id="ix_dommodUT")))
We're not going to show you how TDD works in this book, but we want to show you
how we would construct a model from this business conversation.
+我们不会在本书中向您展示TDD的工作原理,但我们想向您展示我们如何从这场业务对话中构建模型。
+
[role="nobreakinside less_space"]
-.Exercise for the Reader
+.Exercise for the Reader(读者练习)
******************************************************************************
Why not have a go at solving this problem yourself? Write a few unit tests to
see if you can capture the essence of these business rules in nice, clean
code (ideally without looking at the solution we came up with below!)
+为什么不自己动手尝试解决这个问题呢?编写一些单元测试,看看是否可以用优雅、简洁的代码捕捉这些业务规则的核心(最好不要偷看我们下面提出的解决方案!)
+
You'll find some https://github.com/cosmicpython/code/tree/chapter_01_domain_model_exercise[placeholder unit tests on GitHub], but you could just start from
scratch, or combine/rewrite them however you like.
+你会在 https://github.com/cosmicpython/code/tree/chapter_01_domain_model_exercise[GitHub 上找到一些占位单元测试],
+但你也可以从头开始,或者随意组合/重写它们。
+
//TODO: add test_cannot_allocate_same_line_twice ?
//(EJ3): nice to have for completeness, but not necessary
@@ -244,6 +355,8 @@ scratch, or combine/rewrite them however you like.
Here's what one of our first tests might look like:
+以下是我们最初的一个测试可能的样子:
+
[[first_test]]
.A first test for allocation (test_batches.py)
====
@@ -264,9 +377,14 @@ system, and the names of the classes and variables that we use are taken from th
business jargon. We could show this code to our nontechnical coworkers, and
they would agree that this correctly describes the behavior of the system.
+我们的单元测试名称描述了我们期望系统表现出的行为,而我们使用的类名和变量名来源于业务术语。
+我们可以将这段代码展示给我们的非技术同事,他们会认可这段代码正确地描述了系统的行为。
+
[role="pagebreak-before"]
And here is a domain model that meets our requirements:
+以下是一个符合我们需求的领域模型:
+
[[domain_model_1]]
.First cut of a domain model for batches (model.py)
====
@@ -296,6 +414,8 @@ class Batch:
with no behavior.footnote:[In previous Python versions, we
might have used a namedtuple. You could also check out Hynek Schlawack's
excellent https://pypi.org/project/attrs[attrs].]
+`OrderLine` 是一个不可变的 dataclass,没有任何行为。脚注:[在早期版本的 _Python_ 中,
+我们可能会使用 namedtuple。你也可以去了解一下 Hynek Schlawack 出色的 https://pypi.org/project/attrs[attrs]。]
<2> We're not showing imports in most code listings, in an attempt to keep them
clean. We're hoping you can guess
@@ -304,12 +424,17 @@ class Batch:
anything, you can see the full working code for each chapter in
its branch (e.g.,
https://github.com/cosmicpython/code/tree/chapter_01_domain_model[chapter_01_domain_model]).
+在大多数代码清单中,我们没有展示导入内容,以尽量保持简洁。我们希望你能猜到这是通过 `from dataclasses import dataclass` 引入的;
+同样的还有 `typing.Optional` 和 `datetime.date`。如果你想核实任何内容,可以在相应分支中查看每章的完整可运行代码
+(例如,https://github.com/cosmicpython/code/tree/chapter_01_domain_model[chapter_01_domain_model])。
<3> Type hints are still a matter of controversy in the Python world. For
domain models, they can sometimes help to clarify or document what the
expected arguments are, and people with IDEs are often grateful for them.
You may decide the price paid in terms of readability is too high.
((("type hints")))
+类型提示在 _Python_ 世界中仍然是一个有争议的话题。对于领域模型来说,它们有时可以帮助澄清或记录预期的参数是什么,
+而使用 IDE 的人通常会对此表示感激。不过你可能会认为为此付出的可读性代价过高。
Our implementation here is trivial:
a `Batch` just wraps an integer `available_quantity`,
@@ -320,8 +445,19 @@ Or perhaps you think there's not enough code?
What about some sort of check that the SKU in the `OrderLine` matches `Batch.sku`?
We saved some thoughts on validation for <>.]
+我们的实现非常简单:
+一个 `Batch` 只是包装了一个整数 `available_quantity`,
+我们在分配时对这个值进行递减。
+我们写了相当多的代码,只是为了实现从一个数字中减去另一个数字,
+但我们认为,精确地建模我们的领域会有所回报。脚注:
+[或者你认为代码还不够?
+那是否应该加入某种检查,用于验证 `OrderLine` 中的 SKU 是否匹配 `Batch.sku`?
+关于校验的一些想法,我们保存在了 <> 中。]
+
Let's write some new failing tests:
+让我们编写一些新的失败测试:
+
[[test_can_allocate]]
.Testing logic for what we can allocate (test_batches.py)
@@ -359,10 +495,16 @@ the same SKU; and we've written four simple tests for a new method
`can_allocate`. Again, notice that the names we use mirror the language of our
domain experts, and the examples we agreed upon are directly written into code.
+这里没有什么太出乎意料的地方。我们对测试套件进行了重构,以避免为同一个 SKU 创建批次和订单行时重复相同的代码;
+然后我们为新方法 `can_allocate` 编写了四个简单的测试。同样需要注意的是,我们使用的名称反映了领域专家的语言,
+而我们事先商定的示例也被直接编写进了代码中。
+
We can implement this straightforwardly, too, by writing the `can_allocate`
method of `Batch`:
+我们也可以通过编写 `Batch` 的 `can_allocate` 方法来简单直接地实现这一点:
+
[[can_allocate]]
.A new method in the model (model.py)
====
@@ -377,6 +519,9 @@ So far, we can manage the implementation by just incrementing and decrementing
`Batch.available_quantity`, but as we get into `deallocate()` tests, we'll be
forced into a more intelligent solution:
+到目前为止,我们可以仅通过增加和减少 `Batch.available_quantity` 来管理实现,
+但随着我们进入 `deallocate()` 测试时,我们将不得不采用一个更智能的解决方案:
+
[role="pagebreak-before"]
[[test_deallocate_unallocated]]
.This test is going to require a smarter model (test_batches.py)
@@ -396,6 +541,9 @@ needs to understand which lines have been allocated. Let's look at the
implementation:
+在这个测试中,我们断言从批次中解除一个订单行分配没有任何效果,除非该批次之前已经分配了该订单行。为了实现这一点,
+我们的 `Batch` 需要了解哪些订单行已被分配。让我们来看一下实现:
+
[[domain_model_complete]]
.The domain model now tracks allocations (model.py)
====
@@ -473,13 +621,21 @@ Now we're getting somewhere! A batch now keeps track of a set of allocated
just add to the set. Our `available_quantity` is now a calculated property:
purchased quantity minus allocated quantity.
+现在我们有点进展了!一个批次现在会跟踪一组已分配的 `OrderLine` 对象。当我们进行分配时,如果有足够的可用数量,我们就将订单行添加到集合中。
+我们的 `available_quantity` 现在是一个计算属性:采购数量减去分配数量。
+
Yes, there's plenty more we could do. It's a little disconcerting that
both `allocate()` and `deallocate()` can fail silently, but we have the
basics.
+是的,我们还有很多可以改进的地方。目前有些令人不安的是,`allocate()` 和 `deallocate()` 都可能以静默方式失败,
+但我们已经实现了基础功能。
+
Incidentally, using a set for `._allocations` makes it simple for us
to handle the last test, because items in a set are unique:
+顺便提一下,使用集合 (`set`) 来存储 `._allocations` 使我们可以轻松处理最后一个测试,因为集合中的元素是唯一的:
+
[[last_test]]
.Last batch test! (test_batches.py)
@@ -506,6 +662,12 @@ warehouse in a different region if we're out of stock in the home region. And
so on. A real business in the real world knows how to pile on complexity faster
than we can show on the page!
+目前,批评领域模型过于简单,以至于无需使用领域驱动设计(DDD)(甚至不用面向对象编程!)可能是合理的。
+在现实生活中,会出现无数的业务规则和边界情况:例如,客户可能会要求在特定的未来日期送货,
+这意味着我们可能不希望将他们的订单分配到最早的批次。一些SKU(库存单位)并不在批次中,而是直接从供应商按需订购,
+因此它们遵循不同的逻辑。根据客户所在的位置,我们只能将订单分配给他们所在区域内的一部分仓库和运输点——不过有些SKU在家乡区域库存不足时,
+我们也愿意从其他区域的仓库发货。诸如此类的复杂情况数不胜数!现实世界中的真实业务堆叠复杂性的速度,比我们在页面上展示的还要快!
+
But taking this simple domain model as a placeholder for something more
complex, we're going to extend our simple domain model in the rest of the book
and plug it into the real world of APIs and databases and spreadsheets. We'll
@@ -513,14 +675,19 @@ see how sticking rigidly to our principles of encapsulation and careful
layering will help us to avoid a ball of mud.
+不过,我们将把这个简单的领域模型作为更复杂事物的占位符,并在本书的其余部分扩展这个简单的领域模型,
+将其融入真实世界中的 APIs、数据库和电子表格。我们会看到,坚持封装原则和精心设计的分层结构,将如何帮助我们避免陷入一团混乱。
+
[role="nobreakinside"]
-.More Types for More Type Hints
+.More Types for More Type Hints(更多类型以加强类型提示)
*******************************************************************************
((("type hints")))
If you really want to go to town with type hints, you could go so far as
wrapping primitive types by using `typing.NewType`:
+如果你真的想在类型提示上大展身手,可以通过使用 `typing.NewType` 将原始类型包装起来:
+
[[too_many_types]]
.Just taking it way too far, Bob
====
@@ -547,11 +714,16 @@ class Batch:
That would allow our type checker to make sure that we don't pass a `Sku` where a
`Reference` is expected, for example.
+例如,这将允许我们的类型检查器确保我们不会在需要 `Reference` 的地方误传入一个 `Sku`。
+
Whether you think this is wonderful or appalling is a matter of debate.footnote:[It is appalling. Please, please don't do this. —Harry]
+你认为这是绝妙的还是糟糕的,这方面见仁见智。脚注:[这是糟糕的,拜托,千万别这么做。——Harry]
+
*******************************************************************************
==== Dataclasses Are Great for Value Objects
+数据类非常适合作为值对象
((("value objects", "using dataclasses for")))
((("dataclasses", "use for value objects")))
@@ -561,6 +733,9 @@ line? In our business language, an _order_ has multiple _line_ items, where
each line has a SKU and a quantity. We can imagine that a simple YAML file
containing order information might look like this:
+在之前的代码示例中,我们广泛使用了 `line`,但什么是 line 呢?在我们的业务语言中,一个 _订单_(order)包含多个 _订单行_(line)项目,
+其中每个订单行都有一个 SKU 和一个数量。我们可以想象一个简单的包含订单信息的 YAML 文件可能如下所示:
+
[[yaml_order_example]]
.Order info as YAML
@@ -585,12 +760,18 @@ Notice that while an order has a _reference_ that uniquely identifies it, a
_line_ does not. (Even if we add the order reference to the `OrderLine` class,
it's not something that uniquely identifies the line itself.)
+请注意,虽然一个订单有一个能够唯一标识它的 _reference_(引用),但一个 _line_(订单行)没有。
+(即使我们将订单的引用添加到 `OrderLine` 类中,它也无法唯一标识订单行本身。)
+
((("value objects", "defined")))
Whenever we have a business concept that has data but no identity, we
often choose to represent it using the _Value Object_ pattern. A _value object_ is any
domain object that is uniquely identified by the data it holds; we usually
make them immutable:
+当我们遇到某个具有数据但没有唯一标识的业务概念时,我们通常会选择用 _值对象_(Value Object)模式来表示它。
+一个 _值对象_ 是能够由其持有的数据唯一标识的领域对象;我们通常将它们设计为不可变的:
+
// [SG] seems a bit odd to hear about value objects before any mention of entities.
[[orderline_value_object]]
@@ -612,6 +793,9 @@ One of the nice things that dataclasses (or namedtuples) give us is _value
equality_, which is the fancy way of saying, "Two lines with the same `orderid`,
`sku`, and `qty` are equal."
+数据类(或 namedtuples)提供的一个好处是 _值相等_(value equality),这是一个高大上的说法,
+用来表达:“两个具有相同 `orderid`、`sku` 和 `qty` 的订单行是相等的。”
+
[[more_value_objects]]
.More examples of value objects
@@ -650,6 +834,10 @@ product code, and quantity. We can still have complex behavior on a value
object, though. In fact, it's common to support operations on values; for
example, mathematical operators:
+这些值对象符合我们对其值在现实世界中如何运作的直观理解。我们谈论的究竟是 _哪张_ 10英镑纸币并不重要,因为它们的面值是相同的。
+同样地,如果名字和姓氏都相同,那么两个姓名就是相等的;而如果两个订单行具有相同的客户订单、产品代码和数量,它们也是等价的。
+不过,值对象仍然可以具有复杂的行为。事实上,支持基于值的操作是很常见的,比如数学运算符操作:
+
[[value_object_maths_tests]]
.Testing Math with value objects
@@ -685,6 +873,8 @@ def multiplying_two_money_values_is_an_error():
To get those tests to actually pass you'll need to start implementing some
magic methods on our `Money` class:
+为了让那些测试真正通过,你需要开始在我们的 `Money` 类上实现一些魔术方法:
+
[[value_object_maths]]
.Implementing Math with value objects
====
@@ -707,6 +897,7 @@ class Money:
==== Value Objects and Entities
+值对象与实体
((("value objects", "and entities", secondary-sortas="entities")))
((("domain modeling", "unit testing domain models", "value objects and entities")))
@@ -716,14 +907,23 @@ value object: any object that is identified only by its data and doesn't have a
long-lived identity. What about a batch, though? That _is_ identified by a
reference.
+一个订单行是由其订单ID、SKU 和数量唯一标识的;如果我们更改其中的一个值,就得到了一个新的订单行。
+这就是值对象的定义:任何仅由其数据标识且没有长期存在标识的对象。
+那么,对于一个批次(batch)呢?它是由一个引用(reference)标识的。
+
((("entities", "defined")))
We use the term _entity_ to describe a domain object that has long-lived
identity. On the previous page, we introduced a `Name` class as a value object.
If we take the name Harry Percival and change one letter, we have the new
`Name` object Barry Percival.
+我们使用术语 _实体_(entity)来描述具有长期标识的领域对象。在前一页中,我们引入了一个作为值对象的 `Name` 类。
+如果我们将名字 "Harry Percival" 改变一个字母,就会得到一个新的 `Name` 对象 "Barry Percival"。
+
It should be clear that Harry Percival is not equal to Barry Percival:
+显然,Harry Percival 不等于 Barry Percival:
+
[[test_equality]]
.A name itself cannot change...
@@ -742,6 +942,9 @@ marital status, and even their gender, but we continue to recognize them as the
same individual. That's because humans, unlike names, have a persistent
_identity_:
+但是作为一个 _人_ 的 Harry 呢?人可以改变他们的名字、婚姻状况,甚至性别,但是我们仍然将他们视为同一个个体。
+这是因为人类与名字不同,拥有一个持久的 _身份_:
+
[[person_identity]]
.But a person can!
@@ -774,10 +977,15 @@ and they are still recognizably the same thing. Batches, in our example, are
entities. We can allocate lines to a batch, or change the date that we expect
it to arrive, and it will still be the same entity.
+实体与值对象不同,具有 _身份相等_(identity equality)。我们可以更改它们的值,但它们仍然可以被识别为同一个事物。
+在我们的示例中,批次(batches)是实体。我们可以将订单行分配到一个批次,或者更改我们期望它到达的日期,但它仍然是同一个实体。
+
((("equality operators, implementing on entities")))
We usually make this explicit in code by implementing equality operators on
entities:
+我们通常通过在实体上实现相等运算符来在代码中显式表达这一点:
+
[[equality_on_batches]]
@@ -804,6 +1012,9 @@ Python's +++__eq__+++ magic method
defines the behavior of the class for the `==` operator.footnote:[The
+++__eq__+++ method is pronounced "dunder-EQ." By some, at least.]
+_Python_ 的 +++__eq__+++ 魔术方法定义了类在 `==` 运算符下的行为。
+脚注:[+++__eq__+++ 方法的发音是“dunder-EQ”(双下划线 EQ),至少对某些人来说是这样的。]
+
((("magic methods", "__hash__", secondary-sortas="hash")))
((("__hash__ magic method", primary-sortas="hash")))
For both entity and value objects, it's also worth thinking through how
@@ -811,10 +1022,15 @@ For both entity and value objects, it's also worth thinking through how
behavior of objects when you add them to sets or use them as dict keys;
you can find more info https://oreil.ly/YUzg5[in the Python docs].
+对于实体和值对象,同样值得深入思考 +++__hash__+++ 的工作原理。这是 _Python_ 用来控制对象在被添加到
+集合(sets)中或用作字典(dict)键时行为的魔术方法;更多信息可以参考 https://oreil.ly/YUzg5[Python 官方文档]。
+
For value objects, the hash should be based on all the value attributes,
and we should ensure that the objects are immutable. We get this for
free by specifying `@frozen=True` on the dataclass.
+对于值对象,哈希值应基于所有的值属性,并且我们应确保这些对象是不可变的。通过在数据类上指定 `@frozen=True`,我们可以免费获得这一特性。
+
For entities, the simplest option is to say that the hash is ++None++, meaning
that the object is not hashable and cannot, for example, be used in a set.
If for some reason you decide you really do want to use set or dict operations
@@ -822,6 +1038,9 @@ with entities, the hash should be based on the attribute(s), such as
`.reference`, that defines the entity's unique identity over time. You should
also try to somehow make _that_ attribute read-only.
+对于实体,最简单的选择是将哈希值设置为 ++None++,这意味着对象是不可哈希的,因此不能用于集合(set)中。例如,如果出于某些原因你确实想对实体
+使用集合或字典操作,哈希值应基于那些定义实体唯一标识的属性,比如 `.reference`。同时,你还应该尽量使 _该_ 属性只读。
+
WARNING: This is tricky territory; you shouldn't modify +++__hash__+++
without also modifying +++__eq__+++. If you're not sure what
you're doing, further reading is suggested.
@@ -829,20 +1048,24 @@ WARNING: This is tricky territory; you shouldn't modify +++__hash__
Hynek Schlawack is a good place to start.
((("unit testing", "of domain models", startref="ix_UTDM")))
((("domain modeling", "unit testing domain models", startref="ix_dommodUT")))
-
+这是一个棘手的领域;如果你修改了 +++__hash__+++,同时也需要修改 +++__eq__+++。
+如果你不确定自己在做什么,建议进一步阅读相关内容。可以从我们的技术审阅者 Hynek Schlawack 所著的 https://oreil.ly/vxkgX[《Python Hashes and Equality》] 开始学习。
=== Not Everything Has to Be an Object: A Domain Service Function
+并不是所有东西都必须是对象:领域服务函数
((("domain services")))
((("domain modeling", "functions for domain services", id="ix_dommodfnc")))
We've made a model to represent batches, but what we actually need
to do is allocate order lines against a specific set of batches that
represent all our stock.
+我们已经创建了一个用于表示批次的模型,但我们实际需要做的是将订单行分配到表示我们所有库存的一组特定批次中。
[quote, Eric Evans, Domain-Driven Design]
____
Sometimes, it just isn't a thing.
+有时候,它根本就不需要是一个“东西”。
____
((("service-layer services vs. domain services")))
@@ -858,8 +1081,16 @@ function, and we can take advantage of the fact that Python is a multiparadigm
language and just make it a function.
((("domain services", "function for")))
+Evans 讨论了领域服务(Domain Service)的操作,这些操作在实体或值对象中没有一个自然的归宿。
+脚注:[领域服务与<>中的服务并不是同一个概念,尽管它们常常密切相关。
+领域服务代表的是一个业务概念或流程,而服务层服务代表的是应用程序的一个用例。通常服务层会调用领域服务。]
+一个用于在给定一组批次的情况下分配订单行的“东西”,听起来更像是一个函数。我们可以利用 _Python_ 是一种多范式语言的特点,
+直接将其实现为一个函数。
+
Let's see how we might test-drive such a function:
+让我们来看一下如何通过测试驱动的方式构建这样一个函数:
+
[[test_allocate]]
.Testing our domain service (test_allocate.py)
@@ -902,6 +1133,8 @@ def test_returns_allocated_batch_ref():
((("functions", "for domain services")))
And our service might look like this:
+我们的服务可能看起来像这样:
+
[[domain_service]]
.A standalone function for our domain service (model.py)
@@ -917,6 +1150,7 @@ def allocate(line: OrderLine, batches: List[Batch]) -> str:
====
==== Python's Magic Methods Let Us Use Our Models with Idiomatic Python
+_Python_ 的魔法方法让我们可以用惯用的 _Python_ 风格来使用我们的模型
((("__gt__ magic method", primary-sortas="gt")))
((("magic methods", "allowing use of domain model with idiomatic Python")))
@@ -924,8 +1158,12 @@ You may or may not like the use of `next()` in the preceding code, but we're pre
sure you'll agree that being able to use `sorted()` on our list of
batches is nice, idiomatic Python.
+你可能会喜欢或不喜欢前面代码中使用 `next()`,但我们很确定你会同意能够对我们的批次列表使用 `sorted()` 是不错的、符合 _Python_ 惯用风格的做法。
+
To make it work, we implement +++__gt__+++ on our domain model:
+为了让其正常工作,我们在我们的领域模型上实现了 +++__gt__+++:
+
[[dunder_gt]]
@@ -947,6 +1185,8 @@ class Batch:
That's lovely.
+那真是太好了。
+
==== Exceptions Can Express Domain Concepts Too
@@ -957,6 +1197,9 @@ concepts too. In our conversations with domain experts, we've learned about the
possibility that an order cannot be allocated because we are _out of stock_,
and we can capture that by using a _domain exception_:
+我们还有一个最后的概念需要探讨:异常也可以用来表达领域概念。在与领域专家的交流中,我们了解到订单可能无法分配的情况,
+因为我们处于 _缺货_ 状态,我们可以通过使用 _领域异常_ 来捕获这种情况:
+
[[test_out_of_stock]]
.Testing out-of-stock exception (test_allocate.py)
@@ -974,15 +1217,16 @@ def test_raises_out_of_stock_exception_if_cannot_allocate():
[role="nobreakinside"]
-.Domain Modeling Recap
+.Domain Modeling Recap(领域建模总结)
*****************************************************************
-Domain modeling::
+Domain modeling(领域建模)::
This is the part of your code that is closest to the business,
the most likely to change, and the place where you deliver the
most value to the business. Make it easy to understand and modify.
((("domain modeling", startref="ix_dommod")))
+这是你的代码中最贴近业务的部分,也是最有可能发生变化的地方,同时也是你为业务带来最大价值的地方。确保它易于理解和修改。
-Distinguish entities from value objects::
+Distinguish entities from value objects(区分实体与值对象)::
A value object is defined by its attributes. It's usually best
implemented as an immutable type. If you change an attribute on
a Value Object, it represents a different object. In contrast,
@@ -991,21 +1235,27 @@ Distinguish entities from value objects::
an entity (usually some sort of name or reference field).
((("entities", "value objects versus")))
((("value objects", "entities versus")))
+值对象由其属性定义。通常最好将其实现为不可变类型。如果你更改值对象的一个属性,它就代表了一个不同的对象。
+相比之下,实体的属性可能会随时间变化,但它仍然是同一个实体。关键是要定义清楚是什么 _确实_ 唯一标识一个实体(通常是某种名称或引用字段)。
-Not everything has to be an object::
+Not everything has to be an object(并不是所有东西都必须是对象)::
Python is a multiparadigm language, so let the "verbs" in your
code be functions. For every `FooManager`, `BarBuilder`, or `BazFactory`,
there's often a more expressive and readable `manage_foo()`, `build_bar()`,
or `get_baz()` waiting to happen.
((("functions")))
+_Python_ 是一门多范式语言,所以让代码中的“动词”成为函数。对于每一个 `FooManager`、`BarBuilder` 或 `BazFactory`,
+通常可以找到更加具有表现力和可读性的 `manage_foo()`、`build_bar()` 或 `get_baz()` 来代替。
-This is the time to apply your best OO design principles::
+This is the time to apply your best OO design principles(这是应用你最佳面向对象设计原则的时候。)::
Revisit the SOLID principles and all the other good heuristics like "has a versus is-a,"
"prefer composition over inheritance," and so on.
((("object-oriented design principles")))
+重新审视 SOLID 原则以及其他优秀的设计启发,比如“有一个(Has-a) vs 是一个(Is-a)”、“优先使用组合而非继承”等等。
-You'll also want to think about consistency boundaries and aggregates::
+You'll also want to think about consistency boundaries and aggregates(你还需要考虑一致性边界和聚合)::
But that's a topic for <>.
+但这是 <> 的主题。
*****************************************************************
@@ -1013,6 +1263,9 @@ We won't bore you too much with the implementation, but the main thing
to note is that we take care in naming our exceptions in the ubiquitous
language, just as we do our entities, value objects, and services:
+我们不会通过过多的实现细节让你感到枯燥,但需要注意的主要一点是,我们在通用语言中命名异常时,
+与命名我们的实体、值对象和服务一样,需格外用心:
+
[[out_of_stock]]
.Raising a domain exception (model.py)
@@ -1035,6 +1288,8 @@ def allocate(line: OrderLine, batches: List[Batch]) -> str:
<> is a visual representation of where we've ended up.
+<> 是我们最终结果的视觉表示。
+
[[maps_chapter_01_withtext]]
.Our domain model at the end of the chapter
image::images/apwp_0104.png[]
@@ -1042,3 +1297,5 @@ image::images/apwp_0104.png[]
((("domain modeling", "functions for domain services", startref="ix_dommodfnc")))
That'll probably do for now! We have a domain service that we can use for our
first use case. But first we'll need a database...
+
+到这里应该差不多了!我们已经有了一个可以用于首个用例的领域服务。但首先,我们需要一个数据库……
From 697ef941687b1d1b90047f61b172ee50def5995c Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Thu, 30 Jan 2025 22:10:22 +0800
Subject: [PATCH 06/75] update Readme.md chapter_02_repository.asciidoc
---
Readme.md | 4 +-
chapter_02_repository.asciidoc | 254 ++++++++++++++++++++++++++++++++-
2 files changed, 253 insertions(+), 5 deletions(-)
diff --git a/Readme.md b/Readme.md
index 07152bc4..5a2a0592 100644
--- a/Readme.md
+++ b/Readme.md
@@ -20,8 +20,8 @@ O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布
| [Introduction: Why do our designs go wrong?
引言:为什么我们的设计会出问题?(已翻译)](introduction.asciidoc) | ||
| [**Part 1 Intro**](part1.asciidoc) | |
| [Chapter 1: Domain Model
第一章:领域模型(已翻译)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 2: Repository
第二章:仓储(翻译中...)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(未翻译)](chapter_03_abstractions.asciidoc) | |
+| [Chapter 2: Repository
第二章:仓储(已翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(翻译中...)](chapter_03_abstractions.asciidoc) | |
| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(未翻译)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(未翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 6: Unit of Work
第六章:工作单元(未翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
diff --git a/chapter_02_repository.asciidoc b/chapter_02_repository.asciidoc
index cd7bd7fe..55f0b7cb 100644
--- a/chapter_02_repository.asciidoc
+++ b/chapter_02_repository.asciidoc
@@ -1,9 +1,12 @@
[[chapter_02_repository]]
== Repository Pattern
+仓储模式
It's time to make good on our promise to use the dependency inversion principle as
a way of decoupling our core logic from infrastructural concerns.
+是时候兑现我们的承诺,使用依赖倒置原则将核心逻辑与基础设施问题解耦了。
+
((("storage", seealso="repositories; Repository pattern")))
((("Repository pattern")))
((("data storage, Repository pattern and")))
@@ -12,9 +15,14 @@ allowing us to decouple our model layer from the data layer. We'll present a
concrete example of how this simplifying abstraction makes our system more
testable by hiding the complexities of the database.
+我们将引入 _仓储_ 模式,这是一种对数据存储的简化抽象,能够让我们的模型层与数据层解耦。
+我们会提供一个具体示例,展示这种简化抽象如何通过隐藏数据库的复杂性,使我们的系统更具可测试性。
+
<> shows a little preview of what we're going to build:
a `Repository` object that sits between our domain model and the database.
+<> 简要预览了我们将要构建的内容:一个位于领域模型和数据库之间的 `Repository` 对象。
+
[[maps_chapter_02]]
.Before and after the Repository pattern
image::images/apwp_0201.png[]
@@ -24,6 +32,8 @@ image::images/apwp_0201.png[]
The code for this chapter is in the
chapter_02_repository branch https://oreil.ly/6STDu[on GitHub].
+本章的代码位于 GitHub 上的 chapter_02_repository 分支 https://oreil.ly/6STDu[链接]。
+
----
git clone https://github.com/cosmicpython/code.git
cd code
@@ -35,6 +45,7 @@ git checkout chapter_01_domain_model
=== Persisting Our Domain Model
+持久化我们的领域模型
((("domain model", "persisting")))
In <> we built a simple domain model that can allocate orders
@@ -43,27 +54,43 @@ there aren't any dependencies or infrastructure to set up. If we needed to run
a database or an API and create test data, our tests would be harder to write
and maintain.
+在 <> 中,我们构建了一个简单的领域模型,它可以将订单分配给库存批次。
+因为这段代码没有任何依赖或基础设施需要设置,所以我们很容易为其编写测试。
+如果我们需要运行一个数据库或 API 并创建测试数据,那么测试将会更难编写和维护。
+
Sadly, at some point we'll need to put our perfect little model in the hands of
users and contend with the real world of spreadsheets and web
browsers and race conditions. For the next few chapters we're going to look at
how we can connect our idealized domain model to external state.
+遗憾的是,某些时候我们需要将我们完美的小模型交到用户手中,并应对现实世界中存在的电子表格、网页浏览器和竞争条件的问题。
+在接下来的几章中,我们将探讨如何将我们的理想化领域模型连接到外部状态。
+
((("minimum viable product")))
We expect to be working in an agile manner, so our priority is to get to a
minimum viable product as quickly as possible. In our case, that's going to be
a web API. In a real project, you might dive straight in with some end-to-end
tests and start plugging in a web framework, test-driving things outside-in.
+我们希望以敏捷的方式开展工作,因此我们的首要任务是尽快实现一个最小可行产品。在我们的案例中,这将是一个 Web API。在实际项目中,
+你可能会直接从一些端到端测试入手,并开始集成一个 Web 框架,以从外到内进行测试驱动开发。
+
But we know that, no matter what, we're going to need some form of persistent
storage, and this is a textbook, so we can allow ourselves a tiny bit more
bottom-up development and start to think about storage and databases.
+但我们知道,无论如何,我们都会需要某种形式的持久化存储。而且这是一本教科书,所以我们可以稍微允许自己进行一些自下而上的开发,
+开始考虑存储和数据库的问题。
+
=== Some Pseudocode: What Are We Going to Need?
+一些伪代码:我们需要什么?
When we build our first API endpoint, we know we're going to have
some code that looks more or less like the following.
+当我们构建第一个 API 端点时,我们知道会有一些代码大致如下所示。
+
[[api_endpoint_pseudocode]]
.What our first API endpoint will look like
====
@@ -87,15 +114,22 @@ NOTE: We've used Flask because it's lightweight, but you don't need
to be a Flask user to understand this book. In fact, we'll show you how
to make your choice of framework a minor detail.
((("Flask framework")))
+我们使用了 Flask,因为它很轻量化,但你并不需要是 Flask 的用户就能理解本书的内容。
+实际上,我们会向你展示如何让框架的选择成为一个无足轻重的细节。
We'll need a way to retrieve batch info from the database and instantiate our domain
model objects from it, and we'll also need a way of saving them back to the
database.
+我们需要一种方法从数据库中检索批次信息,并据此实例化我们的领域模型对象,同时也需要一种方法将这些对象保存回数据库。
+
_What? Oh, "gubbins" is a British word for "stuff." You can just ignore that. It's pseudocode, OK?_
+_什么?哦,“gubbins”是一个英国词,意思是“东西”。你可以忽略它。这只是伪代码,好吗?_
+
=== Applying the DIP to Data Access
+将依赖倒置原则 (DIP) 应用于数据访问
((("layered architecture")))
((("data access, applying dependency inversion principle to")))
@@ -103,6 +137,9 @@ As mentioned in the <>, a layered architecture is a
approach to structuring a system that has a UI, some logic, and a database (see
<>).
+正如在 <> 中提到的,分层架构是一种常见的方法,用于构建具有用户界面、逻辑和数据库的系统
+(参见 <>)。
+
[role="width-75"]
[[layered_architecture2]]
.Layered architecture
@@ -114,6 +151,9 @@ Model-View-Controller (MVC). In any case, the aim is to keep the layers
separate (which is a good thing), and to have each layer depend only on the one
below it.
+Django 的模型-视图-模板(Model-View-Template, MVT)结构与此密切相关,模型-视图-控制器(Model-View-Controller, MVC)也是如此。
+无论是哪种情况,其目标都是将各层分离(这是一件好事),并使每一层仅依赖其下方的那一层。
+
((("dependencies", "none in domain model")))
But we want our domain model to have __no dependencies whatsoever__.footnote:[
I suppose we mean "no stateful dependencies." Depending on a helper library is
@@ -121,11 +161,18 @@ fine; depending on an ORM or a web framework is not.]
We don't want infrastructure concerns bleeding over into our domain model and
slowing our unit tests or our ability to make changes.
+但我们希望我们的领域模型 __完全没有任何依赖__。脚注:[我想我们指的是“没有有状态的依赖”。
+依赖一个辅助库是可以的;但依赖一个 ORM 或 Web 框架则不行。]
+我们不希望基础设施的相关问题渗透到领域模型中,从而降低单元测试的速度或限制我们进行更改的能力。
+
((("onion architecture")))
Instead, as discussed in the introduction, we'll think of our model as being on the
"inside," and dependencies flowing inward to it; this is what people sometimes call
_onion architecture_ (see <>).
+相反,正如在引言中讨论的那样,我们将把我们的模型视为处于“内部”,依赖关系向内流向它;
+这有时被称为 _洋葱架构_(参见 <>)。
+
[role="width-75"]
[[onion_architecture]]
.Onion architecture
@@ -154,8 +201,11 @@ image::images/apwp_0203.png[]
If you've been reading about architectural patterns, you may be asking
yourself questions like this:
+如果你一直在阅读有关架构模式的内容,你可能会问自己这样的问题:
+
____
_Is this ports and adapters? Or is it hexagonal architecture? Is that the same as onion architecture? What about the clean architecture? What's a port, and what's an adapter? Why do you people have so many words for the same thing?_
+_这是端口与适配器架构吗?还是六边形架构?这和洋葱架构是一样的吗?那“整洁架构”又是什么?什么是端口,什么是适配器?你们为什么用这么多词来描述同一件事?_
____
((("dependency inversion principle")))
@@ -166,19 +216,30 @@ dependency inversion principle: high-level modules (the domain) should
not depend on low-level ones (the infrastructure).footnote:[Mark Seemann has
https://oreil.ly/LpFS9[an excellent blog post] on the topic.]
+尽管有些人喜欢在细节上挑剔这些名称的区别,但它们基本上是同一件事的不同叫法,它们都归结于依赖倒置原则:高层模块(领域)
+不应该依赖低层模块(基础设施)。脚注:[Mark Seemann 在这个主题上写了一篇https://oreil.ly/LpFS9[出色的博客文章]。]
+
We'll get into some of the nitty-gritty around "depending on abstractions,"
and whether there is a Pythonic equivalent of interfaces,
<>. See also <>.
+
+我们将在本书的 <> 部分深入探讨一些关于“依赖抽象”的细节,以及是否存在 _Python_ 式的接口等价物。
+另请参见 <>。
****
=== Reminder: Our Model
+提醒:我们的模型
((("domain model", id="ix_domod")))
Let's remind ourselves of our domain model (see <>):
an allocation is the concept of linking an `OrderLine` to a `Batch`. We're
storing the allocations as a collection on our `Batch` object.
+让我们回顾一下我们的领域模型(参见 <>):
+“分配”是将一个 `OrderLine` 关联到一个 `Batch` 的概念。
+我们将分配存储为 `Batch` 对象上的一个集合。
+
[[model_diagram_reminder]]
.Our model
image::images/apwp_0103.png[]
@@ -186,8 +247,11 @@ image::images/apwp_0103.png[]
Let's see how we might translate this to a relational database.
+让我们看看如何将其转换为关系型数据库。
+
==== The "Normal" ORM Way: Model Depends on ORM
+“常规” ORM 方法:模型依赖于 ORM
((("SQL", "generating for domain model objects")))
((("domain model", "translating to relational database", "normal ORM way, model depends on ORM")))
@@ -195,11 +259,15 @@ These days, it's unlikely that your team members are hand-rolling their own SQL
Instead, you're almost certainly using some kind of framework to generate
SQL for you based on your model objects.
+如今,你的团队成员很可能不再手写 SQL 查询了。相反,你几乎肯定会使用某种框架,根据模型对象为你生成 SQL。
+
((("object-relational mappers (ORMs)")))
These frameworks are called _object-relational mappers_ (ORMs) because they exist to
bridge the conceptual gap between the world of objects and domain modeling and
the world of databases and relational algebra.
+这些框架被称为 _对象关系映射器_(ORM),因为它们的存在是为了弥合对象和领域建模的世界与数据库和关系代数的世界之间的概念差距。
+
((("persistence ignorance")))
The most important thing an ORM gives us is _persistence ignorance_: the idea
that our fancy domain model doesn't need to know anything about how data is
@@ -208,11 +276,19 @@ on particular database technologies.footnote:[In this sense, using an ORM is
already an example of the DIP. Instead of depending on hardcoded SQL, we depend
on an abstraction, the ORM. But that's not enough for us—not in this book!]
+ORM 提供给我们的最重要的功能是 _持久化无感(persistence ignorance)_:即我们的高级领域模型无需了解数据如何加载或持久化。
+这样可以使我们的领域模型避免直接依赖特定的数据库技术。
+脚注:[从这个角度来看,使用 ORM 本身已经是依赖倒置原则(DIP)的一个示例。
+与其依赖硬编码的 SQL,我们依赖的是一个抽象层,即 ORM。
+但这对于我们来说还不够——至少在本书中还不足够!]
+
((("object-relational mappers (ORMs)", "SQLAlchemy, model depends on ORM")))
((("SQLAlchemy", "declarative syntax, model depends on ORM")))
But if you follow the typical SQLAlchemy tutorial, you'll end up with something
like this:
+但如果你按照典型的 SQLAlchemy 教程操作,你最终会得到如下代码:
+
[[typical_sqlalchemy_example]]
.SQLAlchemy "declarative" syntax, model depends on ORM (orm.py)
@@ -247,6 +323,10 @@ Can we really say this model is ignorant of the database? How can it be
separate from storage concerns when our model properties are directly coupled
to database columns?
+即使你不了解 SQLAlchemy,也能看出我们原本干净的模型现在充满了对 ORM 的依赖,而且看起来开始非常难看。
+我们真的还能说这个模型对数据库是无感知的吗?当我们的模型属性直接与数据库列耦合时,
+它怎么可能与存储问题分离?
+
[role="nobreakinside less_space"]
.Django's ORM Is Essentially the Same, but More Restrictive
****
@@ -256,6 +336,8 @@ to database columns?
If you're more used to Django, the preceding "declarative" SQLAlchemy snippet
translates to something like this:
+如果你更熟悉 Django,上述“声明式”的 SQLAlchemy 代码片段可以转换成类似如下的内容:
+
[[django_orm_example]]
.Django ORM example
====
@@ -279,15 +361,20 @@ The point is the same--our model classes inherit directly from ORM
classes, so our model depends on the ORM. We want it to be the other
way around.
+重点是一样的——我们的模型类直接继承自 ORM 类,因此我们的模型依赖于 ORM。而我们希望情况正好相反。
+
Django doesn't provide an equivalent for SQLAlchemy's classical mapper,
but see <> for examples of how to apply dependency
inversion and the Repository pattern to Django.
+Django 不提供与 SQLAlchemy 的经典映射器等价的功能,但请参阅 <>,了解如何将依赖倒置原则和仓储模式应用于 Django 的示例。
+
****
==== Inverting the Dependency: ORM Depends on Model
+依赖倒置:ORM 依赖于模型
((("mappers")))
((("classical mapping")))
@@ -300,6 +387,9 @@ to define your schema separately, and to define an explicit _mapper_ for how to
between the schema and our domain model, what SQLAlchemy calls a
https://oreil.ly/ZucTG[classical mapping]:
+幸运的是,这并不是使用 SQLAlchemy 的唯一方法。另一种方式是单独定义你的模式,并明确定义一个 _映射器_(mapper),
+用于在模式和我们的领域模型之间进行转换,SQLAlchemy 将其称为 https://oreil.ly/ZucTG[经典映射]:
+
[role="nobreakinside less_space"]
[[sqlalchemy_classical_mapper]]
.Explicit ORM mapping with SQLAlchemy Table objects (orm.py)
@@ -331,15 +421,19 @@ def start_mappers():
<1> The ORM imports (or "depends on" or "knows about") the domain model, and
not the other way around.
+ORM 导入(或“依赖于”或“了解”)领域模型,而不是相反的方向。
<2> We define our database tables and columns by using SQLAlchemy's
abstractions.footnote:[Even in projects where we don't use an ORM, we
often use SQLAlchemy alongside Alembic to declaratively create
schemas in Python and to manage migrations, connections,
and sessions.]
+我们使用 SQLAlchemy 的抽象来定义数据库表和列。脚注:[即使在没有使用 ORM 的项目中,我们通常也会结合使用 SQLAlchemy 和 Alembic,
+在 _Python_ 中以声明式创建模式,并管理迁移、连接和会话。]
<3> When we call the `mapper` function, SQLAlchemy does its magic to bind
our domain model classes to the various tables we've defined.
+当我们调用 `mapper` 函数时,SQLAlchemy 施展它的魔法,将我们的领域模型类绑定到我们定义的各个表。
// TODO: replace mapper() with registry.map_imperatively()
// https://docs.sqlalchemy.org/en/14/orm/mapping_styles.html?highlight=sqlalchemy#orm-imperative-mapping
@@ -349,6 +443,9 @@ easily load and save domain model instances from and to the database. But if
we never call that function, our domain model classes stay blissfully
unaware of the database.
+最终的结果是,如果我们调用 `start_mappers`,我们将能够轻松地从数据库加载和保存领域模型实例。
+但如果我们从未调用那个函数,我们的领域模型类将完全不需要了解数据库的存在。
+
// IDEA: add a note about mapper being maybe-deprecated, but link to
// the mailing list post where mike shows how to reimplement it manually.
@@ -356,10 +453,15 @@ This gives us all the benefits of SQLAlchemy, including the ability to use
`alembic` for migrations, and the ability to transparently query using our
domain classes, as we'll see.
+这为我们带来了 SQLAlchemy 的所有好处,包括使用 `alembic` 进行迁移的能力,
+以及使用领域类进行透明查询的能力,正如我们将会看到的那样。
+
((("object-relational mappers (ORMs)", "ORM depends on the data model", "testing the ORM")))
When you're first trying to build your ORM config, it can be useful to write
tests for it, as in the following example:
+当你第一次尝试构建 ORM 配置时,编写测试可能会很有用,例如以下示例所示:
+
[[orm_tests]]
.Testing the ORM directly (throwaway tests) (test_orm.py)
@@ -398,6 +500,9 @@ def test_orderline_mapper_can_save_lines(session):
pytest will inject them to the tests that need them by looking at their
function arguments. In this case, it's a SQLAlchemy database session.
((("pytest", "session argument")))
+如果你没用过 pytest,那么这个测试中的 `session` 参数需要解释一下。对于本书来说,你不必担心 pytest 或其夹具(fixtures)的细节,
+但简短的解释是:你可以将测试中的通用依赖定义为“夹具”,而 pytest 会通过检查测试函数的参数,
+将它们注入到需要的测试中。在这个例子中,`session` 是一个 SQLAlchemy 数据库会话。
////
[SG] I set up the conftest to have a session, and could only get the tests to
@@ -414,12 +519,19 @@ only a small additional step to implement another abstraction called the
Repository pattern, which will be easier to write tests against and will
provide a simple interface for faking out later in tests.
+你可能不会保留这些测试——正如你即将看到的,一旦你完成了 ORM 和领域模型的依赖倒置,
+再实现另一个称为仓储模式(Repository pattern)的抽象就只需迈出一小步。
+该模式将更容易编写测试,并提供一个简单的接口,以便在之后的测试中方便地进行模拟。
+
But we've already achieved our objective of inverting the traditional
dependency: the domain model stays "pure" and free from infrastructure
concerns. We could throw away SQLAlchemy and use a different ORM, or a totally
different persistence system, and the domain model doesn't need to change at
all.
+但我们已经实现了依赖倒置这一目标:领域模型保持“纯粹”,不涉及基础设施问题。我们可以抛弃 SQLAlchemy,
+使用不同的 ORM,甚至是完全不同的持久化系统,而领域模型完全不需要做任何改变。
+
Depending on what you're doing in your domain model, and especially if you
stray far from the OO paradigm, you may find it increasingly hard to get the
@@ -429,10 +541,16 @@ maintainers, and to Mike Bayer in particular.] As so often happens with
architectural decisions, you'll need to consider a trade-off. As the
Zen of Python says, "Practicality beats purity!"
+根据你在领域模型中执行的操作,尤其是当你偏离面向对象(OO)范式时,你可能会发现越来越难以让 ORM 产生满足你需求的准确行为,
+这时可能需要修改领域模型。脚注:[特别感谢极其乐于助人的 SQLAlchemy 维护人员,尤其是 Mike Bayer。] 正如架构决策中经常发生的事情,
+你需要权衡利弊。正如 _Python_ 之禅所说:“实用性胜过纯粹性!”
+
((("SQLAlchemy", "using directly in API endpoint")))
At this point, though, our API endpoint might look something like
the following, and we could get it to work just fine:
+不过,此时我们的 API 端点可能看起来如下所示,而且我们应该可以正常使其工作:
+
[[api_endpoint_with_session]]
.Using SQLAlchemy directly in our API endpoint
====
@@ -470,16 +588,21 @@ add a try finally to close the session
////
=== Introducing the Repository Pattern
+引入仓储模式
((("Repository pattern", id="ix_Repo")))
((("domain model", startref="ix_domod")))
The _Repository_ pattern is an abstraction over persistent storage. It hides the
boring details of data access by pretending that all of our data is in memory.
+_仓储_ 模式是一种对持久存储的抽象。它通过假装所有数据都在内存中,隐藏了数据访问中乏味的细节。
+
If we had infinite memory in our laptops, we'd have no need for clumsy databases.
Instead, we could just use our objects whenever we liked. What would that look
like?
+如果我们的笔记本电脑拥有无限的内存,就不需要笨重的数据库了。我们可以随时使用我们的对象。那么这会是什么样子呢?
+
[[all_my_data]]
.You have to get your data from somewhere
====
@@ -505,8 +628,12 @@ find them again. Our in-memory data would let us add new objects, just like a
list or a set. Because the objects are in memory, we never need to call a
`.save()` method; we just fetch the object we care about and modify it in memory.
+即使我们的对象在内存中,我们仍需要将它们放在 _某个地方_,以便能够再次找到它们。我们的内存数据允许我们像使用列表或集合那样添加新对象。
+由于对象在内存中,我们完全不需要调用 `.save()` 方法;只需获取我们关心的对象并在内存中修改它即可。
+
==== The Repository in the Abstract
+抽象中的仓储模式
((("Repository pattern", "simplest possible repository")))
((("Unit of Work pattern")))
@@ -520,9 +647,17 @@ We stick rigidly to using these methods for data access in our domain and our
service layer. This self-imposed simplicity stops us from coupling our domain
model to the database.
+最简单的仓库只包含两个方法:`add()` 用于将新项目加入仓库,`get()` 用于返回先前添加的项目。
+脚注:[ 你可能会想,“那 `list`、`delete` 或 `update` 呢?” 然而,在理想的情况下,
+我们一次只对模型对象进行修改,而删除通常以软删除的方式处理——比如 `batch.cancel()`。
+最后,更新操作由工作单元(Unit of Work)模式处理,如你将在 <> 中看到的那样。]
+我们严格坚持使用这些方法在领域层和服务层中进行数据访问。这种自我施加的简化能够防止我们的领域模型与数据库耦合。
+
((("abstract base classes (ABCs)", "ABC for the repository")))
Here's what an abstract base class (ABC) for our repository would look like:
+以下是我们的仓库的一个抽象基类(Abstract Base Class, ABC)的样子:
+
[[abstract_repo]]
.The simplest possible repository (repository.py)
====
@@ -547,10 +682,14 @@ class AbstractRepository(abc.ABC):
may be), be running helpers like `pylint` and `mypy`.]
((("@abc.abstractmethod")))
((("abstract methods")))
+_Python_ 提示:`@abc.abstractmethod` 是让抽象基类(ABCs)在 _Python_ 中真正“起作用”的为数不多的机制之一。
+如果一个类没有实现其父类中定义的所有 `abstractmethods`,_Python_ 将拒绝让你实例化该类。
+脚注:[如果想真正充分利用抽象基类的好处(如果它们有的话),可以运行如 `pylint` 和 `mypy` 这样的辅助工具。]
<2> `raise NotImplementedError` is nice, but it's neither necessary nor sufficient.
In fact, your abstract methods can have real behavior that subclasses
can call out to, if you really want.
+`raise NotImplementedError` 很好用,但它既不是必要的,也不是充分的。实际上,如果你确实需要,你的抽象方法甚至可以包含实际的行为,供子类调用。
[role="pagebreak-before less_space"]
.Abstract Base Classes, Duck Typing, and Protocols
@@ -561,6 +700,8 @@ class AbstractRepository(abc.ABC):
We're using abstract base classes in this book for didactic reasons: we hope
they help explain what the interface of the repository abstraction is.
+我们在本书中使用抽象基类是出于教学目的:我们希望它能帮助说明仓库抽象接口的定义。
+
((("duck typing")))
In real life, we've sometimes found ourselves deleting ABCs from our production
code, because Python makes it too easy to ignore them, and they end up
@@ -568,15 +709,23 @@ unmaintained and, at worst, misleading. In practice we often just rely on
Python's duck typing to enable abstractions. To a Pythonista, a repository is
_any_ object that has pass:[add(thing)] and pass:[get(id)] methods.
+在实际工作中,我们有时会从生产代码中删除抽象基类(ABCs),因为 _Python_ 让忽略它们变得太容易了,结果这些类往往无人维护,
+甚至在最坏的情况下会引起误导。实际上,我们经常只是依赖 _Python_ 的鸭子类型来实现抽象。对于一个 _Python_ 开发者来说,
+一个仓库就是 _任何_ 具有 pass:[add(thing)] 和 pass:[get(id)] 方法的对象。
+
((("PEP 544 protocols")))
An alternative to look into is https://oreil.ly/q9EPC[PEP 544 protocols].
These give you typing without the possibility of inheritance, which "prefer
composition over inheritance" fans will particularly like.
+一种可以考虑的替代方案是 https://oreil.ly/q9EPC[PEP 544 协议]。
+它们提供了类型支持,但没有继承的可能性,对于那些提倡“组合优于继承”的爱好者来说,这将特别受欢迎。
+
*******************************************************************************
==== What Is the Trade-Off?
+什么是权衡取舍?
[quote, Rich Hickey]
@@ -584,34 +733,50 @@ ____
You know they say economists know the price of everything and the value of
nothing? Well, programmers know the benefits of everything and the trade-offs
of nothing.
+
+你知道人们常说经济学家知道一切东西的价格,却不知道它们的价值吗?那么,程序员则是知道一切事物的好处,却不了解它们的权衡取舍。
____
((("Repository pattern", "trade-offs")))
Whenever we introduce an architectural pattern in this book, we'll always
ask, "What do we get for this? And what does it cost us?"
+每当我们在本书中引入一种架构模式时,我们都会问:“我们能从中获得什么?而它的代价是什么?”
+
Usually, at the very least, we'll be introducing an extra layer of abstraction,
and although we may hope it will reduce complexity overall, it does add
complexity locally, and it has a cost in terms of the raw numbers of moving parts and
ongoing maintenance.
+通常情况下,至少我们会引入一个额外的抽象层。尽管我们可能希望它能整体上降低复杂性,但它确实会在局部增加复杂性,
+同时在可变部分的数量和持续维护方面也会付出代价。
+
The Repository pattern is probably one of the easiest choices in the book, though,
if you're already heading down the DDD and dependency inversion route. As far
as our code is concerned, we're really just swapping the SQLAlchemy abstraction
(`session.query(Batch)`) for a different one (`batches_repo.get`) that we
designed.
+如果你已经选择了领域驱动设计(DDD)和依赖倒置的路径,那么仓库模式可能是本书中最容易选择的模式之一。
+对于我们的代码来说,我们实际上只是将 SQLAlchemy 的抽象(`session.query(Batch)`)替换为一个我们自己设计的抽象(`batches_repo.get`)。
+
We will have to write a few lines of code in our repository class each time we
add a new domain object that we want to retrieve, but in return we get a
simple abstraction over our storage layer, which we control. The Repository pattern would make
it easy to make fundamental changes to the way we store things (see
<>), and as we'll see, it is easy to fake out for unit tests.
+每次我们新增一个需要检索的领域对象时,都需要在我们的仓库类中编写几行代码,但作为回报,我们获得了一个简单的、由我们掌控的存储层抽象。
+仓库模式让我们可以轻松对存储方式进行根本性的更改(参见 <>), 并且正如我们将会看到的,它也很容易在单元测试中伪造(fake out)。
+
((("domain driven design (DDD)", "Repository pattern and")))
In addition, the Repository pattern is so common in the DDD world that, if you
do collaborate with programmers who have come to Python from the Java and C#
worlds, they're likely to recognize it. <> illustrates the pattern.
+此外,仓库模式在 DDD 世界中非常常见,因此如果你与来自 Java 和 C# 世界的程序员合作,他们可能会认出这个模式。
+<> 展示了这一模式的示意图。
+
[role="width-60"]
[[repository_pattern_diagram]]
.Repository pattern
@@ -646,9 +811,13 @@ integration test, since we're checking that our code (the repository) is
correctly integrated with the database; hence, the tests tend to mix
raw SQL with calls and assertions on our own code.
+一如既往,我们从测试开始。这可能会被归类为集成测试,因为我们要检查我们的代码(仓库)是否正确地与数据库集成;
+因此,这些测试往往会将原始 SQL 和对我们自己代码的调用与断言结合起来。
+
TIP: Unlike the ORM tests from earlier, these tests are good candidates for
staying part of your codebase longer term, particularly if any parts of
your domain model mean the object-relational map is nontrivial.
+与之前的 ORM 测试不同,这些测试非常适合长期保留在你的代码库中,特别是当你的领域模型的某些部分使对象关系映射变得不那么简单时。
[[repo_test_save]]
@@ -671,19 +840,24 @@ def test_repository_can_save_a_batch(session):
====
<1> `repo.add()` is the method under test here.
+`repo.add()` 是这里的被测试方法。
<2> We keep the `.commit()` outside of the repository and make
it the responsibility of the caller. There are pros and cons for
this; some of our reasons will become clearer when we get to
<>.
+我们将 `.commit()` 保留在仓库之外,并将其作为调用者的职责。这么做有利有弊;当我们进入 <> 时,一些原因会变得更加清晰。
<3> We use the raw SQL to verify that the right data has been saved.
+我们使用原始 SQL 来验证是否保存了正确的数据。
((("SQL", "repository test for retrieving complex object")))
((("Repository pattern", "testing the repository with retrieving a complex object")))
The next test involves retrieving batches and allocations, so it's more
complex:
+下一个测试涉及检索批次和分配,因此它更复杂一些:
+
[[repo_test_retrieve]]
.Repository test for retrieving a complex object (test_repository.py)
@@ -727,17 +901,21 @@ def test_repository_can_retrieve_a_batch_with_allocations(session):
<1> This tests the read side, so the raw SQL is preparing data to be read
by the `repo.get()`.
+这个测试关注的是读取部分,因此原始 SQL 用于准备将由 `repo.get()` 读取的数据。
<2> We'll spare you the details of `insert_batch` and `insert_allocation`;
the point is to create a couple of batches, and, for the
batch we're interested in, to have one existing order line allocated to it.
+我们不会详细说明 `insert_batch` 和 `insert_allocation` 的细节;重点是创建几个批次,并为我们感兴趣的那个批次分配一个已有的订单行。
<3> And that's what we verify here. The first `assert ==` checks that the
types match, and that the reference is the same (because, as you remember,
`Batch` is an entity, and we have a custom ++__eq__++ for it).
+这正是我们在这里验证的。第一个 `assert ==` 检查类型是否匹配,以及引用是否相同(因为,如你所记得的,`Batch` 是一个实体,我们为它定义了自定义的 ++__eq__++ 方法)。
<4> So we also explicitly check on its major attributes, including
`._allocations`, which is a Python set of `OrderLine` value objects.
+因此,我们还明确检查了它的主要属性,包括 `._allocations`,这是一个由 `OrderLine` 值对象组成的 _Python_ 集合。
((("Repository pattern", "typical repository")))
Whether or not you painstakingly write tests for every model is a judgment
@@ -747,9 +925,14 @@ at all, if they all follow a similar pattern. In our case, the ORM config
that sets up the `._allocations` set is a little complex, so it merited a
specific test.
+是否为每个模型都细致地编写测试是一个主观判断。一旦你为一个类完成了创建/修改/保存的测试,你可能会满意于仅为其他类编写一个简单的往返测试,
+或者如果它们都遵循类似的模式,甚至可以不编写任何测试。在我们的案例中,设置 `._allocations` 集合的 ORM 配置有些复杂,因此值得编写一个专门的测试。
+
You end up with something like this:
+你最终会得到如下内容:
+
[[batch_repository]]
.A typical repository (repository.py)
@@ -777,6 +960,8 @@ class SqlAlchemyRepository(AbstractRepository):
((("APIs", "using repository directly in API endpoint")))
And now our Flask endpoint might look something like the following:
+现在我们的 Flask 端点可能会看起来如下:
+
[[api_endpoint_with_repo]]
.Using our repository directly in our API endpoint
====
@@ -797,7 +982,7 @@ def allocate_endpoint():
====
[role="nobreakinside less_space"]
-.Exercise for the Reader
+.Exercise for the Reader(留给读者的练习)
******************************************************************************
((("SQL", "ORM and Repository pattern as abstractions in front of")))
@@ -809,19 +994,29 @@ in front of raw SQL, so using one behind the other isn't really necessary. Why
not have a go at implementing our repository without using the ORM?
You'll find the code https://github.com/cosmicpython/code/tree/chapter_02_repository_exercise[on GitHub].
+前几天我们在一次 DDD 会议上遇到了一位朋友,他说:“我已经有 10 年没用过 ORM 了。”仓库模式和 ORM 都是原始 SQL 的抽象,
+因此在一个抽象后面再使用另一个抽象并不是必须的。为什么不尝试一下不使用 ORM 来实现我们的仓库呢?
+你可以在 https://github.com/cosmicpython/code/tree/chapter_02_repository_exercise[GitHub] 上找到相关代码。
+
We've left the repository tests, but figuring out what SQL to write is up
to you. Perhaps it'll be harder than you think; perhaps it'll be easier.
But the nice thing is, the rest of your application just doesn't care.
+我们保留了仓库的测试,但具体要写哪些 SQL 语句就交给你来决定了。也许这会比你想的更难,也许会更简单。
+但很棒的一点是,你的应用程序的其他部分并不关心这些。
+
******************************************************************************
=== Building a Fake Repository for Tests Is Now Trivial!
+为测试构建一个假的仓库现在变得非常简单!
((("Repository pattern", "building fake repository for tests")))
((("set, fake repository as wrapper around")))
Here's one of the biggest benefits of the Repository pattern:
+以下是仓库模式的最大好处之一:
+
[[fake_repository]]
.A simple fake repository using a set (repository.py)
@@ -847,9 +1042,13 @@ class FakeRepository(AbstractRepository):
Because it's a simple wrapper around a `set`, all the methods are one-liners.
+由于它是对一个 `set` 的简单封装,所有方法都可以用一行代码实现。
+
Using a fake repo in tests is really easy, and we have a simple
abstraction that's easy to use and reason about:
+在测试中使用一个假的仓库非常简单,而且我们有一个易于使用且便于理解的简单抽象:
+
[[fake_repository_example]]
.Example usage of fake repository (test_api.py)
====
@@ -862,14 +1061,18 @@ fake_repo = FakeRepository([batch1, batch2, batch3])
You'll see this fake in action in the next chapter.
+你将在下一章中看到这个假的仓库的实际应用。
+
TIP: Building fakes for your abstractions is an excellent way to get design
feedback: if it's hard to fake, the abstraction is probably too
complicated.
+为你的抽象构建假的实现是获取设计反馈的极好方式:如果难以伪造,那么这个抽象可能过于复杂。
[[what_is_a_port_and_what_is_an_adapter]]
=== What Is a Port and What Is an Adapter, in Python?
+在 _Python_ 中,什么是端口(Port),什么是适配器(Adapter)?
((("ports", "defined")))
((("adapters", "defined")))
@@ -878,11 +1081,17 @@ we want to focus on is dependency inversion, and the specifics of the
technique you use don't matter too much. Also, we're aware that different
people use slightly different definitions.
+我们不想在术语上花费太多精力,因为我们主要关注的是依赖倒置,而你使用的具体技术的细节并不是那么重要。
+同时,我们也清楚,不同的人对这些术语的定义可能会略有不同。
+
Ports and adapters came out of the OO world, and the definition we hold onto
is that the _port_ is the _interface_ between our application and whatever
it is we wish to abstract away, and the _adapter_ is the _implementation_
behind that interface or abstraction.
+端口(Ports)和适配器(Adapters)来源于面向对象(OO)世界,我们所坚持的定义是:**端口**(Port)是我们的应用程序与我们
+希望抽象化的事物之间的**接口**,而**适配器**(Adapter)是该接口或抽象背后的**实现**。
+
((("interfaces, Python and")))
((("duck typing", "for ports")))
((("abstract base classes (ABCs)", "using for ports")))
@@ -892,12 +1101,19 @@ abstract base class, that's the port. If not, the port is just the duck type
that your adapters conform to and that your core application expects—the
function and method names in use, and their argument names and types.
+在 _Python_ 中没有真正意义上的接口,因此尽管通常可以很容易地识别适配器,但定义端口可能会更困难。
+如果你使用的是抽象基类(ABC),那么这就是你的端口。如果没有使用抽象基类,那么端口就是你的适配器遵守的鸭子类型,
+以及你的核心应用程序所期望的类型——也就是实际使用的函数和方法名称,以及它们的参数名称和类型。
+
Concretely, in this chapter, `AbstractRepository` is the port, and
`SqlAlchemyRepository` and `FakeRepository` are the adapters.
+具体来说,在本章中,`AbstractRepository` 是端口,而 `SqlAlchemyRepository` 和 `FakeRepository` 则是适配器。
+
=== Wrap-Up
+总结
((("Repository pattern", "and persistence ignorance, trade-offs")))
((("persistence ignorance", "trade-offs")))
@@ -908,10 +1124,16 @@ to be built this way; only sometimes does the complexity of the app and domain
make it worth investing the time and effort in adding these extra layers of
indirection.
+记住 Rich Hickey 的那句名言,在每一章中,我们都会总结我们引入的每种架构模式的成本和收益。
+我们希望明确一点,我们并不是说每个应用程序都需要以这种方式构建;只有当应用程序和领域的复杂性足够高时,
+才值得投入时间和精力来添加这些额外的间接层。
+
With that in mind, <> shows
some of the pros and cons of the Repository pattern and our persistence-ignorant
model.
+考虑到这一点,<> 展示了仓库模式及我们的持久化无关模型的一些优点和缺点。
+
////
[SG] is it worth mentioning that the repository is specifically intended for add and get
of our domain model objects, rather than something used to add and get any old data
@@ -925,23 +1147,28 @@ which you might call a DAO. Repository is more close to the business domain.
|Pros|Cons
a|
* We have a simple interface between persistent storage and our domain model.
+我们在持久化存储和领域模型之间有一个简单的接口。
* It's easy to make a fake version of the repository for unit testing, or to
swap out different storage solutions, because we've fully decoupled the model
from infrastructure concerns.
+为单元测试制作一个仓库的假版本非常容易,或者更换不同的存储解决方案也很方便,因为我们已经完全将模型与基础设施的关切解耦了。
* Writing the domain model before thinking about persistence helps us focus on
the business problem at hand. If we ever want to radically change our approach,
we can do that in our model, without needing to worry about foreign keys
or migrations until later.
+在考虑持久化之前编写领域模型可以帮助我们专注于手头的业务问题。如果我们想彻底改变我们的解决方法,我们可以在模型中进行,而不需要在初期就为外键或迁移操心。
* Our database schema is really simple because we have complete control over
how we map our objects to tables.
+我们的数据库模式非常简单,因为我们完全可以控制如何将对象映射到表中。
a|
* An ORM already buys you some decoupling. Changing foreign keys might be hard,
but it should be pretty easy to swap between MySQL and Postgres if you
ever need to.
+ORM 已经为你提供了一定程度的解耦。更改外键可能会比较困难,但如果你需要在 MySQL 和 Postgres 之间切换,应该会相对容易一些。
////
[KP] I always found this benefit of ORMs rather weak. In the rare cases when I
@@ -952,10 +1179,12 @@ Postgres fields) you usually lose the portability.
* Maintaining ORM mappings by hand requires extra work and extra code.
+手动维护 ORM 映射需要额外的工作量和代码量。
* Any extra layer of indirection always increases maintenance costs and
adds a "WTF factor" for Python programmers who've never seen the Repository pattern
before.
+任何额外的间接层都会增加维护成本,并对那些从未见过仓库模式的 _Python_ 程序员增加一种“WTF 因素”(困惑感)。
|===
<> shows the basic thesis: yes, for simple
@@ -963,8 +1192,12 @@ cases, a decoupled domain model is harder work than a simple ORM/ActiveRecord
pattern.footnote:[Diagram inspired by a post called
https://oreil.ly/fQXkP["Global Complexity, Local Simplicity"] by Rob Vens.]
+<> 展示了基本的论点:是的,对于简单的情况,一个解耦的领域模型比一个简单的 ORM/ActiveRecord 模式要更费事。
+脚注:[图示灵感来源于 Rob Vens 的一篇名为 https://oreil.ly/fQXkP[《全局复杂性,局部简单性》(Global Complexity, Local Simplicity)] 的文章。]
+
TIP: If your app is just a simple CRUD (create-read-update-delete) wrapper
around a database, then you don't need a domain model or a repository.
+如果你的应用程序只是一个围绕数据库的简单 CRUD(创建-读取-更新-删除)封装,那么你不需要领域模型或仓库。
((("domain model", "trade-offs as a diagram")))
((("Vens, Rob")))
@@ -973,6 +1206,8 @@ But the more complex the domain, the more an investment in freeing
yourself from infrastructure concerns will pay off in terms of the ease of
making changes.
+但领域越复杂,在摆脱基础设施相关问题上的投入就越有回报,因为这会显著提高更改的灵活性和方便性。
+
[[domain_model_tradeoffs_diagram]]
.Domain model trade-offs as a diagram
@@ -988,26 +1223,39 @@ before we could run any tests. As it is, because our model is just plain
old Python objects, we can change a `set()` to being a new attribute, without
needing to think about the database until later.
+我们的示例代码的复杂性不足以完整地展现图表右侧的情况,但其中确实提供了一些提示。例如,想象一下,
+如果有一天我们决定将分配(allocations)从 `Batch` 对象移至 `OrderLine`,在使用 Django 这样的框架时,
+我们必须先定义并仔细考虑数据库迁移的问题,然后才能运行任何测试。而按照我们的方式,因为我们的模型只是一些普通的 _Python_ 对象,
+所以我们可以简单地将一个 `set()` 改为新的属性,而不需要在初期考虑数据库问题。
+
[role="nobreakinside"]
.Repository Pattern Recap
*****************************************************************
-Apply dependency inversion to your ORM::
+Apply dependency inversion to your ORM(对你的 ORM 应用依赖倒置原则)::
Our domain model should be free of infrastructure concerns,
so your ORM should import your model, and not the other way
around.
((("Repository pattern", "recap of important points")))
+我们的领域模型应当与基础设施无关,因此你的 ORM 应该导入模型,而不是模型导入 ORM。
-The Repository pattern is a simple abstraction around permanent storage::
+The Repository pattern is a simple abstraction around permanent storage(仓储模式是一种围绕永久存储的简单抽象。)::
The repository gives you the illusion of a collection of in-memory
objects. It makes it easy to create a `FakeRepository` for
testing and to swap fundamental details of your
infrastructure without disrupting your core application. See
<> for an example.
+仓储为你提供了一种内存对象集合的假象。它使你可以轻松创建一个用于测试的 `FakeRepository`,
+并在不干扰核心应用程序的情况下更换基础设施的关键细节。请参见 <> 获取示例。
*****************************************************************
You'll be wondering, how do we instantiate these repositories, fake or
real? What will our Flask app actually look like? You'll find out in the next
exciting installment, <>.
+你可能会想,我们如何实例化这些仓储,无论是假的还是实际的?我们的 Flask 应用实际上会是什么样子?
+答案将在下一章节 <> 的精彩内容中揭晓。
+
But first, a brief digression.
((("Repository pattern", startref="ix_Repo")))
+
+但首先,让我们稍作旁注。
From 752e72eb10287b521d7806543a816317bbfcd307 Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Thu, 30 Jan 2025 22:51:15 +0800
Subject: [PATCH 07/75] update Readme.md chapter_03_abstractions.asciidoc
---
Readme.md | 4 +-
chapter_03_abstractions.asciidoc | 194 ++++++++++++++++++++++++++++++-
2 files changed, 195 insertions(+), 3 deletions(-)
diff --git a/Readme.md b/Readme.md
index 5a2a0592..07a38c81 100644
--- a/Readme.md
+++ b/Readme.md
@@ -21,8 +21,8 @@ O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布
| [**Part 1 Intro**](part1.asciidoc) | |
| [Chapter 1: Domain Model
第一章:领域模型(已翻译)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 2: Repository
第二章:仓储(已翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(翻译中...)](chapter_03_abstractions.asciidoc) | |
-| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(未翻译)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(已翻译)](chapter_03_abstractions.asciidoc) | |
+| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(翻译中...)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(未翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 6: Unit of Work
第六章:工作单元(未翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 7: Aggregates
第七章:聚合(未翻译)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
diff --git a/chapter_03_abstractions.asciidoc b/chapter_03_abstractions.asciidoc
index b85981fb..e370b329 100644
--- a/chapter_03_abstractions.asciidoc
+++ b/chapter_03_abstractions.asciidoc
@@ -1,5 +1,6 @@
[[chapter_03_abstractions]]
== A Brief Interlude: On Coupling [.keep-together]#and Abstractions#
+短暂的插曲:关于耦合与抽象
((("abstractions", id="ix_abs")))
Allow us a brief digression on the subject of abstractions, dear reader.
@@ -7,12 +8,17 @@ We've talked about _abstractions_ quite a lot. The Repository pattern is an
abstraction over permanent storage, for example. But what makes a good
abstraction? What do we want from abstractions? And how do they relate to testing?
+亲爱的读者,请允许我们对抽象这一主题做一个简短的旁注。我们已经多次提到 _抽象_。例如,仓储模式就是对永久存储的抽象。
+那么,什么才是一个良好的抽象?我们希望从抽象中获得什么?它们又是如何与测试相关的?
+
[TIP]
====
The code for this chapter is in the
chapter_03_abstractions branch https://oreil.ly/k6MmV[on GitHub]:
+本章的代码位于 GitHub 的 chapter_03_abstractions 分支 https://oreil.ly/k6MmV[链接如下]:
+
----
git clone https://github.com/cosmicpython/code.git
git checkout chapter_03_abstractions
@@ -30,6 +36,10 @@ we get to play with ideas freely, hammering things out and refactoring
aggressively. In a large-scale system, though, we become constrained by the
decisions made elsewhere in the system.
+本书的一个核心主题,隐藏在各种花哨的模式中,就是我们可以通过简单的抽象来隐藏杂乱的细节。当我们为乐趣编写代码,或者在进行编程练习(kata)时,
+脚注:[代码 kata 是一种小型、封闭的编程挑战,通常用于练习 TDD。请参考 https://web.archive.org/web/20221024055359/http://www.peterprovost.org/blog/2012/05/02/kata-the-only-way-to-learn-tdd/["Kata—The Only Way to Learn TDD"],作者:Peter Provost。]
+我们可以自由地尝试想法,大胆推敲并积极地进行重构。然而,在一个大型系统中,我们却会受到系统其他部分所做决定的限制。
+
((("coupling")))
((("cohesion, high, between coupled elements")))
When we're unable to change component A for fear of breaking component B, we say
@@ -38,6 +48,9 @@ a sign that our code is working together, each component supporting the others,
fitting in place like the gears of a watch. In jargon, we say this works when
there is high _cohesion_ between the coupled elements.
+当我们因为担心修改组件A会破坏组件B而无法改变组件A时,我们称这些组件变得 _耦合_ 了。在局部范围内,耦合是件好事:它表明我们的代码协同工作,
+每个组件都在支持其他组件,所有组件像手表的齿轮一样完美契合。用术语来说,这种情况在耦合元素之间具有高度 _内聚_ 时是有效的。
+
((("Ball of Mud pattern")))
((("coupling", "disadvantages of")))
Globally, coupling is a nuisance: it increases the risk and the cost of changing
@@ -47,12 +60,18 @@ if we're unable to prevent coupling between elements that have no cohesion, that
coupling increases superlinearly until we are no longer able to effectively
change our systems.
+从全局来看,耦合却是一种麻烦:它增加了修改代码的风险和成本,有时甚至会让我们觉得完全无法做出任何更改。
+这正是“泥球模式”(Ball of Mud pattern)的问题所在:随着应用程序的增长,如果我们无法阻止没有内聚性的元素之间的耦合,
+这种耦合会呈现超线性增长,直到我们再也无法有效地修改系统。
+
((("abstractions", "using to reduce coupling")))
((("coupling", "reducing by abstracting away details")))
We can reduce the degree of coupling within a system
(<>) by abstracting away the details
(<>).
+我们可以通过抽象掉细节(<>)来减少系统中的耦合程度(<>)。
+
[role="width-50"]
[[coupling_illustration1]]
.Lots of coupling
@@ -93,14 +112,21 @@ two; the number of arrows indicates lots of kinds of dependencies
between the two. If we need to change system B, there's a good chance that the
change will ripple through to system A.
+在这两张图中,我们都有一对子系统,其中一个依赖于另一个。在 <> 中,这两个系统之间有高度的耦合;
+箭头的数量表明两者之间存在多种依赖关系。如果我们需要更改系统B,很可能这种更改会波及到系统A。
+
In <>, though, we have reduced the degree of coupling by inserting a
new, simpler abstraction. Because it is simpler, system A has fewer
kinds of dependencies on the abstraction. The abstraction serves to
protect us from change by hiding away the complex details of whatever system B
does—we can change the arrows on the right without changing the ones on the left.
+然而,在 <> 中,我们通过引入一个新的、更简单的抽象来降低耦合程度。由于抽象更简单,系统A对该抽象的依赖种类就更少。
+这个抽象通过隐藏系统B的复杂细节,保护我们免受变更的影响——我们可以更改右边的箭头,而不需要更改左边的箭头。
+
[role="pagebreak-before less_space"]
=== Abstracting State Aids Testability
+抽象状态有助于提高可测试性
((("abstractions", "abstracting state to aid testability", id="ix_absstate")))
((("testing", "abstracting state to aid testability", id="ix_tstabs")))
@@ -109,10 +135,15 @@ does—we can change the arrows on the right without changing the ones on the le
Let's see an example. Imagine we want to write code for synchronizing two
file directories, which we'll call the _source_ and the _destination_:
+让我们来看一个例子。假设我们想编写用于同步两个文件目录的代码,我们将它们分别称为 _源目录_ 和 _目标目录_:
+
* If a file exists in the source but not in the destination, copy the file over.
+如果文件存在于源目录但不存在于目标目录中,则将文件复制过去。
* If a file exists in the source, but it has a different name than in the destination,
rename the destination file to match.
+如果文件存在于源目录中,但在目标目录中的名称不同,则将目标目录中的文件重命名以匹配源目录。
* If a file exists in the destination but not in the source, remove it.
+如果文件存在于目标目录但不存在于源目录中,则将其删除。
((("hashing a file")))
Our first and third requirements are simple enough: we can just compare two
@@ -121,6 +152,9 @@ we'll have to inspect the content of files. For this, we can use a hashing
function like MD5 or SHA-1. The code to generate a SHA-1 hash from a file is simple
enough:
+我们的第一个和第三个需求相对简单:我们只需比较两组路径列表即可。然而,第二个需求就比较棘手了。
+为了检测重命名,我们必须检查文件的内容。为此,我们可以使用诸如 MD5 或 SHA-1 之类的哈希函数。从文件生成一个 SHA-1 哈希的代码相对简单:
+
[[hash_file]]
.Hashing a file (sync.py)
====
@@ -143,12 +177,18 @@ def hash_file(path):
Now we need to write the bit that makes decisions about what to do—the business
logic, if you will.
+现在我们需要编写用于决定如何操作的部分——也就是所谓的业务逻辑。
+
When we have to tackle a problem from first principles, we usually try to write
a simple implementation and then refactor toward better design. We'll use
this approach throughout the book, because it's how we write code in the real
world: start with a solution to the smallest part of the problem, and then
iteratively make the solution richer and better designed.
+当我们从基本原理入手解决问题时,通常会尝试先编写一个简单的实现,然后逐步重构以实现更好的设计。
+我们将在整本书中使用这种方法,因为这也是我们在现实世界中编写代码的方式:从问题中最小的部分开始找到一个解决方案,
+然后通过迭代使解决方案更加完善且设计更优。
+
////
[SG] this may just be my lack of Python experience but it would have helped me to see
from pathlib import Path before this code snippet so that I might be able to guess
@@ -158,6 +198,8 @@ be too much to ask..
Our first hackish approach looks something like this:
+我们第一个有些粗糙的实现看起来像这样:
+
[[sync_first_cut]]
.Basic sync algorithm (sync.py)
====
@@ -206,6 +248,8 @@ def sync(source, dest):
Fantastic! We have some code and it _looks_ OK, but before we run it on our
hard drive, maybe we should test it. How do we go about testing this sort of thing?
+太棒了!我们已经有了一些代码,而且它 _看起来_ 没问题,但在我们运行它操作硬盘之前,也许应该先测试一下。那么,我们该如何测试这类东西呢?
+
[[ugly_sync_tests]]
.Some end-to-end tests (test_sync.py)
@@ -262,17 +306,26 @@ our domain logic, "figure out the difference between two directories," is tightl
coupled to the I/O code. We can't run our difference algorithm without calling
the `pathlib`, `shutil`, and `hashlib` modules.
+哇,这仅仅为了两个简单的用例就要进行这么多的设置!问题在于,我们的领域逻辑“找出两个目录之间的差异”与I/O代码耦合得太紧密了。
+我们无法在不调用 `pathlib`、`shutil` 和 `hashlib` 模块的情况下运行我们的差异算法。
+
And the trouble is, even with our current requirements, we haven't written
enough tests: the current implementation has several bugs (the
`shutil.move()` is wrong, for example). Getting decent coverage and revealing
these bugs means writing more tests, but if they're all as unwieldy as the preceding
ones, that's going to get real painful real quickly.
+问题在于,即使按照我们当前的需求,我们也没有编写足够的测试:当前的实现中存在几个错误(例如,`shutil.move()` 是错误的)。
+为了获得足够的覆盖率并揭示这些问题,我们需要编写更多的测试,但如果每个测试都像前面那样笨重,问题将很快变得非常棘手且痛苦。
+
On top of that, our code isn't very extensible. Imagine trying to implement
a `--dry-run` flag that gets our code to just print out what it's going to
do, rather than actually do it. Or what if we wanted to sync to a remote server,
or to cloud storage?
+除此之外,我们的代码扩展性也很差。想象一下,如果我们尝试实现一个 `--dry-run` 标志,让代码只是打印出它将要执行的操作,
+而不是实际执行操作,该怎么做?又或者,如果我们想要同步到远程服务器或云存储呢?
+
((("abstractions", "abstracting state to aid testability", startref="ix_absstate")))
((("testing", "abstracting state to aid testability", startref="ix_tstabs")))
((("state", "abstracting to aid testability", startref="ix_stateabs")))
@@ -284,25 +337,37 @@ We can definitely refactor these tests (some of the cleanup could go into pytest
fixtures, for example) but as long as we're doing filesystem operations, they're
going to stay slow and be hard to read and write.
+我们的高级代码与低级细节耦合在一起,这让生活变得困难。随着我们考虑的场景变得更加复杂,我们的测试将变得越发笨重。
+我们确实可以重构这些测试(例如,可以将一些清理操作放入 pytest 的 fixture 中),但只要我们继续执行文件系统操作,
+测试仍然会很慢,并且难以阅读和编写。
+
[role="pagebreak-before less_space"]
=== Choosing the Right Abstraction(s)
+选择合适的抽象
((("abstractions", "choosing right abstraction", id="ix_abscho")))
((("filesystems", "writing code to synchronize source and target directories", "choosing right abstraction", id="ix_filesyncabs")))
What could we do to rewrite our code to make it more testable?
+我们可以做些什么来重写代码以使其更具可测试性呢?
+
((("responsibilities of code")))
First, we need to think about what our code needs from the filesystem.
Reading through the code, we can see that three distinct things are happening.
We can think of these as three distinct _responsibilities_ that the code has:
+首先,我们需要思考代码对文件系统的需求。通过阅读代码,我们可以看到发生了三个不同的操作。我们可以将这些视为代码的三项不同 _职责_:
+
1. We interrogate the filesystem by using `os.walk` and determine hashes for a
series of paths. This is similar in both the source and the
destination cases.
+我们通过使用 `os.walk` 查询文件系统,并为一系列路径生成哈希值。这在源目录和目标目录这两种情况下是相似的。
2. We decide whether a file is new, renamed, or redundant.
+我们判断一个文件是新的、被重命名的,还是多余的。
3. We copy, move, or delete files to match the source.
+我们复制、移动或删除文件以使其与源目录匹配。
((("simplifying abstractions")))
@@ -311,10 +376,13 @@ responsibilities. That will let us hide the messy details so we can
focus on the interesting logic.footnote:[If you're used to thinking in terms of
interfaces, that's what we're trying to define here.]
+请记住,我们希望为这些职责中的每一项找到 _简化的抽象_。这将使我们能够隐藏繁琐的细节,从而专注于有趣的逻辑。脚注:[如果你习惯于从接口的角度思考,这正是我们想要在这里定义的内容。]
+
NOTE: In this chapter, we're refactoring some gnarly code into a more testable
structure by identifying the separate tasks that need to be done and giving
each task to a clearly defined actor, along similar lines to <>.
+在本章中,我们通过识别需要完成的独立任务,并将每个任务交给一个明确定义的参与者,来将一些复杂的代码重构为更具可测试性的结构,这与 <> 的方法类似。
((("dictionaries", "for filesystem operations")))
((("hashing a file", "dictionary of hashes to paths")))
@@ -324,17 +392,24 @@ build up a dictionary for the destination folder as well as the source, and
then we just compare two dicts?" That seems like a nice way to abstract the
current state of the filesystem:
+对于步骤 1 和 2,我们已经直观地开始使用一种抽象,即一个从哈希值到路径的字典。你可能已经在想:“为什么不同时为目标文件夹和源文件夹构建一个字典,
+然后简单地比较两个字典呢?”这似乎是一个很好地抽象文件系统当前状态的方法:
+
source_files = {'hash1': 'path1', 'hash2': 'path2'}
dest_files = {'hash1': 'path1', 'hash2': 'pathX'}
What about moving from step 2 to step 3? How can we abstract out the
actual move/copy/delete filesystem interaction?
+那么,从步骤 2 到步骤 3 呢?我们如何抽象化实际的移动/复制/删除文件系统交互呢?
+
((("coupling", "separating what you want to do from how to do it")))
We'll apply a trick here that we'll employ on a grand scale later in
the book. We're going to separate _what_ we want to do from _how_ to do it.
We're going to make our program output a list of commands that look like this:
+我们将在这里运用一个技巧,这个技巧后来将在本书中大规模应用。我们将把 _我们想做什么_ 与 _如何去做_ 分离开来。我们会让程序输出一个命令列表,看起来像这样:
+
("COPY", "sourcepath", "destpath"),
("MOVE", "old", "new"),
@@ -342,10 +417,14 @@ We're going to make our program output a list of commands that look like this:
Now we could write tests that just use two filesystem dicts as inputs, and we would
expect lists of tuples of strings representing actions as outputs.
+现在,我们可以编写测试,使用两个文件系统字典作为输入,并期望得到一个由字符串元组组成的列表作为输出,这些元组代表动作。
+
Instead of saying, "Given this actual filesystem, when I run my function,
check what actions have happened," we say, "Given this _abstraction_ of a filesystem,
what _abstraction_ of filesystem actions will happen?"
+我们不再说:“给定这个实际文件系统,当我运行我的函数时,检查发生了哪些操作。”而是说:“给定这个文件系统的 _抽象_,会发生哪些文件系统操作的 _抽象_?”
+
[[better_tests]]
.Simplified inputs and outputs in our tests (test_sync.py)
@@ -369,6 +448,7 @@ what _abstraction_ of filesystem actions will happen?"
=== Implementing Our Chosen Abstractions
+实现我们选择的抽象
((("abstractions", "implementing chosen abstraction", id="ix_absimpl")))
((("abstractions", "choosing right abstraction", startref="ix_abscho")))
@@ -377,6 +457,8 @@ what _abstraction_ of filesystem actions will happen?"
That's all very well, but how do we _actually_ write those new
tests, and how do we change our implementation to make it all work?
+这都很好,但我们 _实际上_ 要如何编写这些新测试,并且如何更改我们的实现使其全部正常工作呢?
+
((("Functional Core, Imperative Shell (FCIS)")))
((("Bernhardt, Gary")))
((("testing", "after implementing chosen abstraction", id="ix_tstaftabs")))
@@ -388,6 +470,9 @@ by Gary Bernhardt as
https://oreil.ly/wnad4[Functional
Core, Imperative Shell], or FCIS).
+我们的目标是隔离系统中巧妙的部分,并能够彻底地测试它,而无需设置真实的文件系统。我们将创建一个“核心”代码,其不依赖于外部状态,
+然后观察当我们提供来自外部世界的输入时它如何响应(这种方法由 Gary Bernhardt 描述为 https://oreil.ly/wnad4[函数式核心,命令式外壳],简称 FCIS)。
+
((("I/O", "disentangling details from program logic")))
((("state", "splitting off from logic in the program")))
((("business logic", "separating from state in code")))
@@ -397,6 +482,8 @@ the logic.
And our top-level function will contain almost no logic at all; it's just an
imperative series of steps: gather inputs, call our logic, apply outputs:
+让我们从拆分代码开始,将有状态的部分与逻辑部分分离开来。
+
[[three_parts]]
.Split our code into three (sync.py)
====
@@ -422,14 +509,18 @@ def sync(source, dest):
====
<1> Here's the first function we factor out, `read_paths_and_hashes()`, which
isolates the I/O part of our application.
+这里是我们提取的第一个函数 `read_paths_and_hashes()`,它将应用程序的 I/O 部分隔离出来。
<2> Here is where we carve out the functional core, the business logic.
+这里是我们分离出函数式核心和业务逻辑的地方。
((("dictionaries", "dictionary of hashes to paths")))
The code to build up the dictionary of paths and hashes is now trivially easy
to write:
+现在,用于构建路径和哈希字典的代码变得极其简单:
+
[[read_paths_and_hashes]]
.A function that just does I/O (sync.py)
====
@@ -449,6 +540,9 @@ which says, "Given these two sets of hashes and filenames, what should we
copy/move/delete?". It takes simple data structures and returns simple data
structures:
+`determine_actions()` 函数将是我们业务逻辑的核心,它描述了:“给定这两个哈希值和文件名的集合,
+我们应该执行哪些复制/移动/删除操作?” 它接受简单的数据结构并返回简单的数据结构:
+
[[determine_actions]]
.A function that just does business logic (sync.py)
====
@@ -474,6 +568,8 @@ def determine_actions(source_hashes, dest_hashes, source_folder, dest_folder):
Our tests now act directly on the `determine_actions()` function:
+我们的测试现在直接针对 `determine_actions()` 函数进行操作:
+
[[harry_tests]]
.Nicer-looking tests (test_sync.py)
@@ -499,6 +595,8 @@ def test_when_a_file_has_been_renamed_in_the_source():
Because we've disentangled the logic of our program--the code for identifying
changes--from the low-level details of I/O, we can easily test the core of our code.
+因为我们已经将程序的逻辑(用于识别更改的代码)与底层的 I/O 细节解耦,我们可以轻松地测试代码的核心部分。
+
((("edge-to-edge testing", id="ix_edgetst")))
With this approach, we've switched from testing our main entrypoint function,
`sync()`, to testing a lower-level function, `determine_actions()`. You might
@@ -508,8 +606,13 @@ another option, which is to modify the `sync()` function so it can
be unit tested _and_ end-to-end tested; it's an approach Bob calls
_edge-to-edge testing_.
+通过这种方法,我们已从测试主要入口函数 `sync()` 转变为测试更底层的函数 `determine_actions()`。你可能会认为这样不错,
+因为现在 `sync()` 非常简单了。或者,你可能决定保留一些集成/验收测试来测试 `sync()`。但还有另一种选择,就是修改 `sync()` 函数,
+使其既能够进行单元测试 _又_ 能进行端到端测试,这是一种 Bob 称为 _边到边测试_ 的方法。
+
==== Testing Edge to Edge with Fakes and Dependency Injection
+使用伪对象和依赖注入进行边到边测试
((("dependencies", "edge-to-edge testing with dependency injection", id="ix_depinj")))
((("testing", "after implementing chosen abstraction", "edge-to-edge testing with fakes and dependency injection", id="ix_tstaftabsedge")))
@@ -518,11 +621,15 @@ When we start writing a new system, we often focus on the core logic first,
driving it with direct unit tests. At some point, though, we want to test bigger
chunks of the system together.
+当我们开始编写一个新系统时,通常会先专注于核心逻辑,并通过直接的单元测试来驱动它。然而,在某个阶段,我们会希望将系统中的更大块内容一起进行测试。
+
((("faking", "faking I/O in edge-to-edge test")))
We _could_ return to our end-to-end tests, but those are still as tricky to
write and maintain as before. Instead, we often write tests that invoke a whole
system together but fake the I/O, sort of _edge to edge_:
+我们 _可以_ 回到端到端测试,但这些测试依然和以前一样难以编写和维护。相反,我们通常会编写一些测试,这些测试调用整个系统,但伪造了 I/O,有点像 _边到边_ 测试:
+
[[di_version]]
.Explicit dependencies (sync.py)
@@ -552,22 +659,28 @@ def sync(source, dest, filesystem=FileSystem()): #<1>
====
<1> Our top-level function now exposes a new dependency, a `FileSystem`.
+我们的顶层函数现在暴露了一个新依赖项,即 `FileSystem`。
<2> We invoke `filesystem.read()` to produce our files dict.
+我们调用 `filesystem.read()` 来生成我们的文件字典。
<3> We invoke the ++FileSystem++'s `.copy()`, `.move()` and `.delete()` methods
to apply the changes we detect.
+我们调用 ++FileSystem++ 的 `.copy()`、`.move()` 和 `.delete()` 方法来应用我们检测到的更改。
TIP: Although we're using dependency injection, there is no need
to define an abstract base class or any kind of explicit interface. In this
book, we often show ABCs because we hope they help you understand what the
abstraction is, but they're not necessary. Python's dynamic nature means
we can always rely on duck typing.
+虽然我们使用了依赖注入,但没有必要定义抽象基类或任何形式的显式接口。在本书中,我们经常展示抽象基类(ABCs),因为我们希望它们能帮助你理解抽象的概念,但它们并不是必需的。 _Python_ 的动态特性意味着我们始终可以依赖于鸭子类型。
// IDEA [KP] Again, one could mention PEP544 protocols here. For some reason, I like them.
The real (default) implementation of our FileSystem abstraction does real I/O:
+我们 FileSystem 抽象的真实(默认)实现执行真实的 I/O:
+
[[real_filesystem_wrapper]]
.The real dependency (sync.py)
====
@@ -593,6 +706,8 @@ class FileSystem:
But the fake one is a wrapper around our chosen abstractions,
rather than doing real I/O:
+但伪对象是围绕我们选择的抽象的一个包装,而不是执行真实的 I/O:
+
[[fake_filesystem]]
.Tests using DI
====
@@ -620,6 +735,7 @@ class FakeFilesystem:
<1> We initialize our fake filesysem using the abstraction we chose to
represent filesystem state: dictionaries of hashes to paths.
+我们使用我们选择的抽象来表示文件系统状态来初始化我们的伪文件系统:即哈希到路径的字典。
<2> The action methods in our `FakeFileSystem` just appends a record to an list
of `.actions` so we can inspect it later. This means our test double is both
@@ -627,6 +743,7 @@ class FakeFilesystem:
((("test doubles")))
((("fake objects")))
((("spy objects")))
+我们 `FakeFileSystem` 中的操作方法只是将一个记录附加到 `.actions` 的列表中,以便我们稍后检查。这意味着我们的测试替身既是一个“伪对象”,也是一个“间谍”。
So now our tests can act on the real, top-level `sync()` entrypoint,
but they do so using the `FakeFilesystem()`. In terms of their
@@ -634,6 +751,9 @@ setup and assertions, they end up looking quite similar to the ones
we wrote when testing directly against the functional core `determine_actions()`
function:
+现在我们的测试可以作用于真实的顶层入口点 `sync()`,但它们使用的是 `FakeFilesystem()`。从设置和断言的角度来看,
+它们最终看起来与我们直接针对函数式核心 `determine_actions()` 函数编写的测试非常相似:
+
[[bob_tests]]
.Tests using DI
@@ -667,6 +787,9 @@ our stateful components explicit and pass them around.
David Heinemeier Hansson, the creator of Ruby on Rails, famously described this
as "test-induced design damage."
+这种方法的优点是我们的测试作用于生产代码中使用的完全相同的函数。缺点是我们必须使有状态的组件显式化并在代码中传递它们。
+Ruby on Rails 的创建者 David Heinemeier Hansson 曾著名地将此描述为“测试引发的设计损伤”。
+
((("edge-to-edge testing", startref="ix_edgetst")))
((("testing", "after implementing chosen abstraction", "edge-to-edge testing with fakes and dependency injection", startref="ix_tstaftabsedge")))
((("dependencies", "edge-to-edge testing with dependency injection", startref="ix_depinj")))
@@ -674,8 +797,11 @@ as "test-induced design damage."
In either case, we can now work on fixing all the bugs in our implementation;
enumerating tests for all the edge cases is now much easier.
+无论哪种情况,我们现在都可以专注于修复实现中的所有错误;为所有边界情况列举测试现在变得更加容易。
+
==== Why Not Just Patch It Out?
+为什么不直接用补丁来解决?
((("mock.patch method")))
((("mocking", "avoiding use of mock.patch")))
@@ -684,38 +810,53 @@ enumerating tests for all the edge cases is now much easier.
At this point you may be scratching your head and thinking,
"Why don't you just use `mock.patch` and save yourself the effort?"
+此时,你可能会挠头思考:“为什么不直接使用 `mock.patch` 来省事呢?”
+
We avoid using mocks in this book and in our production code too. We're not
going to enter into a Holy War, but our instinct is that mocking frameworks,
particularly monkeypatching, are a code smell.
+在本书以及我们的生产代码中,我们避免使用 Mock。我们不想引发一场“圣战”,但我们的直觉是,Mock 框架,尤其是猴子补丁(monkeypatching),是一种代码坏味道。
+
Instead, we like to clearly identify the responsibilities in our codebase, and to
separate those responsibilities into small, focused objects that are easy to
replace with a test double.
+相反,我们更倾向于清晰地识别代码库中的职责,并将这些职责分离成小而专注的对象,这些对象容易被测试替身替代。
+
NOTE: You can see an example in <>,
where we `mock.patch()` out an email-sending module, but eventually we
replace that with an explicit bit of dependency injection in
<>.
+你可以在 <> 中看到一个示例,我们使用 `mock.patch()` 替换了一个发送电子邮件的模块,但最终我们在 <> 中用依赖注入的明确实现替代了它。
We have three closely related reasons for our preference:
+我们对这种偏好的原因有三个密切相关的方面:
+
* Patching out the dependency you're using makes it possible to unit test the
code, but it does nothing to improve the design. Using `mock.patch` won't let your
code work with a `--dry-run` flag, nor will it help you run against an FTP
server. For that, you'll need to introduce abstractions.
+通过补丁替换掉你所使用的依赖,可以让代码进行单元测试,但对改进设计毫无帮助。
+使用 `mock.patch` 不会让你的代码支持一个 `--dry-run` 标志,也不会帮助你运行在一个 FTP 服务器上。要做到这些,你需要引入抽象。
* Tests that use mocks _tend_ to be more coupled to the implementation details
of the codebase. That's because mock tests verify the interactions between
things: did we call `shutil.copy` with the right arguments? This coupling between
code and test _tends_ to make tests more brittle, in our experience.
((("coupling", "in tests that use mocks")))
+使用 Mock 的测试 _往往_ 更加耦合于代码库的实现细节。这是因为 Mock 测试验证的是各部分之间的交互:我们是否以正确的参数调用了 `shutil.copy`?
+根据我们的经验,这种代码与测试之间的耦合 _往往_ 会使测试更脆弱。
* Overuse of mocks leads to complicated test suites that fail to explain the
code.
+过度使用 Mock 会导致测试套件变得复杂,并且无法很好地解释代码。
NOTE: Designing for testability really means designing for
extensibility. We trade off a little more complexity for a cleaner design
that admits novel use cases.
+为测试性而设计实际上意味着为可扩展性而设计。我们用稍微多一些的复杂性换取更简洁的设计,从而能够支持新的用例。
[role="nobreakinside less_space"]
.Mocks Versus Fakes; Classic-Style Versus London-School TDD
@@ -727,15 +868,20 @@ NOTE: Designing for testability really means designing for
Here's a short and somewhat simplistic definition of the difference between
mocks and fakes:
+这里有一个简短且稍显简单的关于 Mock 和 Fake 区别的定义:
+
* Mocks are used to verify _how_ something gets used; they have methods
like `assert_called_once_with()`. They're associated with London-school
TDD.
+Mocks 用于验证某件事情 _如何_ 被使用;它们有像 `assert_called_once_with()` 这样的方法。它们通常与伦敦学派的 TDD(测试驱动开发)相关联。
* Fakes are working implementations of the thing they're replacing, but
they're designed for use only in tests. They wouldn't work "in real life";
our in-memory repository is a good example. But you can use them to make assertions about
the end state of a system rather than the behaviors along the way, so
they're associated with classic-style TDD.
+Fakes 是被替代对象的工作实现,但它们仅用于测试中。它们在“现实生活”中无法正常工作;我们的内存中存储库就是一个很好的例子。
+但你可以用它们对系统的最终状态进行断言,而不是对过程中发生的行为进行断言,因此它们通常与经典风格的 TDD(测试驱动开发)相关联。
((("Fowler, Martin")))
((("stubbing, mocks and stubs")))
@@ -744,6 +890,9 @@ We're slightly conflating mocks with spies and fakes with stubs here, and you
can read the long, correct answer in Martin Fowler's classic essay on the subject
called https://oreil.ly/yYjBN["Mocks Aren't Stubs"].
+这里我们有些将 Mocks 与 Spies 以及 Fakes 与 Stubs 混为一谈了。你可以阅读 Martin Fowler 关于这一主题的
+经典文章 https://oreil.ly/yYjBN["Mocks Aren't Stubs"] 来了解更长、更准确的答案。
+
((("MagicMock objects")))
((("unittest.mock function")))
((("test doubles", "mocks versus stubs")))
@@ -752,6 +901,9 @@ It also probably doesn't help that the `MagicMock` objects provided by
But they're also often used as stubs or dummies. There, we promise we're done with
the test double terminology nitpicks now.
+`unittest.mock` 提供的 `MagicMock` 对象,严格来说,并不是 Mocks;如果非要定义的话,它们更像是 Spies。
+但它们也经常被用作 Stubs 或 Dummies。好了,我们保证现在已经结束了对测试替身术语的这些吹毛求疵。
+
//IDEA (hynek) you could mention Alex Gaynor's `pretend` which gives you
// stubs without mocks error-prone magic.
@@ -768,17 +920,28 @@ checks on the behavior of intermediary collaborators.footnote:[Which is not to
say that we think the London school people are wrong. Some insanely smart
people work that way. It's just not what we're used to.]
+那么伦敦学派和经典风格的 TDD 之间呢?你可以在我们刚提到的 Martin Fowler 的文章中,
+以及 https://oreil.ly/H2im_[Software Engineering Stack Exchange 网站] 上,阅读更多关于这两种方法的信息。但在本书中,
+我们相当坚定地站在经典派这一边。我们喜欢将测试围绕状态进行设计,无论是在设置还是断言中,并且我们喜欢在尽可能高的抽象层次上工作,
+而不是检查中间协作对象的行为。注释:[这并不是说我们认为伦敦派的人是错误的。一些非常聪明的人是以这种方式工作的。这只是我们不太习惯的方式而已。]
+
Read more on this in <>.
+
+在 <> 中阅读更多相关内容。
*******************************************************************************
We view TDD as a design practice first and a testing practice second. The tests
act as a record of our design choices and serve to explain the system to us
when we return to the code after a long absence.
+我们将 TDD 首先视为一种设计实践,其次才是测试实践。这些测试记录了我们的设计选择,并在我们长时间后重新回到代码时,帮助我们理解系统。
+
((("mocking", "overmocked tests, pitfalls of")))
Tests that use too many mocks get overwhelmed with setup code that hides the
story we care about.
+使用过多 Mock 的测试会被大量的设置代码淹没,从而掩盖了我们真正关心的核心内容。
+
(((""Test-Driven Development: That's Not What We Meant"", primary-sortas="Test-Driven Development")))
((("Freeman, Steve")))
((("PyCon talk on Mocking Pitfalls")))
@@ -789,6 +952,9 @@ You should also check out this PyCon talk, https://oreil.ly/s3e05["Mocking and P
by our esteemed tech reviewer, Ed Jung, which also addresses mocking and its
alternatives.
+Steve Freeman 在他的演讲 https://oreil.ly/jAmtr["Test-Driven Development"] 中展示了一个关于过度 Mock 的精彩示例。
+你还可以看看我们敬爱的技术审稿人 Ed Jung 在 PyCon 上的演讲 https://oreil.ly/s3e05["Mocking and Patching Pitfalls"],其中同样讨论了 Mock 及其替代方案。
+
And while we're recommending talks, check out the wonderful Brandon Rhodes
in https://oreil.ly/oiXJM["Hoisting Your I/O"]. It's not actually about mocks,
but is instead about the general issue of decoupling business logic from I/O,
@@ -796,6 +962,9 @@ in which he uses a wonderfully simple illustrative example.
((("hoisting I/O")))
((("Rhodes, Brandon")))
+同时,既然我们在推荐演讲,也强烈推荐你观看 Brandon Rhodes 的精彩演讲:https://oreil.ly/oiXJM["Hoisting Your I/O"]。
+这其实并非关于 Mock,而是关于将业务逻辑与 I/O 解耦的一般性问题,他在演讲中使用了一个极其简单的示例来进行说明。
+
TIP: In this chapter, we've spent a lot of time replacing end-to-end tests with
unit tests. That doesn't mean we think you should never use E2E tests!
@@ -805,9 +974,12 @@ TIP: In this chapter, we've spent a lot of time replacing end-to-end tests with
for more details.
((("unit testing", "unit tests replacing end-to-end tests")))
((("end-to-end tests", "replacement with unit tests")))
+在本章中,我们花了很多时间用单元测试替换端到端(E2E)测试。但这并不意味着我们认为你永远不应该使用 E2E 测试!
+我们在本书中展示的技术旨在帮助你构建一个合理的测试金字塔,其中尽可能多地包含单元测试,并仅使用最少数量的 E2E 测试以让你感到自信。
+阅读 <> 获取更多详细信息。
-.So Which Do We Use In This Book? Functional or Object-Oriented Composition?
+.So Which Do We Use In This Book? Functional or Object-Oriented Composition?(那么在本书中我们使用哪种方法?函数式还是面向对象的组合?)
******************************************************************************
((("object-oriented composition")))
Both. Our domain model is entirely free of dependencies and side effects,
@@ -816,11 +988,18 @@ so that's our functional core. The service layer that we build around it
and we use dependency injection to provide those services with stateful
components, so we can still unit test them.
+两者兼用。我们的领域模型完全没有依赖和副作用,这就是我们的函数式核心。
+在其周围构建的服务层(见 <>)允许我们以边到边的方式驱动系统,
+并通过依赖注入为这些服务提供有状态的组件,因此我们仍然可以对它们进行单元测试。
+
See <> for more exploration of making our
dependency injection more explicit and centralized.
+
+请参阅 <>,了解更多关于如何使我们的依赖注入更加显式和集中的探索。
******************************************************************************
=== Wrap-Up
+总结
((("abstractions", "implementing chosen abstraction", startref="ix_absimpl")))
((("abstractions", "simplifying interface between business logic and I/O")))
@@ -834,24 +1013,37 @@ systems easier to test and maintain by simplifying the interface between our
business logic and messy I/O. Finding the right abstraction is tricky, but here are
a few heuristics and questions to ask yourself:
+我们会在本书中一再看到这个理念:通过简化业务逻辑和混乱的 I/O 之间的接口,我们可以让系统更容易测试和维护。
+找到合适的抽象是一个难点,但以下是一些启发和可以问自己的问题:
+
* Can I choose a familiar Python data structure to represent the state of the
messy system and then try to imagine a single function that can return that
state?
+我能选择一个熟悉的 _Python_ 数据结构来表示这个混乱系统的状态,然后尝试设想一个可以返回该状态的单一函数吗?
* Separate the _what_ from the _how_:
can I use a data structure or DSL to represent the external effects I want to happen,
independently of _how_ I plan to make them happen?
+将 _what_ 与 _how_ 分离:
+我能否使用一个数据结构或领域专用语言(DSL)来表示我想要发生的外部效果,而与我计划如何实现它们的方式无关?
* Where can I draw a line between my systems,
where can I carve out a https://oreil.ly/zNUGG[seam]
to stick that abstraction in?
((("seams")))
+我可以在哪些地方为我的系统划分界限,
+我可以在哪里开辟一个 https://oreil.ly/zNUGG[接口] 来插入那个抽象?
* What is a sensible way of dividing things into components with different responsibilities?
What implicit concepts can I make explicit?
+将事物划分为具有不同职责的组件,什么样的方式是合理的?
+我可以将哪些隐含的概念显式化?
* What are the dependencies, and what is the core business logic?
+哪些是依赖项,哪些是核心业务逻辑?
((("abstractions", startref="ix_abs")))
Practice makes less imperfect! And now back to our regular programming...
+
+熟能生巧!现在让我们回到正常的编程内容中……
From b803e7eed76368c2f79377883d2eb1da6b266d0b Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Thu, 30 Jan 2025 23:26:27 +0800
Subject: [PATCH 08/75] update Readme.md chapter_04_service_layer.asciidoc
---
Readme.md | 4 +-
chapter_04_service_layer.asciidoc | 213 +++++++++++++++++++++++++++++-
2 files changed, 208 insertions(+), 9 deletions(-)
diff --git a/Readme.md b/Readme.md
index 07a38c81..71c2f6f7 100644
--- a/Readme.md
+++ b/Readme.md
@@ -22,8 +22,8 @@ O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布
| [Chapter 1: Domain Model
第一章:领域模型(已翻译)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 2: Repository
第二章:仓储(已翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(已翻译)](chapter_03_abstractions.asciidoc) | |
-| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(翻译中...)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(未翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(已翻译)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(翻译中...)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 6: Unit of Work
第六章:工作单元(未翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 7: Aggregates
第七章:聚合(未翻译)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [**Part 2 Intro**](part2.asciidoc) | |
diff --git a/chapter_04_service_layer.asciidoc b/chapter_04_service_layer.asciidoc
index 83bf6a57..850acf26 100644
--- a/chapter_04_service_layer.asciidoc
+++ b/chapter_04_service_layer.asciidoc
@@ -1,10 +1,14 @@
[[chapter_04_service_layer]]
== Our First Use Case: [.keep-together]#Flask API and Service Layer#
+我们的第一个用例:Flask API 和服务层
((("service layer", id="ix_serlay")))
((("Flask framework", "Flask API and service layer", id="ix_Flskapp")))
Back to our allocations project! <> shows the point we reached at the end of <>, which covered the Repository pattern.
+回到我们的分配项目!<> 展示了我们在 <> 结束时所达到的阶段,
+该章节讲述了仓库模式(Repository pattern)。
+
[role="width-75"]
[[maps_service_layer_before]]
.Before: we drive our app by talking to repositories and the domain model
@@ -16,17 +20,25 @@ business logic, and interfacing code, and we introduce the _Service Layer_
pattern to take care of orchestrating our workflows and defining the use
cases of our system.
+在本章中,我们将讨论编排逻辑、业务逻辑和接口代码之间的区别,并引入 _服务层_ 模式来负责编排我们的工作流程以及定义系统的用例。
+
We'll also discuss testing: by combining the Service Layer with our repository
abstraction over the database, we're able to write fast tests, not just of
our domain model but of the entire workflow for a use case.
+我们还将讨论测试:通过将服务层与数据库的仓库抽象结合起来,我们不仅可以为领域模型编写快速测试,还可以为用例的整个工作流程编写快速测试。
+
<> shows what we're aiming for: we're going to
add a Flask API that will talk to the service layer, which will serve as the
entrypoint to our domain model. Because our service layer depends on the
`AbstractRepository`, we can unit test it by using `FakeRepository` but run our production code using `SqlAlchemyRepository`.
+<> 展示了我们的目标:我们将添加一个与服务层对接的 Flask API,它将作为进入领域模型的入口。
+由于服务层依赖于 `AbstractRepository`,我们可以通过使用 `FakeRepository` 对其进行单元测试,
+但在生产代码中使用 `SqlAlchemyRepository` 来运行。
+
[[maps_service_layer_after]]
-.The service layer will become the main way into our app
+.The service layer will become the main way into our app(服务层将成为进入我们应用程序的主要方式)
image::images/apwp_0402.png[]
// IDEA more detailed legend
@@ -35,11 +47,16 @@ In our diagrams, we are using the convention that new components
are highlighted with bold text/lines (and yellow/orange color, if you're
reading a digital version).
+在我们的图表中,我们采用的约定是用加粗的文本/线条(如果你阅读的是数字版,还会使用黄色/橙色的颜色)来突出新的组件。
+
[TIP]
====
The code for this chapter is in the
chapter_04_service_layer branch https://oreil.ly/TBRuy[on GitHub]:
+本章的代码位于
+chapter_04_service_layer 分支,链接:https://oreil.ly/TBRuy[在 GitHub 上]:
+
----
git clone https://github.com/cosmicpython/code.git
cd code
@@ -51,6 +68,7 @@ git checkout chapter_02_repository
=== Connecting Our Application to the Real World
+将我们的应用程序连接到现实世界
((("service layer", "connecting our application to real world")))
((("Flask framework", "Flask API and service layer", "connecting the app to real world")))
@@ -59,27 +77,39 @@ in front of the users to start gathering feedback. We have the core
of our domain model and the domain service we need to allocate orders,
and we have the repository interface for permanent storage.
+像任何优秀的敏捷团队一样,我们正在努力推出一个最小可行产品(MVP),并将其呈现在用户面前以开始收集反馈。
+我们已经拥有了分配订单所需的领域模型核心和领域服务,并且还有用于持久存储的仓库接口。
+
Let's plug all the moving parts together as quickly as we
can and then refactor toward a cleaner architecture. Here's our
plan:
+让我们尽快将所有活动部件连接起来,然后再通过重构实现更清晰的架构。以下是我们的计划:
+
1. Use Flask to put an API endpoint in front of our `allocate` domain service.
Wire up the database session and our repository. Test it with
an end-to-end test and some quick-and-dirty SQL to prepare test
data.
((("Flask framework", "putting API endpoint in front of allocate domain service")))
+使用 Flask 在我们的 `allocate` 领域服务前添加一个 API 端点。
+连接数据库会话和我们的仓库。通过端到端测试以及一些快速但简陋的 SQL 来准备测试数据进行测试。
2. Refactor out a service layer that can serve as an abstraction to
capture the use case and that will sit between Flask and our domain model.
Build some service-layer tests and show how they can use
`FakeRepository`.
+重构出一个服务层,作为抽象捕获用例,并位于 Flask 和我们的领域模型之间。
+编写一些服务层的测试,并展示如何使用 `FakeRepository` 来进行测试。
3. Experiment with different types of parameters for our service layer
functions; show that using primitive data types allows the service layer's
clients (our tests and our Flask API) to be decoupled from the model layer.
+尝试为我们的服务层函数使用不同类型的参数;
+展示使用原始数据类型如何使服务层的客户端(我们的测试和 Flask API)与模型层解耦。
=== A First End-to-End Test
+第一个端到端测试
((("APIs", "end-to-end test of allocate API")))
((("end-to-end tests", "of allocate API")))
@@ -90,12 +120,20 @@ an integration test versus a unit test. Different projects need different
combinations of tests, and we've seen perfectly successful projects just split
things into "fast tests" and "slow tests."
+没有人愿意陷入一场关于端到端(E2E)测试、功能测试、验收测试、集成测试与单元测试之间定义的漫长术语争论。不同的项目需要不同组合的测试,
+我们也见过一些非常成功的项目,仅仅将测试分为“快速测试”和“慢速测试”。
+
For now, we want to write one or maybe two tests that are going to exercise
a "real" API endpoint (using HTTP) and talk to a real database. Let's call
them _end-to-end tests_ because it's one of the most self-explanatory names.
+目前,我们希望编写一到两个测试,这些测试将用于运行一个“真实”的 API 端点(使用 HTTP)并与真实的数据库进行交互。
+我们将其称为 _端到端测试_,因为这是最直观易懂的名称之一。
+
The following shows a first cut:
+以下是初步的实现:
+
[[first_api_test]]
.A first API test (test_api.py)
====
@@ -129,12 +167,16 @@ def test_api_returns_allocation(add_stock):
generate randomized characters by using the `uuid` module. Because
we're running against an actual database now, this is one way to prevent
various tests and runs from interfering with each other.
+`random_sku()`、`random_batchref()` 等是一些辅助函数,它们使用 `uuid` 模块生成随机字符。
+因为我们现在正在运行实际的数据库,这是防止不同测试和运行相互干扰的一种方法。
<2> `add_stock` is a helper fixture that just hides away the details of
manually inserting rows into the database using SQL. We'll show a nicer
way of doing this later in the chapter.
+`add_stock` 是一个辅助的 fixture,它只是隐藏了通过 SQL 手动向数据库插入行的细节。稍后在本章中,我们会展示一种更优雅的实现方式。
<3> _config.py_ is a module in which we keep configuration information.
+_config.py_ 是一个用于存放配置信息的模块。
((("Flask framework", "Flask API and service layer", "first API end-to-end test", startref="ix_Flskappe2e")))
Everyone solves these problems in different ways, but you're going to need some
@@ -142,13 +184,19 @@ way of spinning up Flask, possibly in a container, and of talking to a
Postgres database. If you want to see how we did it, check out
<>.
+每个人都会以不同的方式解决这些问题,但你需要某种方法来启动 Flask(可能是在一个容器中),并与一个 Postgres 数据库进行交互。
+如果你想了解我们是如何实现的,可以参考 <>。
+
=== The Straightforward Implementation
+直接的实现方案
((("service layer", "first cut of Flask app", id="ix_serlay1Flapp")))
((("Flask framework", "Flask API and service layer", "first cut of the app", id="ix_Flskapp1st")))
Implementing things in the most obvious way, you might get something like this:
+按照最直接的方式实现,你可能会得到如下代码:
+
[[first_cut_flask_app]]
.First cut of Flask app (flask_app.py)
@@ -188,6 +236,8 @@ def allocate_endpoint():
So far, so good. No need for too much more of your "architecture astronaut"
nonsense, Bob and Harry, you may be thinking.
+到目前为止,一切都很好。你可能会想:“不需要再弄那些所谓的‘架构宇航员’的无谓繁琐了吧,Bob 和 Harry。”
+
((("databases", "testing allocations persisted to database")))
But hang on a minute--there's no commit. We're not actually saving our
allocation to the database. Now we need a second test, either one that will
@@ -195,6 +245,9 @@ inspect the database state after (not very black-boxy), or maybe one that
checks that we can't allocate a second line if a first should have already
depleted the batch:
+但是且慢——这里没有提交操作。我们实际上并没有将分配保存到数据库中。现在我们需要第二个测试,可以是一个检查数据库状态的测试(不是很“黑盒”),
+或者可能是一个测试,验证如果一个批次已经被完全分配,我们不能再为第二个订单行进行分配:
+
[[second_api_test]]
.Test allocations are persisted (test_api.py)
====
@@ -229,22 +282,32 @@ def test_allocations_are_persisted(add_stock):
((("service layer", "first cut of Flask app", startref="ix_serlay1Flapp")))
Not quite so lovely, but that will force us to add the commit.
+虽然不太优雅,但这将迫使我们添加提交操作。
+
=== Error Conditions That Require Database Checks
+需要通过数据库检查的错误情况
((("service layer", "error conditions requiring database checks in Flask app")))
((("Flask framework", "Flask API and service layer", "error conditions requiring database checks")))
If we keep going like this, though, things are going to get uglier and uglier.
+不过,如果我们继续这样下去,事情会变得越来越丑陋。
+
Suppose we want to add a bit of error handling. What if the domain raises an
error, for a SKU that's out of stock? Or what about a SKU that doesn't even
exist? That's not something the domain even knows about, nor should it. It's
more of a sanity check that we should implement at the database layer, before
we even invoke the domain service.
+假设我们想添加一些错误处理。如果域层抛出一个错误,比如某个 SKU 超出库存怎么办?又或者某个 SKU 根本不存在呢?
+这些都不是域层应当知道的事情,也不需要知道。这更像是一种合理性检查,我们应该在调用域服务之前,在数据库层实现它。
+
Now we're looking at two more end-to-end tests:
+现在我们需要再实现两个端到端测试:
+
[[test_error_cases]]
.Yet more tests at the E2E layer (test_api.py)
@@ -277,13 +340,18 @@ def test_400_message_for_invalid_sku(): #<2>
====
<1> In the first test, we're trying to allocate more units than we have in stock.
+在第一个测试中,我们尝试分配超过库存数量的单位。
<2> In the second, the SKU just doesn't exist (because we never called `add_stock`),
so it's invalid as far as our app is concerned.
+在第二个测试中,SKU 根本不存在(因为我们从未调用过 `add_stock`),
+因此对我们的应用程序来说,这是无效的。
And sure, we could implement it in the Flask app too:
+当然,我们也可以在 Flask 应用中实现它:
+
[[flask_error_handling]]
.Flask app starting to get crufty (flask_app.py)
====
@@ -319,8 +387,12 @@ But our Flask app is starting to look a bit unwieldy. And our number of
E2E tests is starting to get out of control, and soon we'll end up with an
inverted test pyramid (or "ice-cream cone model," as Bob likes to call it).
+但是我们的 Flask 应用开始显得有点笨重了。而且我们的端到端(E2E)测试数量也开始失控,
+很快我们就会陷入测试金字塔倒置的情况(或者像 Bob 喜欢称呼的那样,是“冰淇淋蛋筒模型”)。
+
=== Introducing a Service Layer, and Using FakeRepository to Unit Test It
+引入服务层,并使用 FakeRepository 对其进行单元测试
((("service layer", "introducing and using FakeRepository to unit test it", id="ix_serlayintr")))
((("orchestration")))
@@ -333,14 +405,22 @@ web API endpoint (you'd need them if you were building a CLI, for example; see
<>), and they're not really things that need to be tested by
end-to-end tests.
+如果我们查看 Flask 应用正在做的事情,会发现其中相当一部分可以称为“**编排**”——从仓库中获取数据、根据数据库状态验证输入、处理错误以及在正常流程中提交。
+这些事情大多与是否有一个 Web API 端点无关(例如,如果你在构建一个 CLI,这些操作也是必需的;参见 <>),
+而且它们并不是真的需要通过端到端测试来进行验证的内容。
+
((("orchestration layer", see="service layer")))
((("use-case layer", see="service layer")))
It often makes sense to split out a service layer, sometimes called an
_orchestration layer_ or a _use-case layer_.
+通常,将服务层拆分出来是有意义的,它有时也被称为“_编排层_”或“_用例层_”。
+
((("faking", "FakeRepository")))
Do you remember the `FakeRepository` that we prepared in <>?
+你还记得我们在 <> 中准备的 `FakeRepository` 吗?
+
[[fake_repo]]
.Our fake repository, an in-memory collection of batches (test_services.py)
====
@@ -367,6 +447,8 @@ class FakeRepository(repository.AbstractRepository):
Here's where it will come in useful; it lets us test our service layer with
nice, fast unit tests:
+这里就是它派上用场的地方了;它使我们能够通过简洁且快速的单元测试来测试我们的服务层:
+
[[first_services_tests]]
.Unit testing with fakes at the service layer (test_services.py)
@@ -395,6 +477,7 @@ def test_error_for_invalid_sku():
<1> `FakeRepository` holds the `Batch` objects that will be used by our test.
+`FakeRepository` 保存了测试中将要使用的 `Batch` 对象。
<2> Our services module (_services.py_) will define an `allocate()`
service-layer function. It will sit between our `allocate_endpoint()`
@@ -402,11 +485,15 @@ def test_error_for_invalid_sku():
our domain model.footnote:[Service-layer services and domain services do have
confusingly similar names. We tackle this topic later in
<>.]
+我们的服务模块(_services.py_)将定义一个 `allocate()` 服务层函数。
+它位于 API 层的 `allocate_endpoint()` 函数与领域模型中 `allocate()` 领域服务函数之间。
+注释:[服务层的服务和领域服务确实有令人困惑的相似名字。我们将在 <> 中探讨这一主题。]
<3> We also need a `FakeSession` to fake out the database session, as shown in
the following code snippet.
((("faking", "FakeSession, using to unit test the service layer")))
((("testing", "fake database session at service layer")))
+我们还需要一个 `FakeSession` 来模拟数据库会话,如下面的代码片段所示。
[[fake_session]]
@@ -426,6 +513,9 @@ This fake session is only a temporary solution. We'll get rid of it and make
things even nicer soon, in <>. But in the meantime
the fake `.commit()` lets us migrate a third test from the E2E layer:
+这个假的 session 只是一个临时的解决方案。我们很快会在 <> 中将其移除,并使事情变得更加优雅。
+但与此同时,假的 `.commit()` 让我们能够从端到端(E2E)层迁移第三个测试:
+
[[second_services_test]]
.A second test at the service layer (test_services.py)
@@ -446,6 +536,7 @@ def test_commits():
==== A Typical Service Function
+一个典型的服务函数
((("functions", "service layer")))
((("service layer", "typical service function")))
@@ -453,6 +544,8 @@ def test_commits():
((("Flask framework", "Flask API and service layer", "introducing service layer and fake repo to unit test it", startref="ix_Flskappserly")))
We'll write a service function that looks something like this:
+我们将编写一个类似如下的服务函数:
+
[[service_function]]
.Basic allocation service (services.py)
====
@@ -479,25 +572,36 @@ def allocate(line: OrderLine, repo: AbstractRepository, session) -> str:
Typical service-layer functions have similar steps:
+典型的服务层函数具有类似的步骤:
+
<1> We fetch some objects from the repository.
+我们从仓库中获取一些对象。
<2> We make some checks or assertions about the request against
the current state of the world.
+我们根据当前的系统状态对请求进行一些检查或断言。
<3> We call a domain service.
+我们调用一个领域服务。
<4> If all is well, we save/update any state we've changed.
+如果一切正常,我们会保存/更新我们更改的任何状态。
That last step is a little unsatisfactory at the moment, as our service
layer is tightly coupled to our database layer. We'll improve
that in <> with the Unit of Work pattern.
+最后一步目前有点不太令人满意,因为我们的服务层与数据库层紧密耦合。
+我们将在 <> 中使用工作单元(Unit of Work)模式对此进行改进。
+
[role="nobreakinside less_space"]
[[depend_on_abstractions]]
-.Depend on Abstractions
+.Depend on Abstractions(依赖抽象)
*******************************************************************************
Notice one more thing about our service-layer function:
+注意我们服务层函数的另一个特点:
+
[source,python]
[role="skip"]
----
@@ -511,6 +615,9 @@ and we've used the type hint to say that we depend on `AbstractRepository`.
This means it'll work both when the tests give it a `FakeRepository` and
when the Flask app gives it a `SqlAlchemyRepository`.
+它依赖于一个仓库(repository)。我们选择将这种依赖显式化,并使用类型提示来表明我们依赖于 `AbstractRepository`。
+这意味着无论测试传入的是 `FakeRepository`,还是 Flask 应用传入的是 `SqlAlchemyRepository`,它都能正常工作。
+
((("dependencies", "depending on abstractions")))
If you remember <>,
this is what we mean when we say we should "depend on abstractions." Our
@@ -520,10 +627,15 @@ storage also depend on that same abstraction. See
<> and
<>.
+如果你还记得 <>,这就是当我们说“应该依赖抽象”时的意思。我们的 _高层模块_ ——服务层,依赖于仓库(repository)的抽象。
+而具体的持久化存储实现的 _细节_ 也依赖于同样的抽象。请参见 <> 和 <>。
+
See also in <> a worked example of swapping out the
_details_ of which persistent storage system to use while leaving the
abstractions intact.
+另请参见 <> 中的一个示例,展示了在保持抽象不变的情况下更换所使用的持久化存储系统 _细节_ 的操作实例。
+
*******************************************************************************
@@ -532,6 +644,8 @@ abstractions intact.
But the essentials of the service layer are there, and our Flask
app now looks a lot cleaner:
+但是服务层的核心已经存在了,并且我们的 Flask 应用现在看起来干净了许多:
+
[[flask_app_using_service_layer]]
.Flask app delegating to service layer (flask_app.py)
@@ -557,20 +671,28 @@ def allocate_endpoint():
====
<1> We instantiate a database session and some repository objects.
+我们实例化一个数据库会话和一些仓库对象。
<2> We extract the user's commands from the web request and pass them
to the service layer.
+我们从网页请求中提取用户的命令并将其传递给服务层。
<3> We return some JSON responses with the appropriate status codes.
+我们返回一些带有适当状态代码的 JSON 响应。
The responsibilities of the Flask app are just standard web stuff: per-request
session management, parsing information out of POST parameters, response status
codes, and JSON. All the orchestration logic is in the use case/service layer,
and the domain logic stays in the domain.
+Flask 应用的职责只是标准的网络相关工作:每个请求的会话管理、从 POST 参数中解析信息、响应状态代码以及 JSON。
+所有的协调逻辑都放在用例/服务层中,而领域逻辑保留在领域内。
+
((("Flask framework", "Flask API and service layer", "end-to-end tests for happy and unhappy paths")))
((("service layer", "end-to-end test of allocate API, testing happy and unhappy paths")))
Finally, we can confidently strip down our E2E tests to just two, one for
the happy path and one for the unhappy path:
+最后,我们可以自信地将我们的端到端(E2E)测试精简为仅两个:一个用于验证正常路径,另一个用于验证异常路径:
+
[[fewer_e2e_tests]]
.E2E tests only happy and unhappy paths (test_api.py)
@@ -615,27 +737,39 @@ We've successfully split our tests into two broad categories: tests about web
stuff, which we implement end to end; and tests about orchestration stuff, which
we can test against the service layer in memory.
+我们已经成功地将测试拆分为两大类:关于网络相关内容的测试,我们通过端到端(E2E)测试来实现;
+以及关于协调逻辑的测试,我们可以针对服务层在内存中进行测试。
+
[role="nobreakinside less_space"]
-.Exercise for the Reader
+.Exercise for the Reader(读者练习)
******************************************************************************
((("deallocate service, building (exerise)")))
Now that we have an allocate service, why not build out a service for
`deallocate`? We've added https://github.com/cosmicpython/code/tree/chapter_04_service_layer_exercise[an E2E test and a few stub service-layer tests] for
you to get started on GitHub.
+既然我们已经有了一个 `allocate` 服务,那么为什么不为 `deallocate` 构建一个服务呢?我们在 GitHub 上为你提供了一个 https://github.com/cosmicpython/code/tree/chapter_04_service_layer_exercise[E2E 测试和一些服务层的测试桩],
+可以帮助你开始动手实践。
+
If that's not enough, continue into the E2E tests and _flask_app.py_, and
refactor the Flask adapter to be more RESTful. Notice how doing so doesn't
require any change to our service layer or domain layer!
+如果这还不够,可以继续深入研究 E2E 测试和 _flask_app.py_,并重构 Flask 适配器以使其更符合 RESTful 风格。
+注意,这样做并不需要对我们的服务层或领域层进行任何更改!
+
TIP: If you decide you want to build a read-only endpoint for retrieving allocation
info, just do "the simplest thing that can possibly work," which is
`repo.get()` right in the Flask handler. We'll talk more about reads versus
writes in <>.
+如果你决定要构建一个用于检索分配信息的只读端点,只需做“可能有效的最简单的事情”,也就是直接在 Flask 处理器中使用 `repo.get()`。
+我们将在 <> 中进一步讨论读操作与写操作的区别。
******************************************************************************
[[why_is_everything_a_service]]
=== Why Is Everything Called a Service?
+为什么所有东西都被叫做服务?
((("services", "application service and domain service")))
((("service layer", "difference between domain service and")))
@@ -644,23 +778,35 @@ TIP: If you decide you want to build a read-only endpoint for retrieving allocat
Some of you are probably scratching your heads at this point trying to figure
out exactly what the difference is between a domain service and a service layer.
+此时你们中的一些人可能正在抓耳挠腮,试图弄清楚领域服务和服务层之间究竟有什么区别。
+
((("application services")))
We're sorry—we didn't choose the names, or we'd have much cooler and friendlier
ways to talk about this stuff.
+很抱歉——这些名称不是我们起的,否则我们会用更酷、更友好的方式来描述这些东西。
+
((("orchestration", "using application service")))
We're using two things called a _service_ in this chapter. The first is an
_application service_ (our service layer). Its job is to handle requests from the
outside world and to _orchestrate_ an operation. What we mean is that the
service layer _drives_ the application by following a bunch of simple steps:
+在本章中,我们提到了两种被称为 _服务_ 的东西。第一种是 _应用服务_(也就是我们的服务层)。它的职责是处理来自外部世界的请求并 _协调_ 操作。
+我们的意思是,服务层通过执行一系列简单的步骤来 _驱动_ 应用程序:
+
* Get some data from the database
+从数据库获取一些数据
* Update the domain model
+更新领域模型
* Persist any changes
+持久化任何更改
This is the kind of boring work that has to happen for every operation in your
system, and keeping it separate from business logic helps to keep things tidy.
+这是一种在系统中每个操作都必须完成的枯燥工作,将其与业务逻辑分离有助于保持代码整洁有序。
+
((("domain services")))
The second type of service is a _domain service_. This is the name for a piece of
logic that belongs in the domain model but doesn't sit naturally inside a
@@ -671,8 +817,14 @@ part of the model, but it doesn't seem right to have a persisted entity for
the job. Instead a stateless TaxCalculator class or a `calculate_tax` function
can do the job.
+第二种类型的服务是 _领域服务_。这是用于表示属于领域模型的一部分逻辑,但它不自然地适合放在有状态的实体或值对象中。
+例如,如果你正在构建一个购物车应用程序,你可能会选择将税收规则实现为一个领域服务。计算税费是一个与更新购物车分离的任务,
+它是模型中的重要部分,但为这个任务创建一个持久化的实体似乎并不合适。
+相反,一个无状态的 `TaxCalculator` 类或一个 `calculate_tax` 函数可以完成这项任务。
+
=== Putting Things in Folders to See Where It All Belongs
+将内容放入文件夹中以确定它们的归属
((("directory structure, putting project into folders")))
((("projects", "organizing into folders")))
@@ -682,8 +834,12 @@ As our application gets bigger, we'll need to keep tidying our directory
structure. The layout of our project gives us useful hints about what kinds of
object we'll find in each file.
+随着我们的应用程序变得越来越大,我们需要不断整理目录结构。项目的布局为我们提供了关于每个文件中可能会找到哪些类型对象的有用提示。
+
Here's one way we could organize things:
+以下是一种我们可以组织内容的方式:
+
[[nested_folder_tree]]
.Some subfolders
====
@@ -727,11 +883,16 @@ Here's one way we could organize things:
`Aggregate`, and you might add an __exceptions.py__ for domain-layer exceptions
and, as you'll see in <>, [.keep-together]#__commands.py__# and __events.py__.
((("domain model", "folder for")))
+让我们为领域模型创建一个文件夹。目前它只是一个文件,但对于更复杂的应用程序,你可能会为每个类创建一个文件;
+你可能会为 `Entity`、`ValueObject` 和 `Aggregate` 创建辅助父类的文件,你还可以添加一个 __exceptions.py__ 来处理领域层的异常,
+并且正如你会在 <> 中看到的,还可以添加 [.keep-together]#__commands.py__# 和 __events.py__。
<2> We'll distinguish the service layer. Currently that's just one file
called _services.py_ for our service-layer functions. You could
add service-layer exceptions here, and as you'll see in
<>, we'll add __unit_of_work.py__.
+我们将区分服务层。目前它只是一个名为 _services.py_ 的文件,用于保存我们的服务层函数。你可以在这里添加服务层的异常处理,
+并且正如你将在 <> 中看到的,我们还会添加 __unit_of_work.py__。
<3> _Adapters_ is a nod to the ports and adapters terminology. This will fill
up with any other abstractions around external I/O (e.g., a __redis_client.py__).
@@ -741,45 +902,58 @@ Here's one way we could organize things:
((("inward-facing adapters")))
((("secondary adapters")))
((("driven adapters")))
+_Adapters_ 的命名来源于端口和适配器的术语。这里将包含围绕外部 I/O 的其他抽象(例如,一个 __redis_client.py__)。
+严格来说,这些可以称为 _次级_ 适配器或者 _驱动_ 适配器,有时也称为 _面向内部_ 的适配器。
<4> Entrypoints are the places we drive our application from. In the
official ports and adapters terminology, these are adapters too, and are
referred to as _primary_, _driving_, or _outward-facing_ adapters.
((("entrypoints")))
+Entrypoints 是我们驱动应用程序的地方。在正式的端口和适配器术语中,这些也属于适配器,被称为 _主_、_驱动_ 或 _面向外部_ 的适配器。
((("ports", "putting in folder with adapters")))
What about ports? As you may remember, they are the abstract interfaces that the
adapters implement. We tend to keep them in the same file as the adapters that
implement them.
+那么端口(ports)呢?你可能还记得,端口是适配器实现的抽象接口。我们通常将它们与实现它们的适配器保存在同一个文件中。
+
=== Wrap-Up
+总结
((("service layer", "benefits of")))
((("Flask framework", "Flask API and service layer", "service layer benefits")))
Adding the service layer has really bought us quite a lot:
+引入服务层确实为我们带来了不少好处:
+
* Our Flask API endpoints become very thin and easy to write: their
only responsibility is doing "web stuff," such as parsing JSON
and producing the right HTTP codes for happy or unhappy cases.
+我们的 Flask API 端点变得非常简洁且易于编写:它们的唯一职责就是处理“网络相关的事情”,例如解析 JSON 以及为正常或异常情况生成合适的 HTTP 状态代码。
* We've defined a clear API for our domain, a set of use cases or
entrypoints that can be used by any adapter without needing to know anything
about our domain model classes--whether that's an API, a CLI (see
<>), or the tests! They're an adapter for our domain too.
+我们为领域定义了一个清晰的 API,即一组用例或入口点,任何适配器都可以使用它们,而无需了解我们的领域模型类的任何细节——无论是 API、CLI(参见 <>),还是测试!它们本质上也是我们领域的一个适配器。
* We can write tests in "high gear" by using the service layer, leaving us
free to refactor the domain model in any way we see fit. As long as
we can still deliver the same use cases, we can experiment with new
designs without needing to rewrite a load of tests.
+我们可以通过使用服务层以“高速模式”编写测试,这使我们能够自由地按照需要重构领域模型。只要我们仍然能够实现相同的用例,就可以尝试新的设计,而无需重写大量的测试。
* And our test pyramid is looking good--the bulk of our tests
are fast unit tests, with just the bare minimum of E2E and integration
tests.
+而且我们的测试金字塔看起来很不错——大部分测试是快速的单元测试,仅有少量必要的端到端(E2E)和集成测试。
==== The DIP in Action
+依赖倒置原则(DIP)的实践应用
((("dependencies", "abstract dependencies of service layer")))
((("service layer", "dependencies of")))
@@ -788,17 +962,23 @@ Adding the service layer has really bought us quite a lot:
dependencies of our service layer: the domain model
and `AbstractRepository` (the port, in ports and adapters terminology).
+<> 显示了我们服务层的依赖关系:领域模型和 `AbstractRepository`(在端口和适配器的术语中称为端口)。
+
((("dependencies", "abstract dependencies of service layer", "testing")))
((("service layer", "dependencies of", "testing")))
When we run the tests, <> shows
how we implement the abstract dependencies by using `FakeRepository` (the
adapter).
+当我们运行测试时,<> 展示了我们如何通过使用 `FakeRepository`(适配器)来实现抽象依赖。
+
((("service layer", "dependencies of", "real dependencies at runtime")))
((("dependencies", "real service layer dependencies at runtime")))
And when we actually run our app, we swap in the "real" dependency shown in
<>.
+当我们实际运行应用程序时,我们会替换为 <> 中所示的“真实”依赖。
+
[role="width-75"]
[[service_layer_diagram_abstract_dependencies]]
.Abstract dependencies of the service layer
@@ -821,7 +1001,7 @@ image::images/apwp_0403.png[]
[role="width-75"]
[[service_layer_diagram_test_dependencies]]
-.Tests provide an implementation of the abstract dependency
+.Tests provide an implementation of the abstract dependency(测试提供了对抽象依赖的实现)
image::images/apwp_0404.png[]
[role="image-source"]
----
@@ -850,7 +1030,7 @@ image::images/apwp_0404.png[]
[role="width-75"]
[[service_layer_diagram_runtime_dependencies]]
-.Dependencies at runtime
+.Dependencies at runtime(运行时的依赖)
image::images/apwp_0405.png[]
[role="image-source"]
----
@@ -890,41 +1070,53 @@ image::images/apwp_0405.png[]
Wonderful.
+太棒了!
+
((("service layer", "pros and cons or trade-offs")))
((("Flask framework", "Flask API and service layer", "service layer pros and cons")))
Let's pause for <>,
in which we consider the pros and cons of having a service layer at all.
+让我们暂停一下,进入 <>,在那里我们将探讨是否需要服务层的优缺点。
+
[[chapter_04_service_layer_tradeoffs]]
[options="header"]
-.Service layer: the trade-offs
+.Service layer: the trade-offs(Service层:权衡利弊)
|===
-|Pros|Cons
+|Pros(优点)|Cons(缺点)
a|
* We have a single place to capture all the use cases for our application.
+我们有一个统一的位置来收集应用程序的所有用例。
* We've placed our clever domain logic behind an API, which leaves us free to
refactor.
+我们将精妙的领域逻辑置于一个 API 的后面,这使我们可以自由地进行重构。
* We have cleanly separated "stuff that talks HTTP" from "stuff that talks
allocation."
+我们已将“处理 HTTP 的内容”与“处理分配的内容”清晰地分离开来。
* When combined with the Repository pattern and `FakeRepository`, we have
a nice way of writing tests at a higher level than the domain layer;
we can test more of our workflow without needing to use integration tests
(read on to <> for more elaboration on this).
+当与仓库模式(Repository pattern)和 `FakeRepository` 结合时,我们获得了一种在高于领域层级上编写测试的优雅方式;
+我们可以测试更多的工作流程,而无需使用集成测试(在 <> 中将对此进行更详细的阐述)。
a|
* If your app is _purely_ a web app, your controllers/view functions can be
the single place to capture all the use cases.
+如果你的应用程序 _纯粹_ 是一个 Web 应用,那么你的控制器/视图函数可以作为收集所有用例的唯一场所。
* It's yet another layer of abstraction.
+它是另一个抽象层。
* Putting too much logic into the service layer can lead to the _Anemic Domain_
antipattern. It's better to introduce this layer after you spot orchestration
logic creeping into your controllers.
((("domain model", "getting benefits of rich model")))
((("Anemic Domain antipattern")))
+将过多的逻辑放入服务层可能会导致 _贫血领域_ 的反模式。最好是在你发现协调逻辑开始侵入控制器时再引入这个层。
* You can get a lot of the benefits that come from having rich domain models
by simply pushing logic out of your controllers and down to the model layer,
@@ -932,17 +1124,24 @@ a|
controllers").
((("Flask framework", "Flask API and service layer", startref="ix_Flskapp")))
((("service layer", startref="ix_serlay")))
+通过简单地将逻辑从控制器中移到模型层,而无需在它们之间添加额外的层(也就是所谓的“胖模型,瘦控制器”),你可以获得许多使用丰富领域模型所带来的好处。
|===
But there are still some bits of awkwardness to tidy up:
+但仍有一些不太优雅的地方需要整理:
+
* The service layer is still tightly coupled to the domain, because
its API is expressed in terms of `OrderLine` objects. In
<>, we'll fix that and talk about
the way that the service layer enables more productive TDD.
+服务层仍然与领域紧密耦合,因为它的API是通过 `OrderLine` 对象来表达的。在<>中,
+我们会解决这个问题,并讨论服务层如何促进更高效的TDD。
* The service layer is tightly coupled to a `session` object. In <>,
we'll introduce one more pattern that works closely with the Repository and
Service Layer patterns, the Unit of Work pattern, and everything will be absolutely lovely.
You'll see!
+服务层与一个 `session` 对象紧密耦合。在<>中,我们将引入另一个与仓储模式和服务层模式密切配合的模式——
+工作单元(Unit of Work)模式,这将让一切变得非常美好。你会看到的!
From 5740917a3710b4a916fa863864f7f04a631d182d Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Fri, 31 Jan 2025 00:04:50 +0800
Subject: [PATCH 09/75] update Readme.md chapter_05_high_gear_low_gear.asciidoc
---
Readme.md | 4 +-
chapter_05_high_gear_low_gear.asciidoc | 145 ++++++++++++++++++++++++-
2 files changed, 141 insertions(+), 8 deletions(-)
diff --git a/Readme.md b/Readme.md
index 71c2f6f7..7377910b 100644
--- a/Readme.md
+++ b/Readme.md
@@ -23,8 +23,8 @@ O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布
| [Chapter 2: Repository
第二章:仓储(已翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(已翻译)](chapter_03_abstractions.asciidoc) | |
| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(已翻译)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(翻译中...)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 6: Unit of Work
第六章:工作单元(未翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(已翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 6: Unit of Work
第六章:工作单元(翻译中...)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 7: Aggregates
第七章:聚合(未翻译)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [**Part 2 Intro**](part2.asciidoc) | |
| [Chapter 8: Domain Events and a Simple Message Bus
第八章:领域事件与简单消息总线(未翻译)](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
diff --git a/chapter_05_high_gear_low_gear.asciidoc b/chapter_05_high_gear_low_gear.asciidoc
index 265f159c..e5ab8f09 100644
--- a/chapter_05_high_gear_low_gear.asciidoc
+++ b/chapter_05_high_gear_low_gear.asciidoc
@@ -1,5 +1,6 @@
[[chapter_05_high_gear_low_gear]]
== TDD in High Gear and Low Gear
+高速档与低速档中的测试驱动开发 (TDD)
((("test-driven development (TDD)", id="ix_TDD")))
We've introduced the service layer to capture some of the additional
@@ -8,40 +9,58 @@ clearly define our use cases and the workflow for each: what
we need to get from our repositories, what pre-checks and current state
validation we should do, and what we save at the end.
+我们引入了服务层来承担一些实际应用程序中所需的额外协调职责。服务层帮助我们清晰地定义用例以及每个用例的工作流程:我们需要从存储库中获取什么数据,
+我们应该进行哪些预检查和当前状态验证,以及最终需要保存什么内容。
+
((("test-driven development (TDD)", "unit tests operating at lower level, acting directly on model")))
But currently, many of our unit tests operate at a lower level, acting
directly on the model. In this chapter we'll discuss the trade-offs
involved in moving those tests up to the service-layer level, and
some more general testing guidelines.
+但目前,我们的许多单元测试运行在较低的层级,直接操作模型。在本章中,我们将讨论将这些测试上移到服务层级别时涉及的权衡,
+以及一些更为通用的测试指南。
+
-.Harry Says: Seeing a Test Pyramid in Action Was a Light-Bulb Moment
+.Harry Says: Seeing a Test Pyramid in Action Was a Light-Bulb Moment(Harry 说:看到测试金字塔的实际应用让我茅塞顿开)
*******************************************************************************
((("test-driven development (TDD)", "test pyramid, examining")))
Here are a few words from Harry directly:
+以下是 Harry 的几句话:
+
_I was initially skeptical of all Bob's architectural patterns, but seeing
an actual test pyramid made me a convert._
+_起初我对鲍勃的所有架构模式持怀疑态度,但看到一个实际的测试金字塔让我彻底信服了。_
+
_Once you implement domain modeling and the service layer, you really actually can
get to a stage where unit tests outnumber integration and end-to-end tests by
an order of magnitude. Having worked in places where the E2E test build would
take hours ("wait 'til tomorrow," essentially), I can't tell you what a
difference it makes to be able to run all your tests in minutes or seconds._
+_一旦你实现了领域建模和服务层,你真的可以达到这样一个阶段:单元测试的数量能够比集成测试和端到端测试多出一个数量级。曾经我在一些地方工作时,
+端到端测试的构建需要花费数小时(基本上是“等到明天吧”),我没法描述能够在几分钟甚至几秒内运行完所有测试带来的巨大改变。_
+
_Read on for some guidelines on how to decide what kinds of tests to write
and at which level. The high gear versus low gear way of thinking really changed
my testing life._
+
+_继续阅读,了解一些关于如何决定编写哪些类型的测试以及在哪个层级编写的指南。高速档与低速档的思维方式确实改变了我的测试工作方式。_
*******************************************************************************
=== How Is Our Test Pyramid Looking?
+我们的测试金字塔看起来如何?
((("service layer", "using, test pyramid and")))
((("test-driven development (TDD)", "test pyramid with service layer added")))
Let's see what this move to using a service layer, with its own service-layer tests,
does to our test pyramid:
+让我们来看看引入服务层以及为其编写服务层测试对我们的测试金字塔有何影响:
+
[[test_pyramid]]
.Counting types of tests
====
@@ -65,9 +84,12 @@ tests/e2e/test_api.py:2
Not bad! We have 15 unit tests, 8 integration tests, and just 2 end-to-end tests. That's
already a healthy-looking test pyramid.
+不错!我们有 15 个单元测试,8 个集成测试,以及仅仅 2 个端到端测试。这已经是一个非常健康的测试金字塔了。
+
=== Should Domain Layer Tests Move to the Service Layer?
+领域层测试是否应该移到服务层?
((("domain layer", "tests moving to service layer")))
((("service layer", "domain layer tests moving to")))
@@ -77,6 +99,9 @@ software against the service layer, we don't really need tests for the domain
model anymore. Instead, we could rewrite all of the domain-level tests from
<> in terms of the service layer:
+让我们看看再进一步会发生什么。由于我们可以针对服务层测试我们的软件,因此实际上我们不再需要领域模型的测试了。
+相反,我们可以根据服务层,重写所有来自<>的领域层级测试:
+
.Rewriting a domain test at the service layer (tests/unit/test_services.py)
====
@@ -115,20 +140,30 @@ def test_prefers_warehouse_batches_to_shipments():
((("service layer", "domain layer tests moving to", "reasons for")))
Why would we want to do that?
+为什么我们会想要这么做呢?
+
Tests are supposed to help us change our system fearlessly, but often
we see teams writing too many tests against their domain model. This causes
problems when they come to change their codebase and find that they need to
update tens or even hundreds of unit tests.
+测试的目的是帮助我们无所畏惧地更改系统,但我们经常看到团队针对领域模型编写了过多的测试。这会在需要更改代码库时引发问题,
+因为他们可能发现需要更新几十甚至上百个单元测试。
+
This makes sense if you stop to think about the purpose of automated tests. We
use tests to enforce that a property of the system doesn't change while we're
working. We use tests to check that the API continues to return 200, that the
database session continues to commit, and that orders are still being allocated.
+如果你停下来思考一下自动化测试的目的,这就说得通了。我们使用测试是为了确保在我们工作时,系统的某些属性不会发生变化。
+我们使用测试来检查 API 是否仍然返回 200,数据库会话是否仍旧提交,以及订单是否仍被分配。
+
If we accidentally change one of those behaviors, our tests will break. The
flip side, though, is that if we want to change the design of our code, any
tests relying directly on that code will also fail.
+如果我们意外更改了这些行为之一,那么我们的测试就会失败。不过,反过来说,如果我们想更改代码的设计,任何直接依赖该代码的测试也会失败。
+
As we get further into the book, you'll see how the service layer forms an API
for our system that we can drive in multiple ways. Testing against this API
reduces the amount of code that we need to change when we refactor our domain
@@ -136,13 +171,19 @@ model. If we restrict ourselves to testing only against the service layer,
we won't have any tests that directly interact with "private" methods or
attributes on our model objects, which leaves us freer to refactor them.
+随着我们进一步阅读本书,你会看到服务层如何为我们的系统形成一个 API,这个 API 能以多种方式进行驱动。针对这个 API 进行测试可以
+减少在重构领域模型时需要更改的代码量。如果我们只限制自己测试服务层,那么就不会有任何测试直接与模型对象的“私有”方法或属性交互,
+这使得我们可以更自由地对它们进行重构。
+
TIP: Every line of code that we put in a test is like a blob of glue, holding
the system in a particular shape. The more low-level tests we have, the
harder it will be to change things.
+我们在测试中编写的每一行代码都像是一滴胶水,将系统固定成特定的形状。低层级测试越多,改变系统就会变得越困难。
[[kinds_of_tests]]
=== On Deciding What Kind of Tests to Write
+关于如何决定编写哪些类型的测试
((("domain model", "deciding whether to write tests against")))
((("coupling", "trade-off between design feedback and")))
@@ -152,6 +193,9 @@ wrong to write tests against the domain model?" To answer those questions, it's
important to understand the trade-off between coupling and design feedback (see
<>).
+你可能会问自己:“那我是否应该重写所有的单元测试呢?针对领域模型编写测试是不是错的?”要回答这些问题,
+理解耦合与设计反馈之间的取舍非常重要(参见<>)。
+
[[test_spectrum_diagram]]
.The test spectrum
image::images/apwp_0501.png[]
@@ -173,30 +217,47 @@ Extreme programming (XP) exhorts us to "listen to the code." When we're writing
tests, we might find that the code is hard to use or notice a code smell. This
is a trigger for us to refactor, and to reconsider our design.
+极限编程(XP)敦促我们“倾听代码的声音”。当我们编写测试时,可能会发现代码难以使用,或者察觉到代码有异味。
+这就是一个触发点,提醒我们进行重构并重新审视我们的设计。
+
We only get that feedback, though, when we're working closely with the target
code. A test for the HTTP API tells us nothing about the fine-grained design of
our objects, because it sits at a much higher level of abstraction.
+然而,只有当我们与目标代码密切合作时,才能获得这种反馈。针对 HTTP API 的测试无法告诉我们对象的细粒度设计情况,
+因为它处于更高的抽象层级。
+
On the other hand, we can rewrite our entire application and, so long as we
don't change the URLs or request formats, our HTTP tests will continue to pass.
This gives us confidence that large-scale changes, like changing the database schema,
haven't broken our code.
+另一方面,我们可以重写整个应用程序,只要不更改 URL 或请求格式,HTTP 测试仍然会通过。这让我们有信心进行大规模的更改,
+例如修改数据库模式,而不会破坏我们的代码。
+
At the other end of the spectrum, the tests we wrote in <> helped us to
flesh out our understanding of the objects we need. The tests guided us to a
design that makes sense and reads in the domain language. When our tests read
in the domain language, we feel comfortable that our code matches our intuition
about the problem we're trying to solve.
+在光谱的另一端,我们在<>中编写的测试帮助我们完善了对所需对象的理解。这些测试引导我们实现了一个合理的设计,
+并使用了领域语言。当我们的测试以领域语言编写时,我们会感到安心,因为代码与我们试图解决的问题直观认识是一致的。
+
Because the tests are written in the domain language, they act as living
documentation for our model. A new team member can read these tests to quickly
understand how the system works and how the core concepts interrelate.
+由于这些测试是用领域语言编写的,它们可以作为我们模型的动态文档。新团队成员可以通过阅读这些测试快速了解系统的工作原理以及核心概念之间的关系。
+
We often "sketch" new behaviors by writing tests at this level to see how the
code might look. When we want to improve the design of the code, though, we will need to replace
or delete these tests, because they are tightly coupled to a particular
[.keep-together]#implementation#.
+我们经常通过在这个层级编写测试来“勾勒”新行为,来试试看代码可能会是什么样子。然而,当我们想改进代码设计时,就需要替换或删除这些测试,
+因为它们与特定的[.keep-together]#实现#紧密耦合。
+
// IDEA: (EJ3) an example that is overmocked would be good here if you decide to
// add one. Ch12 already has one that could be expanded.
@@ -208,32 +269,44 @@ or delete these tests, because they are tightly coupled to a particular
=== High and Low Gear
+高速档与低速档
((("test-driven development (TDD)", "high and low gear")))
Most of the time, when we are adding a new feature or fixing a bug, we don't
need to make extensive changes to the domain model. In these cases, we prefer
to write tests against services because of the lower coupling and higher coverage.
+大多数情况下,当我们添加新功能或修复一个错误时,并不需要对领域模型进行大规模更改。在这些情况下,我们更倾向于针对服务编写测试,
+因为这样可以降低耦合且提高覆盖率。
+
((("service layer", "writing tests against")))
For example, when writing an `add_stock` function or a `cancel_order` feature,
we can work more quickly and with less coupling by writing tests against the
service layer.
+例如,在编写`add_stock`函数或`cancel_order`功能时,通过针对服务层编写测试,我们可以以更快的速度完成工作,并减少耦合。
+
((("domain model", "writing tests against")))
When starting a new project or when hitting a particularly gnarly problem,
we will drop back down to writing tests against the domain model so we
get better feedback and executable documentation of our intent.
+当启动一个新项目或遇到一个特别棘手的问题时,我们会退回到针对领域模型编写测试,以获得更好的反馈以及可执行的意图文档。
+
The metaphor we use is that of shifting gears. When starting a journey, the
bicycle needs to be in a low gear so that it can overcome inertia. Once we're off
and running, we can go faster and more efficiently by changing into a high gear;
but if we suddenly encounter a steep hill or are forced to slow down by a
hazard, we again drop down to a low gear until we can pick up speed again.
+我们使用的比喻是换挡。当开始一段旅程时,自行车需要处于低速档以克服惯性。一旦起步并行进,
+我们可以换到高速档以更快、更高效地行驶;但如果突然遇到陡坡或由于障碍被迫减速,我们会再次降到低速档,直到能够重新提速。
+
[[primitive_obsession]]
=== Fully Decoupling the Service-Layer Tests from the Domain
+将服务层测试与领域完全解耦
((("service layer", "fully decoupling from the domain", id="ix_serlaydec")))
((("domain layer", "fully decoupling service layer from", id="ix_domlaydec")))
@@ -242,11 +315,17 @@ We still have direct dependencies on the domain in our service-layer
tests, because we use domain objects to set up our test data and to invoke
our service-layer functions.
+我们的服务层测试中仍然直接依赖于领域模型,因为我们使用领域对象来设置测试数据并调用服务层函数。
+
To have a service layer that's fully decoupled from the domain, we need to
rewrite its API to work in terms of primitives.
+要让服务层与领域模型完全解耦,我们需要重写其 API,使其基于基础数据类型(primitives)工作。
+
Our service layer currently takes an `OrderLine` domain object:
+我们的服务层当前接收一个 `OrderLine` 领域对象:
+
[[service_domain]]
.Before: allocate takes a domain object (service_layer/services.py)
====
@@ -259,6 +338,8 @@ def allocate(line: OrderLine, repo: AbstractRepository, session) -> str:
How would it look if its parameters were all primitive types?
+如果其参数全是基础数据类型,会是什么样子呢?
+
[[service_takes_primitives]]
.After: allocate takes strings and ints (service_layer/services.py)
====
@@ -273,6 +354,8 @@ def allocate(
We rewrite the tests in those terms as well:
+我们也用这些基础数据类型重写测试:
+
[[tests_call_with_primitives]]
.Tests now use primitives in function call (tests/unit/test_services.py)
====
@@ -292,8 +375,12 @@ But our tests still depend on the domain, because we still manually instantiate
`Batch` objects. So, if one day we decide to massively refactor how our `Batch`
model works, we'll have to change a bunch of tests.
+但是我们的测试仍然依赖于领域模型,因为我们仍需手动实例化 `Batch` 对象。因此,如果有一天我们决定对 `Batch` 模型的工作方式进行大规模重构,
+就不得不修改许多测试。
+
==== Mitigation: Keep All Domain Dependencies in Fixture Functions
+缓解措施:将所有领域依赖集中在固定装置函数中
((("faking", "FakeRepository", "adding fixture function on")))
((("fixture functions, keeping all domain dependencies in")))
@@ -303,6 +390,8 @@ We could at least abstract that out to a helper function or a fixture
in our tests. Here's one way you could do that, adding a factory
function on `FakeRepository`:
+我们至少可以将其抽象为测试中的一个辅助函数或固定装置(fixture)。以下是实现这一点的一种方式,通过在 `FakeRepository` 上添加一个工厂函数:
+
[[services_factory_function]]
.Factory functions for fixtures are one possibility (tests/unit/test_services.py)
@@ -332,8 +421,11 @@ def test_returns_allocation():
At least that would move all of our tests' dependencies on the domain
into one place.
+至少这样可以将我们所有测试对领域的依赖集中到一个地方。
+
==== Adding a Missing Service
+添加一个缺失的服务
((("test-driven development (TDD)", "fully decoupling service layer from the domain", "adding missing service")))
We could go one step further, though. If we had a service to add stock,
@@ -341,6 +433,9 @@ we could use that and make our service-layer tests fully expressed
in terms of the service layer's official use cases, removing all dependencies
on the domain:
+不过,我们还可以更进一步。如果我们有一个用于添加库存的服务,就可以使用该服务,使我们的服务层测试完全基于服务层的官方用例,
+从而移除对领域模型的所有依赖:
+
[[test_add_batch]]
.Test for new add_batch service (tests/unit/test_services.py)
@@ -359,10 +454,13 @@ def test_add_batch():
TIP: In general, if you find yourself needing to do domain-layer stuff directly
in your service-layer tests, it may be an indication that your service
layer is incomplete.
+通常情况下,如果你发现在服务层测试中需要直接处理领域层的内容,这可能表明你的服务层还不够完善。
[role="pagebreak-before"]
And the implementation is just two lines:
+而实现代码只有两行:
+
[[add_batch_service]]
.A new service for add_batch (service_layer/services.py)
====
@@ -386,12 +484,16 @@ def allocate(
NOTE: Should you write a new service just because it would help remove
dependencies from your tests? Probably not. But in this case, we
almost definitely would need an `add_batch` service one day [.keep-together]#anyway#.
+你是否应该仅仅为了帮助移除测试中的依赖而编写一个新服务?可能不必如此。但在这种情况下,我们几乎可以确定有一天我们会
+需要一个 `add_batch` 服务[.keep-together]#无论如何#。
((("services", "service layer tests only using services")))
That now allows us to rewrite _all_ of our service-layer tests purely
in terms of the services themselves, using only primitives, and without
any dependencies on the model:
+现在,这使得我们可以将*所有*服务层测试纯粹以服务本身为基础重写,只使用基础数据类型(primitives),而无需任何对模型的依赖:
+
[[services_tests_all_services]]
.Services tests now use only services (tests/unit/test_services.py)
@@ -422,8 +524,11 @@ This is a really nice place to be in. Our service-layer tests depend on only
the service layer itself, leaving us completely free to refactor the model as
we see fit.
+这真是一个令人愉快的境地。我们的服务层测试仅依赖于服务层本身,使我们可以完全自由地按照需要重构模型。
+
[role="pagebreak-before less_space"]
=== Carrying the Improvement Through to the E2E Tests
+将改进扩展到端到端(E2E)测试
((("E2E tests", see="end-to-end tests")))
((("end-to-end tests", "decoupling of service layer from domain, carrying through to")))
@@ -434,9 +539,14 @@ tests from the model, adding an API endpoint to add a batch would remove
the need for the ugly `add_stock` fixture, and our E2E tests could be free
of those hardcoded SQL queries and the direct dependency on the database.
+就像添加 `add_batch` 帮助将我们的服务层测试与模型解耦一样,添加一个用于添加批次的 API 端点可以去除丑陋的 `add_stock` 测试夹具的需求,
+而我们的端到端(E2E)测试也可以摆脱那些硬编码的 SQL 查询以及对数据库的直接依赖。
+
Thanks to our service function, adding the endpoint is easy, with just a little
JSON wrangling and a single function call required:
+多亏了我们的服务函数,添加这个端点非常简单,只需处理一点点 JSON,并进行一次函数调用:
+
[[api_for_add_batch]]
.API for adding a batch (entrypoints/flask_app.py)
@@ -467,11 +577,16 @@ NOTE: Are you thinking to yourself, POST to _/add_batch_? That's not
if you'd like to make it all more RESTy, maybe a POST to _/batches_,
then knock yourself out! Because Flask is a thin adapter, it'll be
easy. See <>.
+你是否在心里想,POST 到 _/add_batch_?这不太符合 RESTful!你完全正确。我们在这里确实有点随意,
+但如果你想让它更符合 REST 的风格,或许可以考虑 POST 到 _/batches_,那就随你喜欢了!因为 Flask 是一个轻量级的适配器,
+这会很容易实现。参见 <>。
And our hardcoded SQL queries from _conftest.py_ get replaced with some
API calls, meaning the API tests have no dependencies other than the API,
which is also nice:
+我们在 _conftest.py_ 中的那些硬编码 SQL 查询被一些 API 调用取代了,这意味着 API 测试除了依赖 API 本身之外没有其他依赖,这也非常不错:
+
[[api_tests_with_no_sql]]
.API tests can now add their own batches (tests/e2e/test_api.py)
====
@@ -507,24 +622,28 @@ def test_happy_path_returns_201_and_allocated_batch():
=== Wrap-Up
+总结
((("service layer", "benefits to test-driven development")))
((("test-driven development (TDD)", "benefits of service layer to")))
Once you have a service layer in place, you really can move the majority
of your test coverage to unit tests and develop a healthy test pyramid.
+一旦你建立了服务层,确实可以将大部分测试覆盖移到单元测试中,从而构建一个合理的测试金字塔。
+
[role="nobreakinside less_space"]
[[types_of_test_rules_of_thumb]]
-.Recap: Rules of Thumb for Different Types of Test
+.Recap: Rules of Thumb for Different Types of Test(回顾:针对不同类型测试的经验法则)
******************************************************************************
-Aim for one end-to-end test per feature::
+Aim for one end-to-end test per feature(每个功能目标实现一个端到端测试)::
This might be written against an HTTP API, for example. The objective
is to demonstrate that the feature works, and that all the moving parts
are glued together correctly.
((("end-to-end tests", "aiming for one test per feature")))
+例如,这可能是针对一个 HTTP API 编写的。目标是证明该功能可以正常工作,并且所有的组件都正确地结合在一起。
-Write the bulk of your tests against the service layer::
+Write the bulk of your tests against the service layer(将大部分测试编写在服务层上)::
These edge-to-edge tests offer a good trade-off between coverage,
runtime, and efficiency. Each test tends to cover one code path of a
feature and use fakes for I/O. This is the place to exhaustively
@@ -535,14 +654,19 @@ Write the bulk of your tests against the service layer::
can be useful. But see also <> and
<>.]
((("service layer", "writing bulk of tests against")))
+这些端到端的测试在覆盖范围、运行时间和效率之间提供了良好的权衡。每个测试通常覆盖一个功能的代码路径,并使用假对象(fakes)来处理 I/O。
+这是全面覆盖所有边界情况以及业务逻辑内部细节的最佳位置。脚注:[一个关于在更高层级编写测试的合理担忧是,对于更复杂的用例,
+这可能会导致组合爆炸的风险。在这种情况下,针对各个协作域对象的较低层次单元测试可能是有用的。
+但同时也可以参考 <> 和 <>。]
-Maintain a small core of tests written against your domain model::
+Maintain a small core of tests written against your domain model(维护一小部分针对领域模型编写的核心测试)::
These tests have highly focused coverage and are more brittle, but they have
the highest feedback. Don't be afraid to delete these tests if the
functionality is later covered by tests at the service layer.
((("domain model", "maintaining small core of tests written against")))
+这些测试具有非常集中的覆盖范围,但相对来说更脆弱,但它们提供了最高的反馈速度。如果这些功能后来被服务层的测试覆盖了,不要害怕删除这些测试。
-Error handling counts as a feature::
+Error handling counts as a feature(错误处理也算作一个功能。)::
Ideally, your application will be structured such that all errors that
bubble up to your entrypoints (e.g., Flask) are handled in the same way.
This means you need to test only the happy path for each feature, and to
@@ -550,17 +674,26 @@ Error handling counts as a feature::
unit tests, of course).
((("test-driven development (TDD)", startref="ix_TDD")))
((("error handling", "counting as a feature")))
+理想情况下,你的应用程序结构应确保所有冒泡到入口点(例如,Flask)的错误都以相同的方式处理。这意味着你只需为每个功能测试其正常路径,
+并专门保留一个端到端测试用于测试所有异常路径(当然,还需要许多单元测试来覆盖各种异常路径)。
******************************************************************************
A few
things will help along the way:
+以下几点会对你有所帮助:
+
* Express your service layer in terms of primitives rather than domain objects.
+用原语(primitives)而不是领域对象来表达你的服务层。
* In an ideal world, you'll have all the services you need to be able to test
entirely against the service layer, rather than hacking state via
repositories or the database. This pays off in your end-to-end tests as well.
((("test-driven development (TDD)", "types of tests, rules of thumb for")))
+在理想情况下,你应该拥有所有需要的服务,能够完全针对服务层进行测试,而不是通过存储库或数据库来操作状态。
+这在你的端到端测试中也会有所收益。
Onto the next chapter!
+
+进入下一章!
From 01b5487927593bba9b2f5232c721194c420c2a08 Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Fri, 31 Jan 2025 00:33:25 +0800
Subject: [PATCH 10/75] update Readme.md chapter_06_uow.asciidoc
---
Readme.md | 4 +-
chapter_06_uow.asciidoc | 198 +++++++++++++++++++++++++++++++++++++---
2 files changed, 187 insertions(+), 15 deletions(-)
diff --git a/Readme.md b/Readme.md
index 7377910b..c58867bc 100644
--- a/Readme.md
+++ b/Readme.md
@@ -24,8 +24,8 @@ O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布
| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(已翻译)](chapter_03_abstractions.asciidoc) | |
| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(已翻译)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(已翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 6: Unit of Work
第六章:工作单元(翻译中...)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 7: Aggregates
第七章:聚合(未翻译)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 6: Unit of Work
第六章:工作单元(已翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 7: Aggregates
第七章:聚合(翻译中...)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [**Part 2 Intro**](part2.asciidoc) | |
| [Chapter 8: Domain Events and a Simple Message Bus
第八章:领域事件与简单消息总线(未翻译)](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 9: Going to Town on the MessageBus
第九章:深入探讨消息总线(未翻译)](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
diff --git a/chapter_06_uow.asciidoc b/chapter_06_uow.asciidoc
index 24c9a2a2..7e9c6231 100644
--- a/chapter_06_uow.asciidoc
+++ b/chapter_06_uow.asciidoc
@@ -1,16 +1,22 @@
[[chapter_06_uow]]
== Unit of Work Pattern
+工作单元模式
((("Unit of Work pattern", id="ix_UoW")))
In this chapter we'll introduce the final piece of the puzzle that ties
together the Repository and Service Layer patterns: the _Unit of Work_ pattern.
+在本章中,我们将介绍拼接存储库模式和服务层模式的最后一块拼图:_工作单元_模式。
+
((("UoW", see="Unit of Work pattern")))
((("atomic operations")))
If the Repository pattern is our abstraction over the idea of persistent storage,
the Unit of Work (UoW) pattern is our abstraction over the idea of _atomic operations_. It
will allow us to finally and fully decouple our service layer from the data layer.
+如果说存储库模式是对持久化存储概念的抽象,那么工作单元(Unit of Work,UoW)模式就是对_原子操作_概念的抽象。
+它将使我们最终完全将服务层与数据层解耦。
+
((("Unit of Work pattern", "without, API talking directly to three layers")))
((("APIs", "without Unit of Work pattern, talking directly to three layers")))
<> shows that, currently, a lot of communication occurs
@@ -18,11 +24,17 @@ across the layers of our infrastructure: the API talks directly to the database
layer to start a session, it talks to the repository layer to initialize
`SQLAlchemyRepository`, and it talks to the service layer to ask it to allocate.
+<> 展示了当前我们的基础设施各层之间存在大量通信:API 直接与数据库层交互以启动会话,
+与存储库层交互以初始化 `SQLAlchemyRepository`,并与服务层交互以请求进行分配。
+
[TIP]
====
The code for this chapter is in the
chapter_06_uow branch https://oreil.ly/MoWdZ[on [.keep-together]#GitHub#]:
+本章的代码位于
+chapter_06_uow 分支 https://oreil.ly/MoWdZ[在[.keep-together]#GitHub#]:
+
----
git clone https://github.com/cosmicpython/code.git
cd code
@@ -34,7 +46,7 @@ git checkout chapter_04_service_layer
[role="width-75"]
[[before_uow_diagram]]
-.Without UoW: API talks directly to three layers
+.Without UoW: API talks directly to three layers(没有工作单元(UoW):API 直接与三层交互)
image::images/apwp_0601.png[]
((("databases", "Unit of Work pattern managing state for")))
@@ -45,16 +57,22 @@ collaborates with the UoW (we like to think of the UoW as being part of the
service layer), but neither the service function itself nor Flask now needs
to talk directly to the database.
+<> 展示了我们的目标状态。现在,Flask API 仅执行两件事:初始化一个工作单元(UoW),并调用一个服务。
+服务与工作单元协作(我们倾向于将工作单元视为服务层的一部分),但服务函数本身和 Flask 都不再需要直接与数据库交互。
+
((("context manager")))
And we'll do it all using a lovely piece of Python syntax, a context manager.
+我们将通过一段优雅的 _Python_ 语法——上下文管理器来实现这一切。
+
[role="width-75"]
[[after_uow_diagram]]
-.With UoW: UoW now manages database state
+.With UoW: UoW now manages database state(有了工作单元(UoW):UoW 现在管理数据库状态)
image::images/apwp_0602.png[]
=== The Unit of Work Collaborates with the Repository
+工作单元与存储库协作
//TODO (DS) do you talk anywhere about multiple repositories?
@@ -62,6 +80,8 @@ image::images/apwp_0602.png[]
((("Unit of Work pattern", "collaboration with repository")))
Let's see the unit of work (or UoW, which we pronounce "you-wow") in action. Here's how the service layer will look when we're finished:
+让我们看看工作单元(Unit of Work,简称 UoW,我们发音为“you-wow”)的实际应用。当我们完成后,服务层将如下所示:
+
[[uow_preview]]
.Preview of unit of work in action (src/allocation/service_layer/services.py)
====
@@ -82,12 +102,15 @@ def allocate(
<1> We'll start a UoW as a context manager.
((("context manager", "starting Unit of Work as")))
+我们将以上下文管理器的形式启动一个工作单元(UoW)。
<2> `uow.batches` is the batches repo, so the UoW provides us
access to our permanent storage.
((("storage", "permanent, UoW providing entrypoint to")))
+`uow.batches` 是批次(batches)仓储,因此,工作单元(UoW)为我们提供了访问持久存储的途径。
<3> When we're done, we commit or roll back our work, using the UoW.
+当我们完成后,我们使用 UoW 提交或回滚我们的工作。
((("object neighborhoods")))
((("collaborators")))
@@ -100,26 +123,38 @@ In responsibility-driven design, clusters of objects that collaborate in their
roles are called _object neighborhoods_, which is, in our professional opinion,
totally adorable.]
+UoW 充当我们持久化存储的单一入口,并跟踪加载的对象以及它们的最新状态。脚注:
+你可能遇到过使用 _协作者_ 这个词来描述为实现某种目标而协作的对象。工作单元和存储库在对象建模的意义上是协作者的一个极佳示例。
+在责任驱动设计中,那些在各自职责中协作的对象簇被称为 _对象邻域_,我们专业地认为这个称呼简直太可爱了。
+
This gives us three useful things:
+这为我们提供了三大好处:
+
* A stable snapshot of the database to work with, so the
objects we use aren't changing halfway through an operation
+一个数据库的稳定快照,供我们使用,这样我们操作过程中使用的对象就不会中途发生变化。
* A way to persist all of our changes at once, so if something
goes wrong, we don't end up in an inconsistent state
+一种一次性持久化所有更改的方法,这样如果出现问题,我们就不会陷入不一致的状态。
* A simple API to our persistence concerns and a handy place
to get a repository
+一个简化的持久化操作接口,以及一个获取存储库的方便位置。
=== Test-Driving a UoW with Integration Tests
+通过集成测试驱动工作单元(UoW)的测试
((("integration tests", "test-driving Unit of Work with")))
((("testing", "Unit of Work with integration tests")))
((("Unit of Work pattern", "test driving with integration tests")))
Here are our integration tests for the UOW:
+以下是我们针对 UoW 的集成测试:
+
[[test_unit_of_work]]
.A basic "round-trip" test for a UoW (tests/integration/test_uow.py)
@@ -145,16 +180,21 @@ def test_uow_can_retrieve_a_batch_and_allocate_to_it(session_factory):
<1> We initialize the UoW by using our custom session factory
and get back a `uow` object to use in our `with` block.
+我们通过使用自定义的会话工厂初始化 UoW,并得到一个 `uow` 对象,以便在我们的 `with` 块中使用。
<2> The UoW gives us access to the batches repository via
`uow.batches`.
+UoW 通过 `uow.batches` 为我们提供访问批次存储库的途径。
<3> We call `commit()` on it when we're done.
+当我们完成后,我们调用 `commit()`。
((("SQL", "helpers for Unit of Work")))
For the curious, the `insert_batch` and `get_allocated_batch_ref` helpers look
like this:
+对于感兴趣的读者,`insert_batch` 和 `get_allocated_batch_ref` 辅助函数如下所示:
+
[[sql_helpers]]
.Helpers for doing SQL stuff (tests/integration/test_uow.py)
====
@@ -191,9 +231,13 @@ def get_allocated_batch_ref(session, orderid, sku):
is doing (double) assignment-unpacking to get the single value
back out of these two nested sequences.
It becomes readable once you've used it a few times!
+`[[orderlineid]] =` 语法或许显得有些过于巧妙,我们对此表示歉意。实际上,这里发生的事情是 `session.execute` 返回了一列行的列表,
+其中每一行是一个包含列值的元组;在我们的具体场景中,这是一个只有一行的列表,而这行是一个仅包含一个列值的元组。
+左侧的双重方括号完成了(双重)解包赋值,从这两个嵌套序列中提取出唯一的值。使用过几次后,这种写法就会变得清晰易读了!
=== Unit of Work and Its Context Manager
+工作单元及其上下文管理器
((("Unit of Work pattern", "and its context manager")))
((("context manager", "Unit of Work and", id="ix_ctxtmgr")))
@@ -201,6 +245,8 @@ def get_allocated_batch_ref(session, orderid, sku):
In our tests we've implicitly defined an interface for what a UoW needs to do. Let's make that explicit by using an abstract
base class:
+在我们的测试中,实际上已经隐式定义了工作单元(UoW)需要实现的接口。现在,让我们通过使用抽象基类将其明确化:
+
[[abstract_unit_of_work]]
.Abstract UoW context manager (src/allocation/service_layer/unit_of_work.py)
@@ -226,24 +272,31 @@ class AbstractUnitOfWork(abc.ABC):
<1> The UoW provides an attribute called `.batches`, which will give us access
to the batches repository.
+UoW 提供了一个名为 `.batches` 的属性,它使我们能够访问批次存储库。
<2> If you've never seen a context manager, +++__enter__+++ and +++__exit__+++ are
the two magic methods that execute when we enter the `with` block and
when we exit it, respectively. They're our setup and teardown phases.
((("magic methods", "__enter__ and __exit__", secondary-sortas="enter")))
((("__enter__ and __exit__ magic methods", primary-sortas="enter and exit")))
+如果你从未见过上下文管理器,+++__enter__+++ 和 +++__exit__+++ 是两个魔法方法,
+分别在我们进入 `with` 块和退出 `with` 块时执行。它们对应我们的设置(setup)和销毁(teardown)阶段。
<3> We'll call this method to explicitly commit our work when we're ready.
+当我们准备好时,我们将调用此方法来显式提交我们的工作。
<4> If we don't commit, or if we exit the context manager by raising an error,
we do a `rollback`. (The rollback has no effect if `commit()` has been
called. Read on for more discussion of this.)
((("rollbacks")))
+如果我们没有调用 `commit()`,或者通过引发错误退出上下文管理器,我们将执行一次 `rollback`(回滚)。
+(如果已经调用了 `commit()`,回滚将不起作用。后续会有更多相关讨论。)
// TODO: bring this code listing back under test, remove `return self` from all the uows.
==== The Real Unit of Work Uses SQLAlchemy Sessions
+真正的工作单元使用 SQLAlchemy 会话
((("Unit of Work pattern", "and its context manager", "real UoW using SQLAlchemy session")))
((("databases", "SQLAlchemy adding session for Unit of Work")))
@@ -251,6 +304,8 @@ class AbstractUnitOfWork(abc.ABC):
The main thing that our concrete implementation adds is the
database session:
+我们的具体实现主要增加了一个数据库会话:
+
[[unit_of_work]]
.The real SQLAlchemy UoW (src/allocation/service_layer/unit_of_work.py)
====
@@ -287,17 +342,21 @@ class SqlAlchemyUnitOfWork(AbstractUnitOfWork):
<1> The module defines a default session factory that will connect to Postgres,
but we allow that to be overridden in our integration tests so that we
can use SQLite instead.
+该模块定义了一个默认会话工厂,用于连接到 Postgres,但我们允许在集成测试中重写它,这样我们就可以改用 SQLite。
<2> The +++__enter__+++ method is responsible for starting a database session and instantiating
a real repository that can use that session.
((("__enter__ and __exit__ magic methods", primary-sortas="enter and exit")))
++++__enter__+++ 方法负责启动一个数据库会话并实例化一个能够使用该会话的真实存储库。
<3> We close the session on exit.
+在退出时,我们会关闭会话。
<4> Finally, we provide concrete `commit()` and `rollback()` methods that
use our database session.
((("commits", "commit method")))
((("rollbacks", "rollback method")))
+最后,我们提供了具体的 `commit()` 和 `rollback()` 方法来操作我们的数据库会话。
//IDEA: why not swap out db using os.environ?
// (EJ2) Could be a good idea to point out that this couples the unit of work to postgres.
@@ -310,12 +369,15 @@ class SqlAlchemyUnitOfWork(AbstractUnitOfWork):
==== Fake Unit of Work for Testing
+用于测试的伪工作单元
((("Unit of Work pattern", "and its context manager", "fake UoW for testing")))
((("faking", "FakeUnitOfWork for service layer testing")))
((("testing", "fake UoW for service layer testing")))
Here's how we use a fake UoW in our service-layer tests:
+以下是我们在服务层测试中使用伪工作单元(UoW)的方式:
+
[[fake_unit_of_work]]
.Fake UoW (tests/unit/test_services.py)
====
@@ -352,19 +414,25 @@ def test_allocate_returns_allocation():
<1> `FakeUnitOfWork` and `FakeRepository` are tightly coupled,
just like the real `UnitofWork` and `Repository` classes.
That's fine because we recognize that the objects are collaborators.
+`FakeUnitOfWork` 和 `FakeRepository` 紧密耦合,就像真实的 `UnitOfWork` 和 `Repository` 类一样。
+这没有问题,因为我们知道这些对象是协作者。
<2> Notice the similarity with the fake `commit()` function
from `FakeSession` (which we can now get rid of). But it's
a substantial improvement because we're now [.keep-together]#faking# out
code that we wrote rather than third-party code. Some
people say, https://oreil.ly/0LVj3["Don't mock what you don't own"].
+注意它与 `FakeSession` 中伪造的 `commit()` 函数的相似之处(我们现在可以将其移除)。但这是一项重要的改进,
+因为我们现在是在[.keep-together]#伪造#我们自己编写的代码,而不是第三方代码。
+有些人会说,https://oreil.ly/0LVj3[“不要模拟你不拥有的东西”]。
<3> In our tests, we can instantiate a UoW and pass it to
our service layer, rather than passing a repository and a session.
This is considerably less cumbersome.
+在我们的测试中,我们可以实例化一个 UoW 并将其传递给服务层,而不是传递一个存储库和一个会话。这要简单得多。
[role="nobreakinside less_space"]
-.Don't Mock What You Don't Own
+.Don't Mock What You Don't Own(不要模拟你不拥有的东西)
********************************************************************************
((("SQLAlchemy", "database session for Unit of Work", "not mocking")))
((("mocking", "don't mock what you don't own")))
@@ -373,6 +441,10 @@ Both of our fakes achieve the same thing: they give us a way to swap out our
persistence layer so we can run tests in memory instead of needing to
talk to a real database. The difference is in the resulting design.
+为什么我们对模拟 UoW 比模拟会话更感到放心?
+我们的两个伪造对象(Fake)实现了相同的目标:为我们提供一种替换持久化层的方式,这样我们可以在内存中运行测试,
+而无需与真实数据库交互。区别在于它们带来了不同的设计结果。
+
If we cared only about writing tests that run quickly, we could create mocks
that replace SQLAlchemy and use those throughout our codebase. The problem is
that `Session` is a complex object that exposes lots of persistence-related
@@ -381,25 +453,38 @@ the database, but that quickly leads to data access code being sprinkled all
over the codebase. To avoid that, we want to limit access to our persistence
layer so each component has exactly what it needs and nothing more.
+如果我们只关心编写运行速度快的测试,那么我们可以创建替代 SQLAlchemy 的模拟对象(mocks),并在整个代码库中使用它们。
+问题在于,`Session` 是一个复杂的对象,它暴露了许多与持久化相关的功能。使用 `Session` 可以随意对数据库进行查询,
+但这很容易导致数据访问代码散布在代码库的各个地方。为了避免这种情况,我们希望限制对持久化层的访问,以保证每个组件只拥有它需要的内容,而不多不少。
+
By coupling to the `Session` interface, you're choosing to couple to all the
complexity of SQLAlchemy. Instead, we want to choose a simpler abstraction and
use that to clearly separate responsibilities. Our UoW is much simpler
than a session, and we feel comfortable with the service layer being able to
start and stop units of work.
+通过耦合到 `Session` 接口,你实际上选择了与 SQLAlchemy 的所有复杂性进行耦合。而我们希望选择一个更简单的抽象,并以此清晰地分离职责。
+我们的 UoW 比 `Session` 简单得多,我们也对服务层能够启动和停止工作单元感到放心。
+
"Don't mock what you don't own" is a rule of thumb that forces us to build
these simple abstractions over messy subsystems. This has the same performance
benefit as mocking the SQLAlchemy session but encourages us to think carefully
about our designs.
((("context manager", "Unit of Work and", startref="ix_ctxtmgr")))
+
+“不要模拟你不拥有的东西”是一条经验法则,它促使我们在混乱的子系统之上构建这些简单的抽象。这不仅与模拟 SQLAlchemy 会话具有相同的性能优势,
+还鼓励我们认真思考我们的设计。
********************************************************************************
=== Using the UoW in the Service Layer
+在服务层中使用工作单元(UoW)
((("Unit of Work pattern", "using UoW in service layer")))
((("service layer", "using Unit of Work in")))
Here's what our new service layer looks like:
+以下是新的服务层代码:
+
[[service_layer_with_uow]]
.Service layer using UoW (src/allocation/service_layer/services.py)
@@ -433,9 +518,11 @@ def allocate(
<1> Our service layer now has only the one dependency,
once again on an _abstract_ UoW.
((("dependencies", "service layer dependency on abstract UoW")))
+我们的服务层现在只有一个依赖,再次依赖于一个_抽象的_UoW。
=== Explicit Tests for Commit/Rollback Behavior
+针对提交/回滚行为的明确测试
((("commits", "explicit tests for")))
((("rollbacks", "explicit tests for")))
@@ -444,6 +531,8 @@ def allocate(
To convince ourselves that the commit/rollback behavior works, we wrote
a couple of tests:
+为让我们确信提交/回滚行为的正常运作,我们编写了几个测试:
+
[[testing_rollback]]
.Integration tests for rollback behavior (tests/integration/test_uow.py)
====
@@ -482,18 +571,26 @@ TIP: We haven't shown it here, but it can be worth testing some of the more
some of the tests to using the real database. It's convenient that our UoW
class makes that easy!
((("databases", "testing transactions against real database")))
+我们在这里没有展示,但测试一些更“晦涩”的数据库行为(比如事务)与“真实”数据库的交互可能是值得的——也就是说,使用相同的引擎。
+目前,我们暂时使用 SQLite 而不是 Postgres,但在 <> 中,我们会将部分测试切换为使用真实数据库。
+很方便的是,我们的 UoW 类让这一切变得简单!
=== Explicit Versus Implicit Commits
+显式提交与隐式提交
((("implicit versus explicit commits")))
((("commits", "explicit versus implicit")))
((("Unit of Work pattern", "explicit versus implicit commits")))
Now we briefly digress on different ways of implementing the UoW pattern.
+现在我们将简要讨论实现 UoW 模式的不同方式。
+
We could imagine a slightly different version of the UoW that commits by default
and rolls back only if it spots an exception:
+我们可以设想一种稍有不同的 UoW 实现,它默认提交,并且仅在发现异常时回滚:
+
[[uow_implicit_commit]]
.A UoW with implicit commit... (src/allocation/unit_of_work.py)
====
@@ -515,11 +612,15 @@ class AbstractUnitOfWork(abc.ABC):
====
<1> Should we have an implicit commit in the happy path?
+我们是否应该在正常路径中使用隐式提交?
<2> And roll back only on exception?
+并仅在发生异常时执行回滚?
It would allow us to save a line of code and to remove the explicit commit from our
client code:
+这将使我们节省一行代码,并从客户端代码中移除显式提交的操作:
+
[[add_batch_nocommit]]
.\...would save us a line of code (src/allocation/service_layer/services.py)
====
@@ -536,17 +637,26 @@ def add_batch(ref: str, sku: str, qty: int, eta: Optional[date], uow):
This is a judgment call, but we tend to prefer requiring the explicit commit
so that we have to choose when to flush state.
+这是一种判断上的选择,但我们倾向于要求显式提交,这样我们就必须明确地选择何时刷新状态。
+
Although we use an extra line of code, this makes the software safe by default.
The default behavior is to _not change anything_. In turn, that makes our code
easier to reason about because there's only one code path that leads to changes
in the system: total success and an explicit commit. Any other code path, any
exception, any early exit from the UoW's scope leads to a safe state.
+尽管我们多用了一行代码,但这使得软件在默认情况下是安全的。默认的行为是_不做任何更改_。反过来,这让我们的代码更容易理解,
+因为只有一条代码路径会导致系统发生更改:完全成功并显式提交。任何其他代码路径、任何异常、任何提前退出 UoW 范围的情况都会导致一个安全的状态。
+
Similarly, we prefer to roll back by default because
it's easier to understand; this rolls back to the last commit,
so either the user did one, or we blow their changes away. Harsh but simple.
+同样地,我们倾向于默认执行回滚,因为这样更容易理解;这会回滚到上一次提交的状态,所以要么用户进行了提交,要么我们就丢弃他们的更改。
+虽然严格,但却简单明了。
+
=== Examples: Using UoW to Group Multiple Operations into an Atomic Unit
+示例:使用 UoW 将多个操作组合成一个原子单元
((("atomic operations", "using Unit of Work to group operations into atomic unit", id="ix_atomops")))
((("Unit of Work pattern", "using UoW to group multiple operations into atomic unit", id="ix_UoWatom")))
@@ -554,14 +664,19 @@ Here are a few examples showing the Unit of Work pattern in use. You can
see how it leads to simple reasoning about what blocks of code happen
together.
+以下是一些展示工作单元(Unit of Work)模式使用的示例。你可以看到它如何让我们能够简单地推理哪些代码块会一同执行。
+
==== Example 1: Reallocate
+示例 1:重新分配
((("Unit of Work pattern", "using UoW to group multiple operations into atomic unit", "reallocate function example")))
((("reallocate service function")))
Suppose we want to be able to deallocate and then reallocate orders:
+假设我们希望能够先取消分配订单,然后重新分配订单:
+
[[reallocate]]
-.Reallocate service function
+.Reallocate service function(重新分配服务函数)
====
[source,python]
[role="skip"]
@@ -581,19 +696,24 @@ def reallocate(
====
<1> If `deallocate()` fails, we don't want to call `allocate()`, obviously.
+显然,如果 `deallocate()` 失败,我们不希望调用 `allocate()`。
<2> If `allocate()` fails, we probably don't want to actually commit
the `deallocate()` either.
+如果 `allocate()` 失败,我们可能也不希望实际提交 `deallocate()` 的操作。
==== Example 2: Change Batch Quantity
+示例 2:更改批次数量
((("Unit of Work pattern", "using UoW to group multiple operations into atomic unit", "changing batch quantity example")))
Our shipping company gives us a call to say that one of the container doors
opened, and half our sofas have fallen into the Indian Ocean. Oops!
+我们的运输公司打电话告诉我们,其中一个集装箱的门打开了,我们一半的沙发掉进了印度洋。糟糕!
+
[[change_batch_quantity]]
-.Change quantity
+.Change quantity(更改数量)
====
[source,python]
[role="skip"]
@@ -615,9 +735,11 @@ def change_batch_quantity(
at any stage, we probably want to commit none of the changes.
((("Unit of Work pattern", "using UoW to group multiple operations into atomic unit", startref="ix_UoWatom")))
((("atomic operations", "using Unit of Work to group operations into atomic unit", startref="ix_atomops")))
+在这里,我们可能需要释放任意数量的行。如果在任何阶段出现失败,我们可能希望不提交任何更改。
=== Tidying Up the Integration Tests
+整理集成测试
((("testing", "Unit of Work with integration tests", "tidying up tests")))
((("Unit of Work pattern", "tidying up integration tests")))
@@ -625,6 +747,8 @@ We now have three sets of tests, all essentially pointing at the database:
_test_orm.py_, _test_repository.py_, and _test_uow.py_. Should we throw any
away?
+我们现在有三组测试,它们本质上都指向数据库:_test_orm.py_、_test_repository.py_ 和 _test_uow.py_。我们应该丢弃其中的某些测试吗?
+
====
[source,text]
[role="tree"]
@@ -653,8 +777,12 @@ it's doing are covered in _test_repository.py_. That last test, you might keep a
but we could certainly see an argument for just keeping everything at the highest
possible level of abstraction (just as we did for the unit tests).
+如果你认为某些测试从长期来看不会带来价值,你完全可以随时将它们删除。我们会说 _test_orm.py_ 主要是帮助我们学习 SQLAlchemy 的工具,
+因此从长期来看我们并不需要它,特别是当它的主要功能已经被 _test_repository.py_ 所覆盖时。而对于最后的那个测试 (_test_uow.py_),
+你可能会选择保留,但我们也完全可以接受只保留尽可能高层次抽象的测试(就像我们对单元测试所做的一样)的观点。
+
[role="nobreakinside less_space"]
-.Exercise for the Reader
+.Exercise for the Reader(读者练习)
******************************************************************************
For this chapter, probably the best thing to try is to implement a
UoW from scratch. The code, as always, is https://github.com/cosmicpython/code/tree/chapter_06_uow_exercise[on GitHub]. You could either follow the model we have quite closely,
@@ -665,101 +793,139 @@ or rollback on exit. If you feel like going all-functional rather than
messing about with all these classes, you could use `@contextmanager` from
`contextlib`.
+对于本章来说,可能最好的尝试是从头实现一个 UoW(工作单元)。
+代码一如既往地可以在 https://github.com/cosmicpython/code/tree/chapter_06_uow_exercise[GitHub 上] 找到。
+您可以选择非常贴近我们现有的示例模型,也可以尝试将 UoW 与上下文管理器分离开来进行实验(UoW 的职责是 `commit()`、`rollback()` 并提供 `.batches` 仓储,
+而上下文管理器的职责是进行初始化,然后在退出时执行提交或回滚操作)。如果您想完全采用函数式的方式,而不是处理这些类,您可以使用 `contextlib` 中的 `@contextmanager`。
+
We've stripped out both the actual UoW and the fakes, as well as paring back
the abstract UoW. Why not send us a link to your repo if you come up with
something you're particularly proud of?
+
+我们已经剥离了实际的工作单元(UoW)和伪对象,同时也简化了抽象工作单元。如果你设计出令自己特别自豪的东西,为什么不将你的仓储链接发送给我们呢?
******************************************************************************
TIP: This is another example of the lesson from <>:
as we build better abstractions, we can move our tests to run against them,
which leaves us free to change the underlying details.
+这是来自<>的一课的另一个例子:当我们构建出更好的抽象时,
+我们可以让测试针对这些抽象运行,这使得我们能够自由地更改底层的细节。
=== Wrap-Up
+总结
((("Unit of Work pattern", "benefits of using")))
Hopefully we've convinced you that the Unit of Work pattern is useful, and
that the context manager is a really nice Pythonic way
of visually grouping code into blocks that we want to happen atomically.
+希望我们已经让你相信,工作单元(Unit of Work)模式是有用的,并且上下文管理器是一种非常优雅的 _Python_ 风格方式,
+可以直观地将我们希望原子化执行的代码分组到块中。
+
((("Session object")))
((("SQLAlchemy", "Session object")))
This pattern is so useful, in fact, that SQLAlchemy already uses a UoW
in the shape of the `Session` object. The `Session` object in SQLAlchemy is the way
that your application loads data from the database.
+事实上,这种模式非常有用,以至于 SQLAlchemy 已经在其 `Session` 对象中实现了一个工作单元(UoW)。在 SQLAlchemy 中,
+`Session` 对象是你的应用程序从数据库加载数据的方式。
+
Every time you load a new entity from the database, the session begins to _track_
changes to the entity, and when the session is _flushed_, all your changes are
persisted together. Why do we go to the effort of abstracting away the SQLAlchemy session if it already implements the pattern we want?
+每次你从数据库加载一个新的实体时,`Session` 会开始 _追踪_ 该实体的更改,而当 `Session` 被 _刷新(flushed)_ 时,
+所有的更改都会被一起持久化。那么,既然 SQLAlchemy 的 `Session` 已经实现了我们想要的模式,为什么我们还要费力地对它进行抽象呢?
+
((("Unit of Work pattern", "pros and cons or trade-offs")))
<> discusses some of the trade-offs.
+<> 讨论了一些权衡取舍。
+
[[chapter_06_uow_tradeoffs]]
[options="header"]
-.Unit of Work pattern: the trade-offs
+.Unit of Work pattern: the trade-offs(工作单元模式:权衡取舍)
|===
-|Pros|Cons
+|Pros(优点)|Cons(缺点)
a|
* We have a nice abstraction over the concept of atomic operations, and the
context manager makes it easy to see, visually, what blocks of code are
grouped together atomically.
((("atomic operations", "Unit of Work as abstraction over")))
((("transactions", "Unit of Work and")))
+我们在原子操作的概念上拥有了一个优雅的抽象,上下文管理器使我们能够直观地看到哪些代码块被归组到了一起以原子方式执行。
* We have explicit control over when a transaction starts and finishes, and our
application fails in a way that is safe by default. We never have to worry
that an operation is partially committed.
+我们对事务的开始和结束有明确的控制,并且我们的应用程序默认情况下能以一种安全的方式失败。我们永远不必担心某个操作只被部分提交。
* It's a nice place to put all your repositories so client code can access them.
+这是一个放置所有仓储的好地方,这样客户端代码就可以访问它们。
* As you'll see in later chapters, atomicity isn't only about transactions; it
can help us work with events and the message bus.
+正如你将在后续章节中看到的,原子性不仅仅与事务有关;它还可以帮助我们处理事件和消息总线。
a|
* Your ORM probably already has some perfectly good abstractions around
atomicity. SQLAlchemy even has context managers. You can go a long way
just passing a session around.
+你的 ORM 可能已经有一些非常好的关于原子性的抽象。SQLAlchemy 甚至提供了上下文管理器。仅仅通过传递一个 session,你也能实现很多功能。
* We've made it look easy, but you have to think quite carefully about
things like rollbacks, multithreading, and nested transactions. Perhaps just
sticking to what Django or Flask-SQLAlchemy gives you will keep your life
simpler.
((("Unit of Work pattern", startref="ix_UoW")))
+虽然我们让这一切看起来很简单,但你必须非常仔细地考虑诸如回滚、多线程以及嵌套事务等问题。
+也许只是坚持使用 Django 或 Flask-SQLAlchemy 提供的功能会让你的生活更简单一些。
|===
For one thing, the Session API is rich and supports operations that we don't
want or need in our domain. Our `UnitOfWork` simplifies the session to its
essential core: it can be started, committed, or thrown away.
+首先,`Session` 的 API 非常丰富,并且支持我们在领域中不需要或不想要的操作。
+而我们的 `UnitOfWork` 将会话简化为其核心本质:它可以被启动、提交或丢弃。
+
For another, we're using the `UnitOfWork` to access our `Repository` objects.
This is a neat bit of developer usability that we couldn't do with a plain
SQLAlchemy `Session`.
+另一方面,我们使用 `UnitOfWork` 来访问我们的 `Repository` 对象。这是一种简洁的开发者易用性设计,
+而这是单纯使用 SQLAlchemy 的 `Session` 无法实现的。
+
[role="nobreakinside less_space"]
-.Unit of Work Pattern Recap
+.Unit of Work Pattern Recap(工作单元模式总结)
*****************************************************************
((("Unit of Work pattern", "recap of important points")))
-The Unit of Work pattern is an abstraction around data integrity::
+The Unit of Work pattern is an abstraction around data integrity(工作单元模式是围绕数据完整性的一种抽象)::
It helps to enforce the consistency of our domain model, and improves
performance, by letting us perform a single _flush_ operation at the
end of an operation.
+它通过允许我们在操作结束时执行一次 _刷新(flush)_ 操作,帮助我们强制维护领域模型的一致性,并提高性能。
-It works closely with the Repository and Service Layer patterns::
+It works closely with the Repository and Service Layer patterns(它与仓储模式和服务层模式紧密协作)::
The Unit of Work pattern completes our abstractions over data access by
representing atomic updates. Each of our service-layer use cases runs in a
single unit of work that succeeds or fails as a block.
+工作单元模式通过表示原子更新来完善我们对数据访问的抽象。我们的每个服务层用例都运行在一个单独的工作单元中,该工作单元要么整体成功,要么整体失败。
-This is a lovely case for a context manager::
+This is a lovely case for a context manager(这正是一个上下文管理器的绝佳应用场景)::
Context managers are an idiomatic way of defining scope in Python. We can use a
context manager to automatically roll back our work at the end of a request,
which means the system is safe by default.
+上下文管理器是定义 _Python_ 中作用域的一种惯用方式。我们可以使用上下文管理器在请求结束时自动回滚我们的工作,这意味着系统默认是安全的。
-SQLAlchemy already implements this pattern::
+SQLAlchemy already implements this pattern(SQLAlchemy 已经实现了这种模式)::
We introduce an even simpler abstraction over the SQLAlchemy `Session` object
in order to "narrow" the interface between the ORM and our code. This helps
to keep us loosely coupled.
+我们在 SQLAlchemy 的 `Session` 对象之上引入了一个更简单的抽象,以便“收窄” ORM 和我们的代码之间的接口。这有助于保持松耦合。
*****************************************************************
@@ -770,6 +936,9 @@ implementation at the outside edge of the system. This lines up nicely with
SQLAlchemy's own
https://oreil.ly/tS0E0[recommendations]:
+最后,我们再次受到依赖倒置原则的推动:我们的服务层依赖于一个精简的抽象,而具体的实现则附加在系统的外围。这与 SQLAlchemy 自身的
+[推荐](https://oreil.ly/tS0E0) 非常契合:
+
[quote, SQLALchemy "Session Basics" Documentation]
____
Keep the life cycle of the session (and usually the transaction) separate and
@@ -777,6 +946,9 @@ external. The most comprehensive approach, recommended for more substantial
applications, will try to keep the details of session, transaction, and
exception management as far as possible from the details of the program doing
its work.
+
+将会话(以及通常是事务)的生命周期分离并置于外部。对于更复杂的应用程序,推荐采用最全面的方法,
+该方法将尽量让会话、事务以及异常管理的细节远离实际程序逻辑的细节。
____
From 707ce4654afa57301e03f5b6b1ee9d10d2450d5f Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Fri, 31 Jan 2025 09:47:29 +0800
Subject: [PATCH 11/75] update Readme.md chapter_07_aggregate.asciidoc
---
Readme.md | 50 ++---
chapter_07_aggregate.asciidoc | 331 ++++++++++++++++++++++++++++++++--
2 files changed, 345 insertions(+), 36 deletions(-)
diff --git a/Readme.md b/Readme.md
index c58867bc..31622a40 100644
--- a/Readme.md
+++ b/Readme.md
@@ -14,31 +14,31 @@ In the meantime, pull requests, typofixes, and more substantial feedback + sugge
O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布本书。
与此同时,我们热情欢迎有关拉取请求、错别字修正以及更深入的反馈与建议。
-| Chapter
章节 | |
-|--------------------------------------------------------------------------------------------------------------------------| ----- |
-| [Preface
前言(已翻译)](preface.asciidoc) | |
-| [Introduction: Why do our designs go wrong?
引言:为什么我们的设计会出问题?(已翻译)](introduction.asciidoc) | ||
-| [**Part 1 Intro**](part1.asciidoc) | |
-| [Chapter 1: Domain Model
第一章:领域模型(已翻译)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 2: Repository
第二章:仓储(已翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(已翻译)](chapter_03_abstractions.asciidoc) | |
-| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(已翻译)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(已翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 6: Unit of Work
第六章:工作单元(已翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 7: Aggregates
第七章:聚合(翻译中...)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [**Part 2 Intro**](part2.asciidoc) | |
-| [Chapter 8: Domain Events and a Simple Message Bus
第八章:领域事件与简单消息总线(未翻译)](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 9: Going to Town on the MessageBus
第九章:深入探讨消息总线(未翻译)](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 10: Commands
第十章:命令(未翻译)](chapter_10_commands.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 11: External Events for Integration
第十一章:集成外部事件(未翻译)](chapter_11_external_events.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 12: CQRS
第十二章:命令查询责任分离(未翻译)](chapter_12_cqrs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 13: Dependency Injection
第十三章:依赖注入(未翻译)](chapter_13_dependency_injection.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Epilogue: How do I get there from here?
尾声:我该如何从这里开始?(未翻译)](epilogue_1_how_to_get_there_from_here.asciidoc) | |
-| [Appendix A: Recap table
附录A:总结表格(未翻译)](appendix_ds1_table.asciidoc) | |
-| [Appendix B: Project Structure
附录B:项目结构(未翻译)](appendix_project_structure.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Appendix C: A major infrastructure change, made easy
附录C:轻松实现重大基础设施更改(未翻译)](appendix_csvs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Appendix D: Django
附录D:Django(未翻译)](appendix_django.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Appendix F: Validation
附录F:验证(未翻译)](appendix_validation.asciidoc) | |
+| Chapter
章节 | |
+|-----------------------------------------------------------------------------------------------------------------------------| ----- |
+| [Preface
前言(已翻译)](preface.asciidoc) | |
+| [Introduction: Why do our designs go wrong?
引言:为什么我们的设计会出问题?(已翻译)](introduction.asciidoc) | ||
+| [**Part 1 Intro
第一部分简介**](part1.asciidoc) | |
+| [Chapter 1: Domain Model
第一章:领域模型(已翻译)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 2: Repository
第二章:仓储(已翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(已翻译)](chapter_03_abstractions.asciidoc) | |
+| [Chapter 4: Service Layer (and Flask API)
第四章:服务层(和 Flask API)(已翻译)](chapter_04_service_layer.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(已翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 6: Unit of Work
第六章:工作单元(已翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 7: Aggregates
第七章:聚合(已翻译)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [**Part 2 Intro
第二部分简介**](part2.asciidoc) | |
+| [Chapter 8: Domain Events and a Simple Message Bus
第八章:领域事件与简单消息总线(翻译中...)](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 9: Going to Town on the MessageBus
第九章:深入探讨消息总线(未翻译)](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 10: Commands
第十章:命令(未翻译)](chapter_10_commands.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 11: External Events for Integration
第十一章:集成外部事件(未翻译)](chapter_11_external_events.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 12: CQRS
第十二章:命令查询责任分离(未翻译)](chapter_12_cqrs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 13: Dependency Injection
第十三章:依赖注入(未翻译)](chapter_13_dependency_injection.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Epilogue: How do I get there from here?
尾声:我该如何从这里开始?(未翻译)](epilogue_1_how_to_get_there_from_here.asciidoc) | |
+| [Appendix A: Recap table
附录A:总结表格(未翻译)](appendix_ds1_table.asciidoc) | |
+| [Appendix B: Project Structure
附录B:项目结构(未翻译)](appendix_project_structure.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Appendix C: A major infrastructure change, made easy
附录C:轻松实现重大基础设施更改(未翻译)](appendix_csvs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Appendix D: Django
附录D:Django(未翻译)](appendix_django.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Appendix F: Validation
附录F:验证(未翻译)](appendix_validation.asciidoc) | |
diff --git a/chapter_07_aggregate.asciidoc b/chapter_07_aggregate.asciidoc
index 593c920e..d60cf39a 100644
--- a/chapter_07_aggregate.asciidoc
+++ b/chapter_07_aggregate.asciidoc
@@ -1,5 +1,6 @@
[[chapter_07_aggregate]]
== Aggregates and Consistency Boundaries
+聚合与一致性边界
((("aggregates", "Product aggregate")))
((("consistency boundaries")))
@@ -12,23 +13,34 @@ discuss the concept of a _consistency boundary_ and show how making it
explicit can help us to build high-performance software without compromising
maintainability.
+在本章中,我们将重新审视我们的领域模型,讨论不变量和约束,并探讨领域对象是如何在概念上以及持久化存储中维护其自身的内部一致性的。
+我们会讨论 _一致性边界_ 的概念,并展示如何通过显式定义一致性边界来帮助我们构建高性能的软件,同时不牺牲可维护性。
+
<> shows a preview of where we're headed: we'll introduce
a new model object called `Product` to wrap multiple batches, and we'll make
the old `allocate()` domain service available as a method on `Product` instead.
+<> 展示了我们前进方向的预览:我们将引入一个名为 `Product` 的新模型对象,用来封装多个批次(batches),
+并且我们会将旧的 `allocate()` 领域服务改为在 `Product` 上作为一个方法提供。
+
[[maps_chapter_06]]
-.Adding the Product aggregate
+.Adding the Product aggregate(新增 Product 聚合)
image::images/apwp_0701.png[]
Why? Let's find out.
+为什么?让我们一探究竟。
+
[TIP]
====
The code for this chapter is in the chapter_07_aggregate branch
https://github.com/cosmicpython/code/tree/chapter_07_aggregate[on [.keep-together]#GitHub#]:
+本章的代码位于 chapter_07_aggregate 分支
+https://github.com/cosmicpython/code/tree/chapter_07_aggregate[在[.keep-together]#GitHub#]:
+
----
git clone https://github.com/cosmicpython/code.git
cd code
@@ -40,35 +52,50 @@ git checkout chapter_06_uow
=== Why Not Just Run Everything in a Spreadsheet?
+为什么不直接在电子表格中运行所有内容?
((("domain model", "using spreadsheets instead of")))
((("spreadsheets, using instead of domain model")))
What's the point of a domain model, anyway? What's the fundamental problem
we're trying to address?
+那么,领域模型的意义究竟是什么?我们试图解决的核心问题是什么呢?
+
Couldn't we just run everything in a spreadsheet? Many of our users would be
[.keep-together]#delighted# by that. Business users _like_ spreadsheets because
they're simple, familiar, and yet enormously powerful.
+难道我们不能直接在电子表格中运行所有内容吗?许多用户会对此感到[.keep-together]#非常高兴#。
+业务用户 _喜欢_ 电子表格,因为它们简单、熟悉,却又极其强大。
+
((("CSV over SMTP architecture")))
In fact, an enormous number of business processes do operate by manually sending
spreadsheets back and forth over email. This "CSV over SMTP" architecture has
low initial complexity but tends not to scale very well because it's difficult
to apply logic and maintain consistency.
+事实上,大量的业务流程确实是通过手动在电子邮件中传递电子表格来运作的。这种“通过 SMTP 传递 CSV”的架构初始复杂性很低,
+但往往难以很好地扩展,因为很难应用逻辑并维护一致性。
+
// IDEA: better examples?
Who is allowed to view this particular field? Who's allowed to update it? What
happens when we try to order –350 chairs, or 10,000,000 tables? Can an employee
have a negative salary?
+谁被允许查看这个特定字段?谁被允许更新它?当我们尝试订购 -350 把椅子或 10,000,000 张桌子时会发生什么?一个员工可以有负数的薪水吗?
+
These are the constraints of a system. Much of the domain logic we write exists
to enforce these constraints in order to maintain the invariants of the
system. The _invariants_ are the things that have to be true whenever we finish
an operation.
+这些是系统的约束条件。我们编写的大量领域逻辑是为了实施这些约束,以保持系统的不变量。
+_不变量_ 是指每当我们完成一次操作时,必须保持为真的那些事情。
+
=== Invariants, Constraints, and Consistency
+不变量、约束与一致性
((("invariants", "invariants, constraints, and consistency")))
((("domain model", "invariants, constraints, and consistency")))
@@ -76,11 +103,15 @@ The two words are somewhat interchangeable, but a _constraint_ is a
rule that restricts the possible states our model can get into, while an _invariant_
is defined a little more precisely as a condition that is always true.
+这两个词在某种程度上可以互换使用,但 _约束_ 是限制我们模型可能进入状态的规则,而 _不变量_ 更准确地被定义为始终为真的条件。
+
((("constraints")))
If we were writing a hotel-booking system, we might have the constraint that double
bookings are not allowed. This supports the invariant that a room cannot have more
than one booking for the same night.
+如果我们正在编写一个酒店预订系统,我们可能会有一个不允许重复预订的约束。这项约束支持了这样一个不变量:同一晚一间房间不能有多个预订。
+
((("consistency")))
Of course, sometimes we might need to temporarily _bend_ the rules. Perhaps we
need to shuffle the rooms around because of a VIP booking. While we're moving
@@ -89,11 +120,18 @@ should ensure that, when we're finished, we end up in a final consistent state,
where the invariants are met. If we can't find a way to accommodate all our guests,
we should raise an error and refuse to complete the operation.
+当然,有时我们可能需要暂时 _打破_ 规则。比如,因为 VIP 预订的原因,我们可能需要调整房间的分配。当我们在内存中移动预订时,
+可能会出现重复预订的情况,但我们的领域模型应该确保在操作完成时,最终会达到一个一致的状态,且所有不变量都得到满足。如果无法找到办法容纳所有的客人,我们应当抛出错误并拒绝完成操作。
+
Let's look at a couple of concrete examples from our business requirements; we'll start with this one:
+让我们来看几个源自业务需求的具体示例;我们从下面这个开始:
+
[quote, The business]
____
An order line can be allocated to only one batch at a time.
+
+一个订单项在同一时间只能分配给一个批次。
____
((("business rules", "invariants, constraints, and consistency")))
@@ -104,15 +142,23 @@ on two different batches for the same line, and currently, there's nothing
there to explicitly stop us from doing that.
+这是一个施加了不变量的业务规则。不变量是指一个订单项要么未分配到任何批次,要么只分配到一个批次,但绝不会超过一个批次。
+我们需要确保代码永远不会意外地对同一个订单项在两个不同的批次上调用 `Batch.allocate()`,而目前没有任何机制能够明确地阻止我们这么做。
+
==== Invariants, Concurrency, and Locks
+不变量、并发与锁
((("business rules", "invariants, concurrency, and locks")))
Let's look at another one of our business rules:
+让我们再来看另一个业务规则:
+
[quote, The business]
____
We can't allocate to a batch if the available quantity is less than the
quantity of the order line.
+
+如果批次的可用数量小于订单项的数量,我们就不能将其分配到该批次。
____
((("invariants", "invariants, concurrency, and locks")))
@@ -122,29 +168,43 @@ physical cushion, for example. Every time we update the state of the system, our
to ensure that we don't break the invariant, which is that the available
quantity must be greater than or equal to zero.
+这里的约束是,我们不能将超过批次可用库存的数量分配出去,以避免超卖库存,例如不会将同一个实际的靠垫分配给两个客户。每次更新系统状态时,
+我们的代码都需要确保不会破坏不变量,而不变量是:可用数量必须大于或等于零。
+
In a single-threaded, single-user application, it's relatively easy for us to
maintain this invariant. We can just allocate stock one line at a time, and
raise an error if there's no stock available.
+在单线程、单用户的应用程序中,维护这个不变量相对来说是比较容易的。我们只需一次分配一条订单项,如果没有足够的可用库存,就抛出一个错误即可。
+
((("concurrency")))
This gets much harder when we introduce the idea of _concurrency_. Suddenly we
might be allocating stock for multiple order lines simultaneously. We might
even be allocating order lines at the same time as processing changes to the
batches [.keep-together]#themselves#.
+当我们引入 _并发_ 的概念时,事情就变得困难得多了。突然之间,我们可能会同时为多个订单项分配库存。
+我们甚至可能在分配订单项的同时处理批次[.keep-together]#本身#的变更。
+
((("locks on database tables")))
We usually solve this problem by applying _locks_ to our database tables. This
prevents two operations from happening simultaneously on the same row or same
table.
+我们通常通过对数据库表应用 _锁_ 来解决这个问题。这可以防止两个操作在同一行或同一表上同时发生。
+
As we start to think about scaling up our app, we realize that our model
of allocating lines against all available batches may not scale. If we process
tens of thousands of orders per hour, and hundreds of thousands of
order lines, we can't hold a lock over the whole `batches` table for
every single one--we'll get deadlocks or performance problems at the very least.
+当我们开始考虑扩大应用程序的规模时,我们会意识到,将订单项分配到所有可用批次的这种模型可能无法扩展。
+如果我们每小时处理数万个订单和数十万个订单项,我们无法在每次操作时对整个 `batches` 表加锁——这样做至少会导致死锁或性能问题。
+
=== What Is an Aggregate?
+什么是聚合?
((("aggregates", "about")))
((("concurrency", "allowing for greatest degree of")))
@@ -155,20 +215,31 @@ system but allow for the greatest degree of concurrency. Maintaining our
invariants inevitably means preventing concurrent writes; if multiple users can
allocate `DEADLY-SPOON` at the same time, we run the risk of overallocating.
+好的,那么如果我们每次想分配一个订单项时都无法锁住整个数据库,那我们应该怎么做呢?我们希望保护系统的不变量,同时允许尽可能高的并发性。
+维护不变量不可避免地意味着要防止并发写操作;如果多个用户可以同时分配 `DEADLY-SPOON`,我们就面临着超额分配的风险。
+
On the other hand, there's no reason we can't allocate `DEADLY-SPOON` at the
same time as `FLIMSY-DESK`. It's safe to allocate two products at the
same time because there's no invariant that covers them both. We don't need them
to be consistent with each other.
+另一方面,我们完全可以在分配 `DEADLY-SPOON` 的同时分配 `FLIMSY-DESK`。同时分配两个产品是安全的,
+因为没有不变量将这两个产品关联在一起。我们不需要它们彼此之间保持一致性。
+
((("Aggregate pattern")))
((("domain driven design (DDD)", "Aggregate pattern")))
The _Aggregate_ pattern is a design pattern from the DDD community that helps us
to resolve this tension. An _aggregate_ is just a domain object that contains
other domain objects and lets us treat the whole collection as a single unit.
+_Aggregate_(聚合)模式是来自领域驱动设计(DDD)社区的一种设计模式,可帮助我们解决这种矛盾。
+_聚合_ 只是一个包含其他领域对象的领域对象,并允许我们将整个集合视为一个单元来处理。
+
The only way to modify the objects inside the aggregate is to load the whole
thing, and to call methods on the aggregate itself.
+修改聚合内部对象的唯一方法是加载整个聚合,并调用聚合自身的方法。
+
((("collections")))
As a model gets more complex and grows more entity and value objects,
referencing each other in a tangled graph, it can be hard to keep track of who
@@ -178,6 +249,10 @@ the single entrypoint for modifying their related objects. It makes the system
conceptually simpler and easy to reason about if you nominate some objects to be
in charge of consistency for the others.
+随着模型变得越来越复杂并增加更多实体和值对象,这些对象之间可能会通过一个纠缠不清的图互相引用,这使得追踪谁可以修改什么变得困难。
+尤其是当模型中包含 _集合_(如我们的批次是一个集合)时,指定某些实体作为唯一的入口来修改与其相关的对象是一个好主意。
+如果指定某些对象负责其他对象的一致性,那么系统的概念会变得更加简单,也更容易推理。
+
For example, if we're building a shopping site, the Cart might make a good
aggregate: it's a collection of items that we can treat as a single unit.
Importantly, we want to load the entire basket as a single blob from our data
@@ -185,28 +260,42 @@ store. We don't want two requests to modify the basket at the same time, or we
run the risk of weird concurrency errors. Instead, we want each change to the
basket to run in a single database transaction.
+例如,如果我们在构建一个购物网站,那么购物车(Cart)可能是一个很好的聚合:它是一个可以作为单一单元处理的商品集合。
+重要的是,我们希望将整个购物车作为一个整体从数据存储中加载。我们不希望两个请求同时修改购物车,否则可能会导致奇怪的并发错误。
+相反,我们希望对购物车的每一次修改都在一次单独的数据库事务中运行。
+
((("consistency boundaries")))
We don't want to modify multiple baskets in a transaction, because there's no
use case for changing the baskets of several customers at the same time. Each
basket is a single _consistency boundary_ responsible for maintaining its own
invariants.
+我们不希望在一个事务中修改多个购物车,因为没有同时更改多个客户购物车的用例。每个购物车是一个单独的 _一致性边界_,负责维护其自身的不变量。
+
[quote, Eric Evans, Domain-Driven Design blue book]
____
An AGGREGATE is a cluster of associated objects that we treat as a unit for the
purpose of data changes.
((("Evans, Eric")))
+
+AGGREGATE(聚合)是一些相关对象的集合,我们将其视为一个单元以进行数据更改。
____
Per Evans, our aggregate has a root entity (the Cart) that encapsulates access
to items. Each item has its own identity, but other parts of the system will always
refer to the Cart only as an indivisible whole.
+根据 Evans 的定义,我们的聚合有一个根实体(购物车),它封装了对物品的访问。每个物品都有自己的标识,
+但系统的其他部分将始终将购物车视为一个不可分割的整体进行引用。
+
TIP: Just as we sometimes use pass:[_leading_underscores] to mark methods or functions
as "private," you can think of aggregates as being the "public" classes of our
model, and the rest of the entities and value objects as "private."
+就像我们有时使用 pass:[_前导下划线] 来标记方法或函数为“私有”一样,您可以将聚合视为我们模型中的“公共”类,
+而将其他实体和值对象视为“私有”。
=== Choosing an Aggregate
+选择一个聚合
((("performance", "impact of using aggregates")))
((("aggregates", "choosing an aggregrate", id="ix_aggch")))
@@ -217,35 +306,55 @@ software and prevent weird race issues. We want to draw a boundary around a
small number of objects—the smaller, the better, for performance—that have to
be consistent with one another, and we need to give this boundary a good name.
+在我们的系统中应该选择哪个聚合呢?这个选择在某种程度上是任意的,但却非常重要。聚合将成为我们确保每个操作以一致状态结束的边界。
+这有助于我们更好地理解软件并防止奇怪的竞态问题。我们希望围绕一小部分必须彼此保持一致的对象划定边界——对象越少越好,
+以提高性能——并且我们需要为这个边界起一个合适的名字。
+
((("batches", "collection of")))
The object we're manipulating under the covers is `Batch`. What do we call a
collection of batches? How should we divide all the batches in the system into
discrete islands of consistency?
+我们在底层操作的对象是 `Batch`。那我们该如何称呼一组批次呢?我们又该如何将系统中的所有批次划分为一些独立的一致性单元呢?
+
We _could_ use `Shipment` as our boundary. Each shipment contains several
batches, and they all travel to our warehouse at the same time. Or perhaps we
could use `Warehouse` as our boundary: each warehouse contains many batches,
and counting all the stock at the same time could make sense.
+我们 _可以_ 使用 `Shipment`(货运)作为边界。每个货运包含多个批次,它们会同时运送到我们的仓库。
+或者,我们也可以使用 `Warehouse`(仓库)作为边界:每个仓库包含许多批次,同时统计所有库存可能是合理的选择。
+
Neither of these concepts really satisfies us, though. We should be able to
allocate `DEADLY-SPOONs` or `FLIMSY-DESKs` in one go, even if they're not in the
same warehouse or the same shipment. These concepts have the wrong granularity.
+然而,这些概念都无法真正满足我们的需求。我们应该能够一次性分配 `DEADLY-SPOON` 或 `FLIMSY-DESK`,即使它们不在同一个仓库或同一个货运中。
+这些概念的粒度并不合适。
+
When we allocate an order line, we're interested only in batches
that have the same SKU as the order line. Some sort of concept like
`GlobalSkuStock` could work: a collection of all the batches for a given SKU.
+当我们分配一个订单项时,我们只关心与该订单项有相同 SKU 的批次。一种像 `GlobalSkuStock` 的概念可能会奏效:即给定 SKU 的所有批次的集合。
+
It's an unwieldy name, though, so after some bikeshedding via `SkuStock`, `Stock`,
`ProductStock`, and so on, we decided to simply call it `Product`—after all,
that was the first concept we came across in our exploration of the
domain language back in <>.
+不过,这个名字略显笨拙,所以经过一番关于 `SkuStock`、`Stock`、`ProductStock` 等名称的讨论后,我们最终决定简单地称它为 `Product`——毕竟,
+这是我们在探索领域语言时最早接触到的概念之一,早在 <> 中就已经提到过了。
+
((("allocate service", "allocating against all batches with")))
((("batches", "allocating against all batches using domain service")))
So the plan is this: when we want to allocate an order line, instead of
<>, where we look up all the `Batch` objects in
the world and pass them to the `allocate()` domain service...
+所以计划是这样的:当我们想要分配一个订单项时,与其采用 <> 中的方式,
+即查找系统中所有的 `Batch` 对象并将它们传递给 `allocate()` 领域服务……
+
[role="width-60"]
[[before_aggregates_diagram]]
.Before: allocate against all batches using the domain service
@@ -300,6 +409,9 @@ allocate --> allocate_domain_service: allocate(orderline, batches)
of all the batches _for that SKU_, and we can call a `.allocate()` method on that
instead.
+...我们将进入 <> 所描述的世界,在这个世界中,每个订单项的特定 SKU 会对应一个新的 `Product` 对象,
+它负责该 SKU 的所有批次。然后,我们可以直接在这个对象上调用 `.allocate()` 方法。
+
[role="width-75"]
[[after_aggregates_diagram]]
.After: ask Product to allocate against its batches
@@ -350,6 +462,8 @@ Product o- Batch: has
((("Product object", "code for")))
Let's see how that looks in code form:
+让我们看看这在代码中的样子:
+
[role="pagebreak-before"]
[[product_aggregate]]
.Our chosen aggregate, Product (src/allocation/domain/model.py)
@@ -373,12 +487,15 @@ class Product:
====
<1> ``Product``'s main identifier is the `sku`.
+`Product` 的主要标识符是 `sku`。
<2> Our `Product` class holds a reference to a collection of `batches` for that SKU.
((("allocate service", "moving to be a method on Product aggregate")))
+我们的 `Product` 类保存了对该 SKU 的 `batches` 集合的引用。
<3> Finally, we can move the `allocate()` domain service to
be a method on the [.keep-together]#`Product`# aggregate.
+最后,我们可以将 `allocate()` 领域服务转移为 [.keep-together]#`Product`# 聚合上的一个方法。
// IDEA (hynek): random nitpick: exceptions denoting errors should be
// named *Error. Are you doing this to save space in the listing?
@@ -395,17 +512,21 @@ NOTE: This `Product` might not look like what you'd expect a `Product`
of a product in one app can be very different from another.
See the following sidebar for more discussion.
((("bounded contexts", "product concept and")))
+这个 `Product` 可能看起来不像您期望的那种 `Product` 模型。没有价格、没有描述、没有尺寸。而我们的分配服务并不关心这些东西。
+这正是限界上下文(bounded contexts)的力量;一个应用程序中的产品概念可以与另一个应用程序中的产品概念非常不同。请参阅以下侧栏获取更多讨论。
[role="nobreakinside less_space"]
[[bounded_contexts_sidebar]]
-.Aggregates, Bounded Contexts, and Microservices
+.Aggregates, Bounded Contexts, and Microservices(聚合、限界上下文和微服务)
*******************************************************************************
((("bounded contexts")))
One of the most important contributions from Evans and the DDD community
is the concept of
https://martinfowler.com/bliki/BoundedContext.html[_bounded contexts_].
+Evans 和 DDD 社区最重要的贡献之一是 https://martinfowler.com/bliki/BoundedContext.html[_限界上下文_] 的概念。
+
((("domain driven design (DDD)", "bounded contexts")))
In essence, this was a reaction against attempts to capture entire businesses
into a single model. The word _customer_ means different things to people
@@ -417,30 +538,46 @@ all the use cases, it's better to have several models, draw boundaries
around each context, and handle the translation between different contexts
explicitly.
+本质上,这是一种对试图将整个业务捕获到一个单一模型中的做法的反应。_客户_ 这个词对于销售、客户服务、物流、技术支持等人员来说有着不同的含义。
+在一个上下文中需要的属性在另一个上下文中可能毫无意义;更麻烦的是,同样的术语在不同的上下文中可能有完全不同的意义。
+与其试图构建一个单一模型(或类,或数据库)以满足所有用例,不如为不同的用例构建多个模型,为每个上下文划定边界,并显式地处理不同上下文之间的转换。
+
((("microservices", "bounded contexts and")))
This concept translates very well to the world of microservices, where each
microservice is free to have its own concept of "customer" and its own rules for
translating that to and from other microservices it integrates with.
+这个概念非常适合应用于微服务的世界。在微服务中,每个微服务都可以拥有它自己对“客户”的定义,以及其自身的规则来处理它与其他微服务之间的转换。
+
In our example, the allocation service has `Product(sku, batches)`,
whereas the ecommerce will have `Product(sku, description, price, image_url,
dimensions, etc...)`. As a rule of thumb, your domain models should
include only the data that they need for performing calculations.
+在我们的示例中,分配服务的模型是 `Product(sku, batches)`,
+而电商系统的模型可能是 `Product(sku, description, price, image_url, dimensions, etc...)`。
+通常来说,您的领域模型应仅包含它们执行计算所需的数据。
+
Whether or not you have a microservices architecture, a key consideration
in choosing your aggregates is also choosing the bounded context that they
will operate in. By restricting the context, you can keep your number of
aggregates low and their size manageable.
+无论您是否采用微服务架构,选择聚合时的一个关键考虑因素是选择它们将要运行的限界上下文。通过限制上下文,您可以减少聚合的数量,并使其规模易于管理。
+
((("aggregates", "choosing an aggregrate", startref="ix_aggch")))
Once again, we find ourselves forced to say that we can't give this issue
the treatment it deserves here, and we can only encourage you to read up on it
elsewhere. The Fowler link at the start of this sidebar is a good starting point, and either
(or indeed, any) DDD book will have a chapter or more on bounded contexts.
+再一次,我们不得不说,无法在这里对这一主题进行应有的深入讨论,我们只能鼓励您在其他地方深入阅读。
+此侧栏开头提供的 Fowler 链接是一个不错的起点,任何一本(或者确切地说,任何)DDD 书籍中都会有一章或更多章节专门讨论限界上下文。
+
*******************************************************************************
=== One Aggregate = One Repository
+一个聚合 = 一个仓储
((("aggregates", "one aggregrate = one repository")))
((("repositories", "one aggregrate = one repository")))
@@ -449,14 +586,20 @@ that they are the only entities that are publicly accessible to the outside
world. In other words, the only repositories we are allowed should be
repositories that return aggregates.
+一旦您将某些实体定义为聚合,我们就需要遵循一个规则:它们是唯一对外部世界公开访问的实体。
+换句话说,我们唯一允许的仓储应该是那些返回聚合的仓储。
+
NOTE: The rule that repositories should only return aggregates is the main place
where we enforce the convention that aggregates are the only way into our
domain model. Be wary of breaking it!
+仓储只应返回聚合的这一规则是我们强制执行“聚合是进入领域模型唯一途径”这一约定的主要方式。请谨慎打破这一规则!
((("Unit of Work pattern", "UoW and product repository")))
((("ProductRepository object")))
In our case, we'll switch from `BatchRepository` to `ProductRepository`:
+在我们的例子中,我们将从使用 `BatchRepository` 切换为使用 `ProductRepository`:
+
[[new_uow_and_repository]]
.Our new UoW and repository (unit_of_work.py and repository.py)
@@ -490,6 +633,9 @@ pattern means we don't have to worry about that yet. We can just use
our `FakeRepository` and then feed through the new model into our service
layer to see how it looks with `Product` as its main entrypoint:
+ORM 层需要进行一些调整,以便正确的批次能够自动加载并关联到 `Product` 对象上。值得庆幸的是,仓储模式让我们暂时无需担心这些问题。
+我们可以直接使用我们的 `FakeRepository`,然后将新模型传递到服务层,来看看以 `Product` 作为主要入口点时的表现:
+
[[service_layer_uses_products]]
.Service layer (src/allocation/service_layer/services.py)
====
@@ -524,6 +670,7 @@ def allocate(
====
=== What About Performance?
+那么性能如何呢?
((("performance", "impact of using aggregates")))
((("aggregates", "performance and")))
@@ -532,20 +679,31 @@ to have high-performance software, but here we are loading _all_ the batches whe
we only need one. You might expect that to be inefficient, but there are a few
reasons why we're comfortable here.
+我们已经多次提到,使用聚合建模是因为我们想要构建高性能的软件。但现在我们在只需要一个批次时却加载了 _所有_ 的批次。
+您可能会觉得这样做效率不高,但这里有几个理由让我们对此感到放心。
+
First, we're purposefully modeling our data so that we can make a single
query to the database to read, and a single update to persist our changes. This
tends to perform much better than systems that issue lots of ad hoc queries. In
systems that don't model this way, we often find that transactions slowly
get longer and more complex as the software evolves.
+首先,我们有意对数据进行建模,以便能够通过单一查询从数据库读取数据,并通过单次更新来持久化我们的更改。
+这种方式的性能通常远胜于那些发出大量临时查询的系统。在未按这种方式建模的系统中,我们经常发现事务随着软件的发展会变得越来越长、越来越复杂。
+
Second, our data structures are minimal and comprise a few strings and
integers per row. We can easily load tens or even hundreds of batches in a few
milliseconds.
+其次,我们的数据结构是极简的,每行仅包含少量字符串和整数。我们可以轻松地在几毫秒内加载数十甚至数百个批次。
+
Third, we expect to have only 20 or so batches of each product at a time.
Once a batch is used up, we can discount it from our calculations. This means
that the amount of data we're fetching shouldn't get out of control over time.
+第三,我们预计每种产品同时只有大约 20 个批次。一旦某个批次被用完,就可以将其从我们的计算中排除。
+这意味着我们获取的数据量不会随着时间的推移而失控。
+
If we _did_ expect to have thousands of active batches for a product, we'd have
a couple of options. For one, we could use lazy-loading for the batches in a
product. From the perspective of our code, nothing would change, but in the
@@ -553,30 +711,45 @@ background, SQLAlchemy would page through data for us. This would lead to more
requests, each fetching a smaller number of rows. Because we need to find only a
single batch with enough capacity for our order, this might work pretty well.
+如果我们 _确实_ 预计某个产品会有数千个活动批次,我们会有几个选项可供选择。例如,我们可以对产品中的批次使用延迟加载(lazy-loading)。
+从我们代码的角度来看,这不会引起任何变化,但在后台,SQLAlchemy 会为我们分页加载数据。这将导致多次请求,每次请求获取较少的行数。
+因为我们只需要找到一个能够满足订单容量的批次,这种方法可能会非常有效。
+
[role="nobreakinside less_space"]
-.Exercise for the Reader
+.Exercise for the Reader(读者练习)
******************************************************************************
((("aggregates", "exercise for the reader")))
You've just seen the main top layers of the code, so this shouldn't be too hard,
but we'd like you to implement the `Product` aggregate starting from `Batch`,
just as we did.
+你刚刚看到了代码的主要顶层结构,所以这应该不会太难。我们希望你从`Batch`开始实现`Product`聚合,就像我们做的一样。
+
Of course, you could cheat and copy/paste from the previous listings, but even
if you do that, you'll still have to solve a few challenges on your own,
like adding the model to the ORM and making sure all the moving parts can
talk to each other, which we hope will be instructive.
+当然,你可以通过复制/粘贴之前的代码清单来“作弊”,但即使这样,你仍然需要自行解决一些挑战,
+比如将模型添加到 ORM 中,并确保所有组件能够相互通信。我们希望这些步骤对你有所启发。
+
You'll find the code https://github.com/cosmicpython/code/tree/chapter_07_aggregate_exercise[on GitHub].
We've put in a "cheating" implementation in the delegates to the existing
`allocate()` function, so you should be able to evolve that toward the real
thing.
+你可以在 https://github.com/cosmicpython/code/tree/chapter_07_aggregate_exercise[GitHub上] 找到代码。
+我们在委托中放入了一个“作弊”的实现,委托给了现有的 `allocate()` 函数,所以你应该能够将其逐步完善为真正的实现。
+
((("pytest", "@pytest.skip")))
We've marked a couple of tests with `@pytest.skip()`. After you've read the
rest of this chapter, come back to these tests to have a go at implementing
version numbers. Bonus points if you can get SQLAlchemy to do them for you by
magic!
+我们使用 `@pytest.skip()` 标记了几个测试。在你阅读完本章的剩余部分后,可以回过头来尝试实现版本号。
+如果你能让 SQLAlchemy 魔法般地为你完成这些工作,那就额外加分!
+
******************************************************************************
If all else failed, we'd just look for a different aggregate. Maybe we could
@@ -586,8 +759,13 @@ to help manage some technical constraints around consistency and performance.
There isn't _one_ correct aggregate, and we should feel comfortable changing our
minds if we find our boundaries are causing performance woes.
+如果其他方法都失败了,我们可以尝试寻找一个不同的聚合方式。也许我们可以按照区域或仓储来划分批次,或者围绕发货的概念重新设计我们的数据访问策略。
+聚合模式的目的是帮助应对一致性和性能相关的一些技术约束。并不存在 _唯一_ 正确的聚合方式,如果我们发现定义的边界导致性能问题,
+我们应该随时调整思路,不拘泥于现有方案。
+
=== Optimistic Concurrency with Version Numbers
+使用版本号的乐观并发控制
((("concurrency", "optimistic concurrency with version numbers", id="ix_concopt")))
((("optimistic concurrency with version numbers", id="ix_opticonc")))
@@ -596,17 +774,23 @@ We have our new aggregate, so we've solved the conceptual problem of choosing
an object to be in charge of consistency boundaries. Let's now spend a little
time talking about how to enforce data integrity at the database level.
+我们已经有了新的聚合,因此解决了选择负责一致性边界对象的概念性问题。现在,让我们花点时间讨论如何在数据库层面强制执行数据完整性。
+
NOTE: This section has a lot of implementation details; for example, some of it
is Postgres-specific. But more generally, we're showing one way of managing
concurrency issues, but it is just one approach. Real requirements in this
area vary a lot from project to project. You shouldn't expect to be able to
copy and paste code from here into production.
((("PostgreSQL", "managing concurrency issues")))
+本节包含许多实现细节,例如,其中一些是特定于 Postgres 的。但更普遍来说,我们展示了一种管理并发问题的方法,不过这仅仅是一种方法。
+实际需求在这一领域因项目而异。因此,你不应该期望能够将这里的代码直接复制粘贴到生产环境中使用。
((("locks on database tables", "optimistic locking")))
We don't want to hold a lock over the entire `batches` table, but how will we
implement holding a lock over just the rows for a particular SKU?
+我们不希望对整个 `batches` 表持有锁,但我们将如何实现仅对特定 SKU 的行持有锁呢?
+
((("version numbers", "in the products table, implementing optimistic locking")))
One answer is to have a single attribute on the `Product` model that acts as a marker for
the whole state change being complete and to use it as the single resource
@@ -616,6 +800,10 @@ the `allocations` tables, we force both to also try to update the
`version_number` in the `products` table, in such a way that only one of them
can win and the world stays consistent.
+一个解决方法是在 `Product` 模型上设置一个单一属性,用作整个状态变更完成的标记,并将其作为并发工作者争用的唯一资源。
+如果两个事务同时读取了 `batches` 的状态,并且都试图更新 `allocations` 表,
+我们可以强制它们同时尝试更新 `products` 表中的 `version_number`,以确保只有其中一个能成功,保持系统的一致性。
+
((("transactions", "concurrent, attempting update on Product")))
((("Product object", "two transactions attempting concurrent update on")))
<> illustrates two concurrent
@@ -625,11 +813,17 @@ in order to modify a state. But we set up our database integrity
rules such that only one of them is allowed to `commit` the new `Product`
with `version=4`, and the other update is rejected.
+<> 图解说明了两个并发事务同时进行读取操作,因此它们会看到一个 `Product`,例如,`version=3`。
+它们都会调用 `Product.allocate()` 来修改状态。但我们设置了数据库完整性规则,
+以确保只有其中一个事务被允许 `commit` 带有 `version=4` 的新 `Product`,而另一个更新会被拒绝。
+
TIP: Version numbers are just one way to implement optimistic locking. You
could achieve the same thing by setting the Postgres transaction isolation
level to `SERIALIZABLE`, but that often comes at a severe performance cost.
Version numbers also make implicit concepts explicit.
((("PostgreSQL", "SERIALIZABLE transaction isolation level")))
+版本号只是实现乐观锁的一种方式。你也可以通过将 Postgres 的事务隔离级别设置为 `SERIALIZABLE` 来实现相同的效果,
+但这样往往会带来严重的性能开销。而版本号则能将隐含的概念显式化。
[[version_numbers_sequence_diagram]]
.Sequence diagram: two transactions attempting a concurrent update on [.keep-together]#`Product`#
@@ -664,7 +858,7 @@ Database -[#red]>x Transaction2: Error! version is already 4
[role="nobreakinside less_space"]
-.Optimistic Concurrency Control and Retries
+.Optimistic Concurrency Control and Retries(乐观并发控制和重试)
********************************************************************************
What we've implemented here is called _optimistic_ concurrency control because
@@ -673,6 +867,9 @@ make changes to the database. We think it's unlikely that they will conflict
with each other, so we let them go ahead and just make sure we have a way to
notice if there is a [.keep-together]#problem#.
+我们在这里实现的被称为 _乐观_ 并发控制,因为我们的默认假设是,当两个用户想要对数据库进行修改时,一切都会正常进行。
+我们认为他们发生冲突的可能性很低,因此我们允许他们继续操作,只需确保我们有办法注意到是否存在[.keep-together]#问题#。
+
((("pessimistic concurrency")))
((("locks on database tables", "pessimistic locking")))
((("SELECT FOR UPDATE statement")))
@@ -683,12 +880,19 @@ the whole `batches` table, or using ++SELECT FOR UPDATE++—we're pretending
that we've ruled those out for performance reasons, but in real life you'd
want to do some evaluations and measurements of your own.
+_悲观_ 并发控制基于以下假设:两个用户会引发冲突,因此我们希望在所有情况下都防止冲突发生,于是锁定所有内容以确保安全。
+在我们的示例中,这将意味着锁定整个 `batches` 表,或者使用 ++SELECT FOR UPDATE++。我们假设由于性能原因已经排除了这些选项,
+但在实际情况下,你可能需要进行一些评估和测量来决定最佳方案。
+
((("locks on database tables", "optimistic locking")))
With pessimistic locking, you don't need to think about handling failures
because the database will prevent them for you (although you do need to think
about deadlocks). With optimistic locking, you need to explicitly handle
the possibility of failures in the (hopefully unlikely) case of a clash.
+使用悲观锁定时,你无需考虑处理失败的情况,因为数据库会为你防止这些失败(不过你需要考虑死锁问题)。而使用乐观锁定时,
+你需要显式地处理在(希望是低概率的)冲突情况下可能出现的失败情况。
+
((("retries", "optimistic concurrency control and")))
The usual way to handle a failure is to retry the failed operation from the
beginning. Imagine we have two customers, Harry and Bob, and each submits an order
@@ -699,28 +903,44 @@ version 2 and tries to allocate again. If there is enough stock left, all is
well; otherwise, he'll receive `OutOfStock`. Most operations can be retried this
way in the case of a concurrency problem.
+处理失败的常见方式是从头开始重试失败的操作。想象一下,有两位客户,Harry 和 Bob,他们各自提交了一个 `SHINY-TABLE` 的订单。
+两个线程都加载了版本为 1 的产品并分配了库存。数据库阻止了并发更新,结果 Bob 的订单因为错误而失败。当我们 _重试_ 操作时,
+Bob 的订单会加载版本为 2 的产品并再次尝试分配。如果还有足够的库存,一切就会正常完成;否则,他将收到 `OutOfStock` 的通知。
+在大多数情况下,如果出现并发问题,操作都可以通过这种方式进行重试。
+
Read more on retries in <> and <>.
+
+关于重试的更多内容,请参阅 <> 和 <>。
********************************************************************************
==== Implementation Options for Version Numbers
+实现版本号的选项
+
((("Product object", "version numbers implemented on")))
((("version numbers", "implementation options for")))
There are essentially three options for implementing version numbers:
+实现版本号本质上有三种选项:
+
1. `version_number` lives in the domain; we add it to the `Product` constructor,
and `Product.allocate()` is responsible for incrementing it.
+`version_number` 存在于领域中;我们将其添加到 `Product` 构造函数中,并由 `Product.allocate()` 负责对其进行递增。
2. The service layer could do it! The version number isn't _strictly_ a domain
concern, so instead our service layer could assume that the current version number
is attached to `Product` by the repository, and the service layer will increment it
before it does the `commit()`.
+服务层也可以负责!版本号并不是 _严格_ 的领域关注点,因此我们的服务层可以假设当前版本号是由仓储附加到 `Product` 上的,
+而服务层会在执行 `commit()` 之前递增它。
3. Since it's arguably an infrastructure concern, the UoW and repository
could do it by magic. The repository has access to version numbers for any
products it retrieves, and when the UoW does a commit, it can increment the
version number for any products it knows about, assuming them to have changed.
+由于可以说版本号是一个基础设施层的关注点,UoW(工作单元)和仓储可以通过“魔法”来实现它。仓储能够访问它检索到的任何产品的版本号,
+而当 UoW 执行 `commit` 时,它可以对它已知的任何产品的版本号进行递增,假设这些产品已经发生了更改。
Option 3 isn't ideal, because there's no real way of doing it without having to
assume that _all_ products have changed, so we'll be incrementing version numbers
@@ -728,12 +948,19 @@ when we don't have to.footnote:[Perhaps we could get some ORM/SQLAlchemy magic t
us when an object is dirty, but how would that work in the generic case—for example, for a
`CsvRepository`?]
+选项3并不理想,因为没有实际的方式可以实现它而不假设 _所有_ 的产品都已被更改,因此我们会在不需要的情况下递增版本号。
+脚注:[或许我们可以借助一些 ORM/SQLAlchemy 的魔法来告诉我们对象何时被修改,但在通用情况下这又该如何工作呢——例如对于一个 `CsvRepository`?]
+
Option 2 involves mixing the responsibility for mutating state between the service
layer and the domain layer, so it's a little messy as well.
+选项2将状态变更的职责混合到了服务层和领域层之间,因此也有点混乱。
+
So in the end, even though version numbers don't _have_ to be a domain concern,
you might decide the cleanest trade-off is to put them in the domain:
+因此,最终,即使版本号不 _一定_ 是领域的关注点,你可能会决定最干净的权衡是将它们放入领域中:
+
[[product_aggregate_with_version_number]]
.Our chosen aggregate, Product (src/allocation/domain/model.py)
====
@@ -757,6 +984,7 @@ class Product:
====
<1> There it is!
+就是这样!
TIP: If you're scratching your head at this version number business, it might
help to remember that the _number_ isn't important. What's important is
@@ -767,9 +995,12 @@ TIP: If you're scratching your head at this version number business, it might
((("concurrency", "optimistic concurrency with version numbers", startref="ix_concopt")))
((("optimistic concurrency with version numbers", startref="ix_opticonc")))
((("aggregates", "optimistic concurrency with version numbers", startref="ix_aggopticon")))
+如果你对这个版本号的概念感到困惑,记住这一点可能会有所帮助:_版本号本身并不重要_。重要的是,每当我们对 `Product` 聚合进行修改时,
+`Product` 数据库行都会被更新。版本号是一种简单且易于理解的方式,用来表示每次写操作都会发生变化的事物,但它同样也可以是每次生成的随机 UUID。
=== Testing for Our Data Integrity Rules
+测试我们的数据完整性规则
((("data integrity", "testing for", id="ix_daint")))
((("aggregates", "testing for data integrity rules", id="ix_aggtstdi")))
@@ -778,6 +1009,8 @@ Now to make sure we can get the behavior we want: if we have two
concurrent attempts to do allocation against the same `Product`, one of them
should fail, because they can't both update the version number.
+现在要确保我们能够获得所需的行为:如果有两个并发操作试图对同一个 `Product` 进行分配,其中一个操作应该失败,因为它们无法同时更新版本号。
+
((("time.sleep function")))
((("time.sleep function", "reproducing concurrency behavior with")))
((("concurrency", "reproducing behavior with time.sleep function")))
@@ -788,6 +1021,9 @@ in our use case, but it's not the most reliable or efficient way to reproduce
concurrency bugs. Consider using semaphores or similar synchronization primitives
shared between your threads to get better guarantees of behavior.]
+首先,让我们通过一个函数来模拟一个“慢”事务,该函数会先进行分配操作,然后显式地调用 sleep:脚注:[在我们的用例中,`time.sleep()` 很有效,
+但它并不是重现并发错误最可靠或最高效的方法。可以考虑使用信号量(semaphores)或类似的线程间同步原语,以更好地保证行为的一致性。]
+
[[time_sleep_thread]]
.time.sleep can reproduce concurrency behavior (tests/integration/test_uow.py)
====
@@ -813,6 +1049,8 @@ def try_to_allocate(orderid, sku, exceptions):
Then we have our test invoke this slow allocation twice, concurrently, using
threads:
+然后,我们的测试会使用线程同时调用这个慢速分配函数两次:
+
[[data_integrity_test]]
.An integration test for concurrency behavior (tests/integration/test_uow.py)
====
@@ -858,23 +1096,30 @@ def test_concurrent_updates_to_version_are_not_allowed(postgres_session_factory)
<1> We start two threads that will reliably produce the concurrency behavior we
want: `read1, read2, write1, write2`.
+我们启动两个线程,这将可靠地重现我们想要的并发行为:`read1, read2, write1, write2`。
<2> We assert that the version number has been incremented only once.
+我们断言版本号只增加了一次。
<3> We can also check on the specific exception if we like.
+如果需要,我们还可以检验具体的异常情况。
<4> And we double-check that only one allocation has gotten through.
+我们进一步确认只有一个分配操作成功了。
// TODO: use """ syntax for sql literal above?
==== Enforcing Concurrency Rules by Using Database Transaction [.keep-together]#Isolation Levels#
+通过使用数据库事务隔离级别来强制执行并发规则
((("transactions", "using to enforce concurrency rules")))
((("concurrency", "enforcing rules using database transactions")))
To get the test to pass as it is, we can set the transaction isolation level
on our session:
+为了让测试按预期通过,我们可以在会话上设置事务隔离级别:
+
[[isolation_repeatable_read]]
.Set isolation level for session (src/allocation/service_layer/unit_of_work.py)
====
@@ -897,8 +1142,12 @@ TIP: Transaction isolation levels are tricky stuff, so it's worth spending time
[.keep-together]#example#.]
((("PostgreSQL", "documentation for transaction isolation levels")))
((("isolation levels (transaction)")))
+事务隔离级别是比较复杂的内容,因此值得花些时间阅读和理解 https://oreil.ly/5vxJA[Postgres 文档]。脚注:[如果你没有使用 Postgres,
+则需要阅读其他数据库的文档。令人遗憾的是,不同的数据库对事务隔离级别的定义往往差异很大。
+例如,Oracle 的 `SERIALIZABLE` 等同于 Postgres 的 `REPEATABLE READ`,这就是一个[.keep-together]#例子#。]
==== Pessimistic Concurrency Control Example: SELECT FOR UPDATE
+悲观并发控制示例:SELECT FOR UPDATE
((("pessimistic concurrency", "example, SELECT FOR UPDATE")))
((("concurrency", "pessimistic concurrency example, SELECT FOR UPDATE")))
@@ -907,6 +1156,8 @@ There are multiple ways to approach this, but we'll show one. https://oreil.ly/i
produces different behavior; two concurrent transactions will not be allowed to
do a read on the same rows at the same time:
+有多种方法可以实现这一点,但我们将展示其中一种方法。https://oreil.ly/i8wKL[`SELECT FOR UPDATE`] 会产生不同的行为:两个并发事务将不能同时读取相同的行:
+
((("SQLAlchemy", "using DSL to specify FOR UPDATE")))
`SELECT FOR UPDATE` is a way of picking a row or rows to use as a lock
(although those rows don't have to be the ones you update). If two
@@ -914,9 +1165,14 @@ transactions both try to `SELECT FOR UPDATE` a row at the same time, one will
win, and the other will wait until the lock is released. So this is an example
of pessimistic concurrency control.
+`SELECT FOR UPDATE` 是一种选择一行或多行用作锁的方法(尽管这些行不一定是你要更新的行)。
+如果两个事务同时尝试对同一行执行 `SELECT FOR UPDATE`,其中一个会成功,而另一个则会等待直到锁被释放。因此,这就是一个悲观并发控制的示例。
+
Here's how you can use the SQLAlchemy DSL to specify `FOR UPDATE` at
query time:
+以下是如何使用 SQLAlchemy 的 DSL 在查询时指定 `FOR UPDATE`:
+
[[with_for_update]]
.SQLAlchemy with_for_update (src/allocation/adapters/repository.py)
====
@@ -936,6 +1192,8 @@ query time:
This will have the effect of changing the concurrency pattern from
+这会将并发模式从以下方式改变:
+
[role="skip"]
----
read1, read2, write1, write2(fail)
@@ -953,6 +1211,9 @@ read1, write1, read2, write2(succeed)
Some people refer to this as the "read-modify-write" failure mode.
Read https://oreil.ly/uXeZI["PostgreSQL Anti-Patterns: Read-Modify-Write Cycles"] for a good [.keep-together]#overview#.
+有些人将这种模式称为“读-修改-写”失败模式。阅读 https://oreil.ly/uXeZI["PostgreSQL Anti-Patterns: Read-Modify-Write Cycles"]
+以获得一个很好的[.keep-together]#概述#。
+
//TODO maybe better diagrams here?
((("data integrity", "testing for", startref="ix_daint")))
@@ -963,9 +1224,14 @@ But if you have a test like the one we've shown, you can specify the behavior
you want and see how it changes. You can also use the test as a basis for
performing some performance experiments.((("aggregates", "testing for data integrity rules", startref="ix_aggtstdi")))
+我们没有足够的时间来详细讨论 `REPEATABLE READ` 和 `SELECT FOR UPDATE` 之间的所有权衡,或者一般情况下乐观锁与悲观锁的对比。
+但如果你有一个像我们展示的那样的测试,你可以指定你想要的行为并观察其变化。你还可以将该测试作为进行一些性能实验的基础。
+((("聚合", "测试数据完整性规则", startref="ix_aggtstdi")))
+
=== Wrap-Up
+总结
((("aggregates", "and consistency boundaries recap")))
Specific choices around concurrency control vary a lot based on business
@@ -975,6 +1241,9 @@ object as being the main entrypoint to some subset of our model, and as being in
charge of enforcing the invariants and business rules that apply across all of
those objects.
+关于并发控制的具体选择因业务环境和存储技术的不同而存在很大差异,但我们希望将本章的重点回归到聚合的概念性思想上:
+我们通过显式建模将一个对象作为模型中某个子集的主要入口,并将其负责强制执行适用于所有这些对象的不变量和业务规则。
+
((("Effective Aggregate Design (Vernon)")))
((("Vernon, Vaughn")))
((("domain driven design (DDD)", "choosing the right aggregate, references on")))
@@ -984,75 +1253,94 @@ We also recommend these three online papers on
https://dddcommunity.org/library/vernon_2011[effective aggregate design]
by Vaughn Vernon (the "red book" author).
+选择合适的聚合是关键,这一决策可能会随着时间的推移而不断重新评估。有关更多内容,你可以查阅多本领域驱动设计(DDD)相关的书籍。
+我们还推荐阅读 Vaughn Vernon(“红皮书”作者)撰写的关于 https://dddcommunity.org/library/vernon_2011[高效聚合设计] 的三篇在线论文。
+
((("aggregates", "pros and cons or trade-offs")))
<> has some thoughts on the trade-offs of implementing the Aggregate pattern.
+<> 提供了一些关于实现聚合模式时权衡取舍的思考。
+
[[chapter_07_aggregate_tradoffs]]
[options="header"]
-.Aggregates: the trade-offs
+.Aggregates: the trade-offs(聚合:权衡取舍)
|===
-|Pros|Cons
+|Pros(优点)|Cons(缺点)
a|
* Python might not have "official" public and private methods, but we do have
the underscores convention, because it's often useful to try to indicate what's for
"internal" use and what's for "outside code" to use. Choosing aggregates is
just the next level up: it lets you decide which of your domain model classes
are the public ones, and which aren't.
+_Python_ 可能没有“官方的”公共和私有方法,但我们有下划线的约定,因为尝试指示哪些是供“内部”使用的,哪些是供“外部代码”使用的,
+通常是很有用的。选择聚合就是更高一级的设计:它让你可以决定你的领域模型类中哪些是公共的,哪些不是。
* Modeling our operations around explicit consistency boundaries helps us avoid
performance problems with our ORM.
((("performance", "consistency boundaries and")))
+围绕显式的一致性边界来建模操作,可以帮助我们避免 ORM 的性能问题。
* Putting the aggregate in sole charge of state changes to its subsidiary models
makes the system easier to reason about, and makes it easier to control invariants.
+让聚合全权负责其子模型的状态变更,可以让系统更容易理解,同时也更容易控制不变量。
a|
* Yet another new concept for new developers to take on. Explaining entities versus
value objects was already a mental load; now there's a third type of domain
model object?
+对于新开发者来说,这又是一个需要掌握的新概念。解释实体与值对象之间的区别已经是一种心智负担了,现在居然又多了一种领域模型对象类型?
* Sticking rigidly to the rule that we modify only one aggregate at a time is a
big mental shift.
+严格遵守一次只修改一个聚合的规则是一个很大的思维转变。
* Dealing with eventual consistency between aggregates can be complex.
+处理聚合之间的最终一致性可能会非常复杂。
|===
[role="nobreakinside less_space"]
-.Aggregates and Consistency Boundaries Recap
+.Aggregates and Consistency Boundaries Recap(聚合和一致性边界回顾)
*****************************************************************
((("consistency boundaries", "recap")))
-Aggregates are your entrypoints into the domain model::
+Aggregates are your entrypoints into the domain model(聚合是你进入领域模型的入口点)::
By restricting the number of ways that things can be changed,
we make the system easier to reason about.
+通过限制可以更改事物的方式数量,我们使系统更容易理解。
-Aggregates are in charge of a consistency boundary::
+Aggregates are in charge of a consistency boundary(聚合负责一致性边界)::
An aggregate's job is to be able to manage our business rules
about invariants as they apply to a group of related objects.
It's the aggregate's job to check that the objects within its
remit are consistent with each other and with our rules, and
to reject changes that would break the rules.
+聚合的职责是管理与一组相关对象相关的不变量业务规则。聚合的任务是检查其管辖范围内的对象之间以及它们与我们的规则之间的一致性,
+并拒绝那些会破坏规则的更改。
-Aggregates and concurrency issues go together::
+Aggregates and concurrency issues go together(聚合与并发问题密切相关)::
When thinking about implementing these consistency checks, we
end up thinking about transactions and locks. Choosing the
right aggregate is about performance as well as conceptual
organization of your domain.
((("concurrency", "aggregates and concurrency issues")))
+在考虑实现这些一致性检查时,我们最终会涉及事务和锁的思考。选择合适的聚合不仅关系到性能,还涉及领域的概念性组织。
*****************************************************************
[role="pagebreak-before less_space"]
=== Part I Recap
+第一部分回顾
((("component diagram at end of Part One")))
Do you remember <>, the diagram we showed at the
beginning of <> to preview where we were heading?
+你还记得 <> 吗?这是我们在 <> 开头展示的一个图,用来预览我们的学习方向。
+
[role="width-75"]
[[recap_components_diagram]]
-.A component diagram for our app at the end of Part I
+.A component diagram for our app at the end of Part I(第一部分结束时我们应用程序的组件图)
image::images/apwp_0705.png[]
So that's where we are at the end of Part I. What have we achieved? We've
@@ -1064,11 +1352,18 @@ have confidence that our tests will help us to prove the new functionality, and
when new developers join the project, they can read our tests to understand how
things work.
+这就是我们在第一部分结束时所处的位置。我们取得了哪些成就呢?我们已经了解了如何构建由一组高层次单元测试驱动的领域模型。
+我们的测试是活的文档:它们以清晰可读的代码描述了我们系统的行为——那些我们与业务相关方达成一致的规则。当业务需求发生变化时,
+我们有信心相信测试将帮助我们验证新的功能;而当新开发者加入项目时,他们可以阅读我们的测试以了解系统是如何工作的。
+
We've decoupled the infrastructural parts of our system, like the database and
API handlers, so that we can plug them into the outside of our application.
This helps us to keep our codebase well organized and stops us from building a
big ball of mud.
+我们已经将系统的基础设施部分(如数据库和 API 处理程序)解耦,使其能够作为外部组件连接到我们的应用程序。这有助于保持代码库的良好组织,
+防止我们构建出一团混乱的代码结构。
+
((("adapters", "ports-and-adapters inspired patterns")))
((("ports", "ports-and-adapters inspired patterns")))
By applying the dependency inversion principle, and by using
@@ -1077,14 +1372,23 @@ made it possible to do TDD in both high gear and low gear and to maintain a
healthy test pyramid. We can test our system edge to edge, and the need for
integration and end-to-end tests is kept to a minimum.
+通过应用依赖反转原则,并使用类似于端口和适配器(Ports-and-Adapters)模式的设计,如仓储(Repository)和工作单元(Unit of Work),
+我们实现了在高效模式和低效模式下进行测试驱动开发(TDD)的可能性,并维护了一个健康的测试金字塔。我们可以从头到尾测试我们的系统,
+同时将对集成测试和端到端测试的需求降至最低。
+
Lastly, we've talked about the idea of consistency boundaries. We don't want to
lock our entire system whenever we make a change, so we have to choose which
parts are consistent with one another.
+最后,我们讨论了一致性边界的概念。我们不希望在每次进行更改时都锁定整个系统,因此必须选择哪些部分需要彼此保持一致。
+
For a small system, this is everything you need to go and play with the ideas of
domain-driven design. You now have the tools to build database-agnostic domain
models that represent the shared language of your business experts. Hurrah!
+对于一个小型系统来说,这已经是探索领域驱动设计(DDD)理念所需的一切了。你现在拥有了构建与数据库无关的领域模型的工具,
+这些模型能够体现你的业务专家之间的通用语言。万岁!
+
NOTE: At the risk of laboring the point--we've been at pains to point out that
each pattern comes at a cost. Each layer of indirection has a price in terms
of complexity and duplication in our code and will be confusing to programmers
@@ -1094,7 +1398,12 @@ NOTE: At the risk of laboring the point--we've been at pains to point out that
use Django, and save yourself a lot of bother.
((("CRUD wrapper around a database")))
((("patterns, deciding whether you need to use them")))
+冒着重复强调这一点的风险——我们一直致力于指出,每种模式都伴随着一定的代价。每一层间接抽象都会在代码中带来复杂性和重复性,
+同时也会让从未见过这些模式的程序员感到困惑。如果你的应用本质上只是一个围绕数据库的简单 CRUD 封装,并且在可预见的未来也不会变得比这更复杂,
+_你完全不需要这些模式_。尽管使用 Django 吧,这样可以为自己省去许多麻烦。
In Part II, we'll zoom out and talk about a bigger topic: if aggregates are our
boundary, and we can update only one at a time, how do we model processes that
cross consistency boundaries?
+
+在第二部分,我们将放大视角,讨论一个更大的主题:如果聚合是我们的边界,并且我们一次只能更新一个,那么我们该如何为跨越一致性边界的流程建模?
From fd8e5988047512e7f4a428a3b03ac30f711b8bfa Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Fri, 31 Jan 2025 09:54:17 +0800
Subject: [PATCH 12/75] update Readme.md part1.asciidoc
---
Readme.md | 2 +-
part1.asciidoc | 32 +++++++++++++++++++++++++++++++-
2 files changed, 32 insertions(+), 2 deletions(-)
diff --git a/Readme.md b/Readme.md
index 31622a40..632b3359 100644
--- a/Readme.md
+++ b/Readme.md
@@ -18,7 +18,7 @@ O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布
|-----------------------------------------------------------------------------------------------------------------------------| ----- |
| [Preface
前言(已翻译)](preface.asciidoc) | |
| [Introduction: Why do our designs go wrong?
引言:为什么我们的设计会出问题?(已翻译)](introduction.asciidoc) | ||
-| [**Part 1 Intro
第一部分简介**](part1.asciidoc) | |
+| [**Part 1 Intro
第一部分简介(已翻译)**](part1.asciidoc) | |
| [Chapter 1: Domain Model
第一章:领域模型(已翻译)](chapter_01_domain_model.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 2: Repository
第二章:仓储(已翻译)](chapter_02_repository.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 3: Interlude: Abstractions
第三章:插曲:抽象(已翻译)](chapter_03_abstractions.asciidoc) | |
diff --git a/part1.asciidoc b/part1.asciidoc
index 2d357b7d..e45b87ea 100644
--- a/part1.asciidoc
+++ b/part1.asciidoc
@@ -2,11 +2,14 @@
[[part1]]
[part]
== Building an Architecture to Support Domain Modeling
+构建支持领域建模的架构
[quote, Cyrille Martraire, DDD EU 2017]
____
Most developers have never seen a domain model, only a data model.
+
+大多数开发者只见过数据模型,而从未见过领域模型。
____
Most developers we talk to about architecture have a nagging sense that
@@ -15,36 +18,54 @@ wrong somehow, and are trying to put some structure back into a ball of mud.
They know that their business logic shouldn't be spread all over the place,
but they have no idea how to fix it.
+我们和大多数开发者谈论架构时,他们通常都有一种挥之不去的感觉:现状本可以更好。很多时候,他们在试图拯救一个以某种方式陷入混乱的系统,
+并努力在一团乱麻中重建一些结构。他们知道业务逻辑不应该到处散落,但却不知道该如何解决这个问题。
+
We've found that many developers, when asked to design a new system, will
immediately start to build a database schema, with the object model treated
as an afterthought. This is where it all starts to go wrong. Instead, _behavior
should come first and drive our storage requirements._ After all, our customers don't care about the data model. They care about what
the system _does_; otherwise they'd just use a spreadsheet.
+我们发现,许多开发者在被要求设计一个新系统时,会直接从构建数据库模式入手,而将对象模型当作事后补充。这正是问题开始出错的地方。
+实际上,_行为应该是首要的,并驱动我们的存储需求。_ 毕竟,客户并不关心数据模型,他们关心的是系统_做了什么_;否则,他们就直接使用电子表格了。
+
The first part of the book looks at how to build a rich object model
through TDD (in <>), and then we'll show how
to keep that model decoupled from technical concerns. We show how to build
persistence-ignorant code and how to create stable APIs around our domain so
that we can refactor aggressively.
+本书的第一部分将探讨如何通过TDD构建一个丰富的对象模型(在<>中),随后我们将展示如何让该模型与技术问题解耦。
+我们会讲解如何构建与持久化无关的代码,以及如何围绕我们的领域创建稳定的API,从而使我们能够进行积极的重构。
+
To do that, we present four key design patterns:
+为此,我们将介绍四个关键的设计模式:
+
* The <>, an abstraction over the
idea of persistent storage
+<>,一种对持久化存储概念的抽象。
* The <> to clearly define where our
use cases begin and end
-
+<>,用于清晰地定义我们的用例从哪里开始以及在哪里结束。
+
[role="pagebreak-before"]
* The <> to provide atomic operations
+<>,用于提供原子操作。
* The <> to enforce the integrity
of our data
+<>,用于确保数据的完整性。
If you'd like a picture of where we're going, take a look at
<>, but don't worry if none of it makes sense
yet! We introduce each box in the figure, one by one, throughout this part of the book.
+如果你想了解我们接下来的内容,可以看看<>,不过如果现在还不明白也别担心!
+我们会在本书的这一部分中逐一介绍图中的每个模块。
+
[role="width-90"]
[[part1_components_diagram]]
.A component diagram for our app at the end of <>
@@ -54,16 +75,25 @@ We also take a little time out to talk about
<>, illustrating it with a simple example that shows how and why we choose our
abstractions.
+我们还会花一些时间讨论<>,并通过一个简单的示例来说明我们是如何以及为什么选择抽象的。
+
Three appendices are further explorations of the content from Part I:
+有三个附录进一步探讨了第一部分的内容:
+
* <> is a write-up of the infrastructure for our example
code: how we build and run the Docker images, where we manage configuration
info, and how we run different types of tests.
+<> 详细介绍了我们示例代码的基础设施:我们如何构建和运行Docker镜像、如何管理配置信息,
+以及如何运行不同类型的测试。
* <> is a "proof of the pudding" kind of content, showing
how easy it is to swap out our entire infrastructure--the Flask API, the
ORM, and Postgres—for a totally different I/O model involving a CLI and
CSVs.
+<> 是一种“实践检验”的内容,展示了将整个基础设施(如Flask API、ORM和Postgres)替换为
+完全不同的I/O模型(包括CLI和CSV文件)是多么简单。
* Finally, <> may be of interest if you're wondering how these
patterns might look if using Django instead of Flask and SQLAlchemy.
+最后,如果你想了解在使用Django而不是Flask和SQLAlchemy时这些模式会是什么样子,可以参考<>。
From 392e51d326d7429f543376b931d59c80dcc37883 Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Fri, 31 Jan 2025 10:00:22 +0800
Subject: [PATCH 13/75] update Readme.md part2.asciidoc
---
Readme.md | 2 +-
part2.asciidoc | 29 +++++++++++++++++++++++++----
2 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/Readme.md b/Readme.md
index 632b3359..52bc7942 100644
--- a/Readme.md
+++ b/Readme.md
@@ -26,7 +26,7 @@ O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布
| [Chapter 5: TDD in High Gear and Low Gear
第五章:高速模式与低速模式下的测试驱动开发(已翻译)](chapter_05_high_gear_low_gear.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 6: Unit of Work
第六章:工作单元(已翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 7: Aggregates
第七章:聚合(已翻译)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [**Part 2 Intro
第二部分简介**](part2.asciidoc) | |
+| [**Part 2 Intro
第二部分简介(已翻译)**](part2.asciidoc) | |
| [Chapter 8: Domain Events and a Simple Message Bus
第八章:领域事件与简单消息总线(翻译中...)](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 9: Going to Town on the MessageBus
第九章:深入探讨消息总线(未翻译)](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 10: Commands
第十章:命令(未翻译)](chapter_10_commands.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
diff --git a/part2.asciidoc b/part2.asciidoc
index 6bfc1db3..5979f4fe 100644
--- a/part2.asciidoc
+++ b/part2.asciidoc
@@ -1,6 +1,7 @@
[[part2]]
[part]
== Event-Driven Architecture
+事件驱动架构
[quote, Alan Kay]
____
@@ -8,9 +9,13 @@ ____
I'm sorry that I long ago coined the term "objects" for this topic because it
gets many people to focus on the lesser idea.
+我很抱歉自己很早就为这个主题创造了“对象”这个术语,因为它让许多人将注意力集中在了次要的概念上。
+
The big idea is "messaging."...The key in making great and growable systems is
much more to design how its modules communicate rather than what their internal
properties and behaviors should be.
+
+核心思想是“消息传递”……构建优秀且可扩展系统的关键更多在于设计模块之间如何通信,而不是它们的内部属性和行为应该是什么样的。
____
It's all very well being able to write _one_ domain model to manage a single bit
@@ -19,20 +24,29 @@ the real world, our applications sit within an organization and need to exchange
information with other parts of the system. You may remember our context
diagram shown in <>.
+能够编写_一个_领域模型来管理单一业务流程当然很好,但是当我们需要编写_多个_模型时会发生什么呢?在现实世界中,我们的应用程序位于一个组织内,
+并且需要与系统的其他部分交换信息。你或许还记得我们在<>中展示的上下文图。
+
Faced with this requirement, many teams reach for microservices integrated
via HTTP APIs. But if they're not careful, they'll end up producing the most
chaotic mess of all: the distributed big ball of mud.
+面对这一需求,许多团队会选择通过HTTP API集成的微服务架构。但如果不小心,他们最终可能会制造出最混乱的局面:分布式的“大泥球”。
+
In Part II, we'll show how the techniques from <> can be extended to
distributed systems. We'll zoom out to look at how we can compose a system from
many small components that interact through asynchronous message passing.
+在第二部分中,我们将展示如何将<>中的技术扩展到分布式系统。我们将放大视角,探讨如何通过异步消息传递将多个小组件组合成一个系统。
+
We'll see how our Service Layer and Unit of Work patterns allow us to reconfigure our app
to run as an asynchronous message processor, and how event-driven systems help
us to decouple aggregates and applications from one another.
+我们将看到如何利用服务层模式和工作单元模式,将我们的应用程序重新配置为一个异步消息处理器,以及事件驱动系统如何帮助我们实现聚合与应用程序之间的解耦。
+
[[allocation_context_diagram_again]]
-.But exactly how will all these systems talk to each other?
+.But exactly how will all these systems talk to each other?(但这些系统究竟如何相互通信呢?)
image::images/apwp_0102.png[]
@@ -44,18 +58,25 @@ image::images/apwp_0102.png[]
We'll look at the following patterns and techniques:
-Domain Events::
+我们将探讨以下模式和技术:
+
+Domain Events(领域事件)::
Trigger workflows that cross consistency boundaries.
+触发跨越一致性边界的工作流。
-Message Bus::
+Message Bus(消息总线)::
Provide a unified way of invoking use cases from any endpoint.
+提供一种从任何端点调用用例的统一方式。
-CQRS::
+CQRS(命令查询责任分离)::
Separating reads and writes avoids awkward compromises in an event-driven
architecture and enables performance and scalability improvements.
+将读取和写入分离可以避免在事件驱动架构中出现尴尬的折中,并提升性能和可扩展性。
Plus, we'll add a dependency injection framework. This has nothing to do with
event-driven architecture per se, but it tidies up an awful lot of loose
ends.
+另外,我们还会引入一个依赖注入框架。虽然这本身与事件驱动架构无关,但它能整理好许多松散的部分。
+
// IDEA: a bit of blurb about making events more central to our design thinking?
From f01ff5558b9983fdd640c44091f33faa95034068 Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Fri, 31 Jan 2025 10:49:25 +0800
Subject: [PATCH 14/75] update Readme.md
chapter_08_events_and_message_bus.asciidoc
---
Readme.md | 4 +-
chapter_08_events_and_message_bus.asciidoc | 207 ++++++++++++++++++++-
2 files changed, 199 insertions(+), 12 deletions(-)
diff --git a/Readme.md b/Readme.md
index 52bc7942..25a37457 100644
--- a/Readme.md
+++ b/Readme.md
@@ -27,8 +27,8 @@ O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布
| [Chapter 6: Unit of Work
第六章:工作单元(已翻译)](chapter_06_uow.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 7: Aggregates
第七章:聚合(已翻译)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [**Part 2 Intro
第二部分简介(已翻译)**](part2.asciidoc) | |
-| [Chapter 8: Domain Events and a Simple Message Bus
第八章:领域事件与简单消息总线(翻译中...)](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 9: Going to Town on the MessageBus
第九章:深入探讨消息总线(未翻译)](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 8: Domain Events and a Simple Message Bus
第八章:领域事件与简单消息总线(已翻译)](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 9: Going to Town on the MessageBus
第九章:深入探讨消息总线(翻译中...)](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 10: Commands
第十章:命令(未翻译)](chapter_10_commands.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 11: External Events for Integration
第十一章:集成外部事件(未翻译)](chapter_11_external_events.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 12: CQRS
第十二章:命令查询责任分离(未翻译)](chapter_12_cqrs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
diff --git a/chapter_08_events_and_message_bus.asciidoc b/chapter_08_events_and_message_bus.asciidoc
index dcbbe761..dcc017b4 100644
--- a/chapter_08_events_and_message_bus.asciidoc
+++ b/chapter_08_events_and_message_bus.asciidoc
@@ -1,27 +1,40 @@
[[chapter_08_events_and_message_bus]]
== Events and the Message Bus
+事件与消息总线
((("events and the message bus", id="ix_evntMB")))
So far we've spent a lot of time and energy on a simple problem that we could
easily have solved with Django. You might be asking if the increased testability
and expressiveness are _really_ worth all the effort.
+到目前为止,我们花费了大量时间和精力解决一个可以轻松用Django解决的简单问题。你可能会问,增加的可测试性和表达能力是否_真的_值得这些努力。
+
In practice, though, we find that it's not the obvious features that make a mess
of our codebases: it's the goop around the edge. It's reporting, and permissions,
and workflows that touch a zillion objects.
+然而,在实践中,我们发现并不是那些显而易见的功能让代码库变得混乱,而是边缘部分的杂乱。比如,报告、权限管理,以及涉及无数对象的工作流程。
+
Our example will be a typical notification requirement: when we can't allocate
an order because we're out of stock, we should alert the buying team. They'll
go and fix the problem by buying more stock, and all will be well.
+我们的示例将是一个典型的通知需求:当我们因为缺货而无法分配订单时,我们应该提醒采购团队。他们会通过采购更多的库存来解决问题,一切就迎刃而解了。
+
For a first version, our product owner says we can just send the alert by email.
+对于第一个版本,我们的产品负责人表示可以仅通过电子邮件发送提醒。
+
Let's see how our architecture holds up when we need to plug in some of the
mundane stuff that makes up so much of our systems.
+让我们看看当我们需要引入一些构成系统大部分的琐碎内容时,我们的架构能否经受住考验。
+
We'll start by doing the simplest, most expeditious thing, and talk about
why it's exactly this kind of decision that leads us to the Big Ball of Mud.
+我们将从最简单、最迅速的方法入手,并探讨为什么正是这种决定会将我们引向“大泥球”的困境。
+
((("Message Bus pattern")))
((("Domain Events pattern")))
((("events and the message bus", "events flowing through the system")))
@@ -33,9 +46,13 @@ those events and how to pass them to the message bus, and finally we'll show
how the Unit of Work pattern can be modified to connect the two together elegantly,
as previewed in <>.
+然后,我们将展示如何使用 _领域事件_ 模式将副作用与用例分离开,并且如何使用一个简单的 _消息总线_ 模式基于这些事件触发行为。
+我们会展示一些创建这些事件的选项,以及如何将它们传递给消息总线,最后将展示如何修改工作单元模式以优雅地将两者连接在一起,
+正如在<>中预览的一样。
+
[[message_bus_diagram]]
-.Events flowing through the system
+.Events flowing through the system(流经系统的事件)
image::images/apwp_0801.png[]
// TODO: add before diagram for contrast (?)
@@ -46,6 +63,8 @@ image::images/apwp_0801.png[]
The code for this chapter is in the
chapter_08_events_and_message_bus branch https://oreil.ly/M-JuL[on GitHub]:
+本章的代码位于 `chapter_08_events_and_message_bus` 分支,https://oreil.ly/M-JuL[在GitHub上]:
+
----
git clone https://github.com/cosmicpython/code.git
cd code
@@ -57,6 +76,7 @@ git checkout chapter_07_aggregate
=== Avoiding Making a Mess
+避免制造混乱
((("web controllers, sending email alerts via, avoiding")))
((("events and the message bus", "sending email alerts when out of stock", id="ix_evntMBeml")))
@@ -64,12 +84,17 @@ git checkout chapter_07_aggregate
So. Email alerts when we run out of stock. When we have new requirements like ones that _really_ have nothing to do with the core domain, it's all too easy to
start dumping these things into our web controllers.
+那么,当我们库存不足时发送电子邮件提醒。当我们遇到类似这样的新需求时,尤其是那些与核心领域_并没有真正关系_的需求,很容易就会开始把这些东西堆到我们的Web控制器里。
+
==== First, Let's Avoid Making a Mess of Our Web Controllers
+首先,让我们避免把我们的 Web 控制器搞得一团糟
((("events and the message bus", "sending email alerts when out of stock", "avoiding messing up web controllers")))
As a one-off hack, this _might_ be OK:
+作为一个一次性的临时解决方案,这_也许_还可以接受:
+
[[email_in_flask]]
.Just whack it in the endpoint—what could go wrong? (src/allocation/entrypoints/flask_app.py)
====
@@ -102,8 +127,11 @@ def allocate_endpoint():
like this. Sending email isn't the job of our HTTP layer, and we'd like to be
able to unit test this new feature.
+...但不难看出,通过像这样打补丁,我们很快就可能陷入混乱。发送电子邮件并不是我们HTTP层的职责,而且我们希望能够对这个新功能进行单元测试。
+
==== And Let's Not Make a Mess of Our Model Either
+同时也不要让我们的模型陷入混乱
((("domain model", "email sending code in, avoiding")))
((("events and the message bus", "sending email alerts when out of stock", "avoiding messing up domain model")))
@@ -111,6 +139,8 @@ Assuming we don't want to put this code into our web controllers, because
we want them to be as thin as possible, we may look at putting it right at
the source, in the model:
+假设我们不想把这段代码放在我们的 Web 控制器中,因为我们希望它们尽可能简洁,那么我们可能会考虑直接把它放到源头——模型中:
+
[[email_in_model]]
.Email-sending code in our model isn't lovely either (src/allocation/domain/model.py)
====
@@ -130,12 +160,17 @@ the source, in the model:
But that's even worse! We don't want our model to have any dependencies on
infrastructure concerns like `email.send_mail`.
+但这就更糟糕了!我们不希望我们的模型对诸如 `email.send_mail` 这样的基础设施问题有任何依赖。
+
This email-sending thing is unwelcome _goop_ messing up the nice clean flow
of our system. What we'd like is to keep our domain model focused on the rule
"You can't allocate more stuff than is actually available."
+这个发送电子邮件的功能是不受欢迎的_杂乱_,它破坏了我们系统的干净流畅。我们希望的是,让我们的领域模型专注于规则:“你不能分配超过实际可用的库存。”
+
==== Or the Service Layer!
+或者用服务层!
((("service layer", "sending email alerts when out of stock, avoiding")))
((("events and the message bus", "sending email alerts when out of stock", "out of place in the service layer")))
@@ -143,9 +178,13 @@ The requirement "Try to allocate some stock, and send an email if it fails" is
an example of workflow orchestration: it's a set of steps that the system has
to follow to [.keep-together]#achieve# a goal.
+需求“尝试分配一些库存,如果失败则发送一封邮件”是一个工作流编排的示例:它是一组系统必须遵循以[.keep-together]#实现#目标的步骤。
+
We've written a service layer to manage orchestration for us, but even here
the feature feels out of place:
+我们已经编写了一个服务层来为我们管理编排,但即使在这里,这个功能也显得格格不入:
+
[[email_in_services]]
.And in the service layer, it's out of place (src/allocation/service_layer/services.py)
====
@@ -177,7 +216,10 @@ Catching an exception and reraising it? It could be worse, but it's
definitely making us unhappy. Why is it so hard to find a suitable home for
this code?
+捕获一个异常然后重新抛出?这可能还不算最糟,但它确实让我们感到不快。为什么要为这段代码找到一个合适的归宿会这么困难呢?
+
=== Single Responsibility Principle
+单一职责原则
((("single responsibility principle (SRP)")))
((("events and the message bus", "sending email alerts when out of stock", "violating the single responsibility principle")))
@@ -187,13 +229,21 @@ Our use case is allocation. Our endpoint, service function, and domain methods
are all called [.keep-together]#`allocate`#, not
`allocate_and_send_mail_if_out_of_stock`.
+实际上,这是违反了__单一职责原则__(SRP)。脚注:[
+这个原则是https://oreil.ly/AIdSD[SOLID]中的_S_。]
+我们的用例是分配。我们的端点、服务函数和领域方法都被称为[.keep-together]#`allocate`#,而不是`allocate_and_send_mail_if_out_of_stock`。
+
TIP: Rule of thumb: if you can't describe what your function does without using
words like "then" or "and," you might be violating the SRP.
+经验法则:如果你在描述函数的作用时无法避免使用“然后”或“和”这样的词语,那么你可能违反了单一职责原则(SRP)。
One formulation of the SRP is that each class should have only a single reason
to change. When we switch from email to SMS, we shouldn't have to update our
`allocate()` function, because that's clearly a separate responsibility.
+单一职责原则(SRP)的一种表述是,每个类应该只有一个导致其变化的原因。当我们从电子邮件切换到短信时,
+不应该需要更新我们的`allocate()`函数,因为这显然是一个独立的职责。
+
((("choreography")))
((("orchestration", "changing to choreography")))
To solve the problem, we're going to split the orchestration
@@ -205,31 +255,45 @@ of sending an alert belongs elsewhere. We should be able to turn this feature
on or off, or to switch to SMS notifications instead, without needing to change
the rules of our domain model.
+为了解决这个问题,我们准备将编排分解为独立的步骤,这样不同的关注点就不会混杂在一起。脚注:[
+我们的技术审阅者Ed Jung喜欢说,当你从命令式流程控制切换到基于事件的流程控制时,你就将_编排_转换成了_协作_。]
+领域模型的职责是知道我们缺货了,但发送警报的责任应该属于其他地方。我们应该能够开启或关闭此功能,或者切换到短信通知,而不需要修改领域模型的规则。
+
We'd also like to keep the service layer free of implementation details. We
want to apply the dependency inversion principle to notifications so that our
service layer depends on an abstraction, in the same way as we avoid depending
on the database by using a unit of work.
+我们还希望让服务层不包含实现细节。我们希望将依赖反转原则应用于通知,
+这样我们的服务层就依赖于一个抽象,就像我们通过使用工作单元(unit of work)来避免依赖数据库一样。
+
=== All Aboard the Message Bus!
+全员登上消息总线!
The patterns we're going to introduce here are _Domain Events_ and the _Message Bus_.
We can implement them in a few ways, so we'll show a couple before settling on
the one we like most.
+我们将在这里介绍的模式是_领域事件_和_消息总线_。它们可以通过几种方式实现,因此我们会先展示几个实现方式,然后再确定我们最喜欢的那一个。
+
// TODO: at this point the message bus is really just a dispatcher. could also mention
// pubsub. once we get a queue, it's more justifiably a bus
==== The Model Records Events
+模型记录事件
((("events and the message bus", "recording events")))
First, rather than being concerned about emails, our model will be in charge of
recording _events_—facts about things that have happened. We'll use a message
bus to respond to events and invoke a new operation.
+首先,我们的模型不再关注电子邮件,而是负责记录_事件_——即已经发生的事实。我们将使用消息总线来响应这些事件并触发新的操作。
+
==== Events Are Simple Dataclasses
+事件是简单的数据类
((("dataclasses", "events")))
((("events and the message bus", "events as simple dataclasses")))
@@ -237,10 +301,15 @@ An _event_ is a kind of _value object_. Events don't have any behavior, because
they're pure data structures. We always name events in the language of the
domain, and we think of them as part of our domain model.
+_事件_是一种_值对象_。事件没有任何行为,因为它们是纯数据结构。我们总是用领域的语言为事件命名,并将它们视为领域模型的一部分。
+
We could store them in _model.py_, but we may as well keep them in their own file
(this might be a good time to consider refactoring out a directory called
_domain_ so that we have _domain/model.py_ and _domain/events.py_):
+我们可以将它们存储在 _model.py_ 中,但不妨将它们放在单独的文件中(此时,可以考虑重构出一个名为 _domain_ 的目录,
+这样我们就有了 _domain/model.py_ 和 _domain/events.py_):
+
[role="nobreakinside less_space"]
[[events_dot_py]]
.Event classes (src/allocation/domain/events.py)
@@ -264,21 +333,28 @@ class OutOfStock(Event): #<2>
<1> Once we have a number of events, we'll find it useful to have a parent
class that can store common attributes. It's also useful for type
hints in our message bus, as you'll see shortly.
+当我们有多个事件时,会发现拥有一个父类来存储通用属性是很有用的。此外,这对于在消息总线中的类型提示也很有帮助,稍后你会看到这一点。
<2> `dataclasses` are great for domain events too.
+`dataclasses` 对于领域事件也非常出色。
==== The Model Raises Events
+模型触发事件
((("events and the message bus", "domain model raising events")))
((("domain model", "raising events")))
When our domain model records a fact that happened, we say it _raises_ an event.
+当我们的领域模型记录一个发生的事实时,我们称其为_触发_一个事件。
+
((("aggregates", "testing Product object to raise events")))
Here's what it will look like from the outside; if we ask `Product` to allocate
but it can't, it should _raise_ an event:
+从外部来看,它会是这样的:如果我们请求 `Product` 分配库存但失败了,它应该_触发_一个事件:
+
[[test_raising_event]]
.Test our aggregate to raise events (tests/unit/test_product.py)
@@ -298,9 +374,12 @@ def test_records_out_of_stock_event_if_cannot_allocate():
<1> Our aggregate will expose a new attribute called `.events` that will contain
a list of facts about what has happened, in the form of `Event` objects.
+我们的聚合将公开一个名为 `.events` 的新属性,该属性将以 `Event` 对象的形式包含一个关于已发生事实的列表。
Here's what the model looks like on the inside:
+以下是模型的内部实现:
+
[[domain_event]]
.The model raises a domain event (src/allocation/domain/model.py)
@@ -326,12 +405,15 @@ class Product:
====
<1> Here's our new `.events` attribute in use.
+以下是我们使用新的 `.events` 属性的示例。
<2> Rather than invoking some email-sending code directly, we record those
events at the place they occur, using only the language of the domain.
+我们并没有直接调用发送电子邮件的代码,而是在事件发生的地方记录这些事件,仅使用领域的语言来描述。
<3> We're also going to stop raising an exception for the out-of-stock
case. The event will do the job the exception was doing.
+我们还将停止在缺货情况下抛出异常。事件将完成之前由异常承担的任务。
@@ -343,10 +425,13 @@ NOTE: We're actually addressing a code smell we had until now, which is that we
confusing to have to reason about events and exceptions together.
((("control flow, using exceptions for")))
((("exceptions", "using for control flow")))
+实际上,我们正在解决之前存在的一种代码异味,也就是我们 https://oreil.ly/IQB51[用异常来控制流程]。通常来说,如果你正在实现领域事件,
+不要通过抛出异常来描述相同的领域概念。正如你稍后会在处理工作单元模式中的事件时看到的那样,同时考虑事件和异常是令人困惑的。
==== The Message Bus Maps Events to Handlers
+消息总线将事件映射到处理器
((("message bus", "mapping events to handlers")))
((("events and the message bus", "message bus mapping events to handlers")))
@@ -356,6 +441,9 @@ handler function." In other words, it's a simple publish-subscribe system.
Handlers are _subscribed_ to receive events, which we publish to the bus. It
sounds harder than it is, and we usually implement it with a dict:
+消息总线的基本作用是,“当我看到这个事件时,我应该调用以下处理器函数。” 换句话说,它是一个简单的发布-订阅系统。处理器_订阅_接收事件,
+而我们将事件发布到总线中。这听起来比实际要复杂,而我们通常用一个字典来实现它:
+
[[messagebus]]
.Simple message bus (src/allocation/service_layer/messagebus.py)
====
@@ -386,6 +474,8 @@ NOTE: Note that the message bus as implemented doesn't give us concurrency becau
"recipe" for how to run each use case is written in a single place. See the
following sidebar.
((("concurrency", "not provided by message bus implementation")))
+请注意,目前实现的消息总线并不支持并发,因为一次只能运行一个处理器。我们的目标并不是支持并行线程,而是从概念上分离任务,
+并尽可能让每个工作单元(UoW)保持小巧。这有助于我们理解代码库,因为每个用例的“运行步骤”都集中记录在一个地方。请参阅以下侧边栏。
[role="nobreakinside less_space"]
[[celery_sidebar]]
@@ -396,6 +486,8 @@ _Celery_ is a popular tool in the Python world for deferring self-contained
chunks of work to an asynchronous task queue.((("Celery tool"))) The message bus we're
presenting here is very different, so the short answer to the above question is no; our message bus
has more in common with an Express.js app, a UI event loop, or an actor framework.
+
+_Celery_ 是 _Python_ 领域中一个流行的工具,用于将独立的工作块推送到异步任务队列中。((("Celery 工具"))) 我们在这里介绍的消息总线与它非常不同,所以对于上面问题的简短回答是“不”;我们的消息总线更类似于 Express.js 应用程序、UI 事件循环或 actor 框架。
// TODO: this "more in common with" line is not super-helpful atm. maybe onclick callbacks in js would be a more helpful example
((("external events")))
@@ -410,6 +502,11 @@ across units of work within a single process/service can be extended across
multiple processes--which may be different containers within the same
service, or totally different microservices.
+如果你确实有将工作从主线程移出的需求,你仍然可以使用我们基于事件的比喻,不过我们建议你为此使用_外部事件_。
+关于这一点,在<>中有更多讨论,但关键在于,如果你实现了一种将事件持久化到集中存储的方法,
+就可以让其他容器或其他微服务订阅这些事件。然后,那种在单个进程/服务内使用事件来分离工作单元间职责的概念,
+就可以扩展到多个进程中——这些进程可以是同一服务中的不同容器,也可以是完全不同的微服务。
+
If you follow us in this approach, your API for distributing tasks
is your event [.keep-together]##classes—##or a JSON representation of them. This allows
you a lot of flexibility in who you distribute tasks to; they need not
@@ -417,10 +514,15 @@ necessarily be Python services. Celery's API for distributing tasks is
essentially "function name plus arguments," which is more restrictive,
and Python-only.
+如果你按照我们的这种方法,你用于分发任务的API就是你的事件[.keep-together]##类##——或者是它们的JSON表示形式。
+这为你在分发任务的对象上提供了很大的灵活性;这些对象不一定非得是 _Python_ 服务。而 _Celery_ 用于分发任务的API本质上是“函数名称加参数”,
+这种方法更具限制性,并且仅限于 _Python_。
+
*******************************************************************************
=== Option 1: The Service Layer Takes Events from the Model and Puts Them on the Message Bus
+选项 1:服务层从模型中获取事件并将其放置到消息总线上
((("domain model", "events from, passing to message bus in service layer")))
((("message bus", "service layer with explicit message bus")))
@@ -432,8 +534,13 @@ handlers whenever an event happens. Now all we need is to connect the two. We
need something to catch events from the model and pass them to the message
bus--the _publishing_ step.
+我们的领域模型触发事件,而我们的消息总线将在事件发生时调用相应的处理器。现在我们只需要将两者连接起来。
+我们需要某种机制来捕获模型中的事件并将其传递到消息总线——这是_发布_的步骤。
+
The simplest way to do this is by adding some code into our service layer:
+最简单的方式是在我们的服务层中添加一些代码:
+
[[service_talks_to_messagebus]]
.The service layer with an explicit message bus (src/allocation/service_layer/services.py)
====
@@ -463,18 +570,23 @@ def allocate(
<1> We keep the `try/finally` from our ugly earlier implementation (we haven't
gotten rid of _all_ exceptions yet, just `OutOfStock`).
+我们保留了之前丑陋实现中的 `try/finally`(我们还没有完全去掉_所有_异常,只是移除了 `OutOfStock`)。
<2> But now, instead of depending directly on an email infrastructure,
the service layer is just in charge of passing events from the model
up to the message bus.
+但现在,服务层不再直接依赖于电子邮件基础设施,而只是负责将模型中的事件传递到消息总线上。
That already avoids some of the ugliness that we had in our naive
implementation, and we have several systems that work like this one, in which the
service layer explicitly collects events from aggregates and passes them to
the message bus.
+这已经避免了我们在原始实现中遇到的一些丑陋之处,而且我们有多个类似的系统,其中服务层明确地从聚合中收集事件并将它们传递到消息总线。
+
=== Option 2: The Service Layer Raises Its Own Events
+选项 2:服务层触发自己的事件
((("service layer", "raising its own events")))
((("events and the message bus", "service layer raising its own events")))
@@ -483,6 +595,8 @@ Another variant on this that we've used is to have the service layer
in charge of creating and raising events directly, rather than having them
raised by the domain model:
+我们使用过的另一种变体是让服务层直接负责创建和触发事件,而不是由领域模型触发事件:
+
[[service_layer_raises_events]]
.Service layer calls messagebus.handle directly (src/allocation/service_layer/services.py)
@@ -513,13 +627,20 @@ def allocate(
wrong. Committing when we haven't changed anything is safe and keeps the
code uncluttered.
+和以前一样,即使分配失败我们也会提交,因为这样代码更简单且更易于理解:除非出问题,否则我们总是提交。
+当没有更改任何内容时提交是安全的,同时也能保持代码简洁。
+
Again, we have applications in production that implement the pattern in this
way. What works for you will depend on the particular trade-offs you face, but
we'd like to show you what we think is the most elegant solution, in which we
put the unit of work in charge of collecting and raising events.
+同样,我们也有一些生产中的应用程序是以这种方式实现该模式的。对你来说,哪种方法有效取决于你所面临的具体权衡,
+但我们想向你展示我们认为最优雅的解决方案,其中我们将工作单元(unit of work)负责收集和触发事件。
+
=== Option 3: The UoW Publishes Events to the Message Bus
+选项 3:工作单元(UoW)将事件发布到消息总线
((("message bus", "Unit of Work publishing events to")))
((("events and the message bus", "UoW publishes events to message bus")))
@@ -528,6 +649,9 @@ The UoW already has a `try/finally`, and it knows about all the aggregates
currently in play because it provides access to the repository. So it's
a good place to spot events and pass them to the message bus:
+工作单元(UoW)已经有了一个 `try/finally`,并且它了解当前正在使用的所有聚合,因为它提供了对仓储的访问。
+因此,它是捕捉事件并将它们传递到消息总线的一个好位置:
+
[[uow_with_messagebus]]
.The UoW meets the message bus (src/allocation/service_layer/unit_of_work.py)
@@ -563,17 +687,21 @@ class SqlAlchemyUnitOfWork(AbstractUnitOfWork):
<1> We'll change our commit method to require a private `._commit()`
method from subclasses.
+我们将修改提交方法,使其需要子类实现一个私有的 `._commit()` 方法。
<2> After committing, we run through all the objects that our
repository has seen and pass their events to the message bus.
+在提交之后,我们会遍历仓储中所有被访问过的对象,并将它们的事件传递到消息总线。
<3> That relies on the repository keeping track of aggregates that have been loaded
using a new attribute, `.seen`, as you'll see in the next listing.
((("repositories", "repository keeping track of aggregates passing through it")))
((("aggregates", "repository keeping track of aggregates passing through it")))
+这依赖于仓储通过一个新属性 `.seen` 来跟踪已加载的聚合对象,正如你将在接下来的代码示例中看到的。
NOTE: Are you wondering what happens if one of the
handlers fails? We'll discuss error handling in detail in <>.
+你是否在想,如果某个处理器失败会发生什么?我们将在 <> 中详细讨论错误处理。
//IDEA: could change ._commit() to requiring super().commit()
@@ -625,24 +753,32 @@ class SqlAlchemyRepository(AbstractRepository):
We use a `set` called `.seen` to store them. That means our implementations
need to call +++super().__init__()+++.
((("super function")))
+为了让工作单元(UoW)能够发布新的事件,它需要能够从仓储中获取出在哪个 `Product` 对象在本次会话中被使用过。
+我们使用一个名为 `.seen` 的 `set` 来存储这些对象。这意味着我们的实现需要调用 +++super().__init__()+++。
<2> The parent `add()` method adds things to `.seen`, and now requires subclasses
to implement `._add()`.
+父类的 `add()` 方法会将对象添加到 `.seen` 中,并且现在要求子类实现 `._add()` 方法。
<3> Similarly, `.get()` delegates to a `._get()` function, to be implemented by
subclasses, in order to capture objects seen.
+类似地,`.get()` 委托给一个 `._get()` 函数,由子类实现,以便捕获被访问过的对象。
NOTE: The use of pass:[._underscorey()] methods and subclassing is definitely not
the only way you could implement these patterns. Have a go at the
<> in this chapter and experiment
with some alternatives.
+使用 pass:[._underscorey()] 方法和子类化绝对不是实现这些模式的唯一方法。
+试着完成本章中的 <>,并尝试一些替代方案。
After the UoW and repository collaborate in this way to automatically keep
track of live objects and process their events, the service layer can be
totally free of event-handling concerns:
((("service layer", "totally free of event handling concerns")))
+在工作单元(UoW)和仓储以这种方式协作,自动跟踪活动对象并处理它们的事件之后,服务层就可以完全摆脱事件处理的事务:
+
[[services_clean]]
.Service layer is clean again (src/allocation/service_layer/services.py)
====
@@ -671,6 +807,9 @@ We do also have to remember to change the fakes in the service layer and make th
call `super()` in the right places, and to implement underscorey methods, but the
changes are minimal:
+我们还需要记住修改服务层中的虚拟实现(fakes),确保在正确的位置调用 `super()`,
+并实现那些以下划线开头的方法(underscorey methods),不过这些更改是很小的:
+
[[services_tests_ugly_fake_messagebus]]
.Service-layer fakes need tweaking (tests/unit/test_services.py)
@@ -701,7 +840,7 @@ class FakeUnitOfWork(unit_of_work.AbstractUnitOfWork):
[role="nobreakinside less_space"]
[[get_rid_of_commit]]
-.Exercise for the Reader
+.Exercise for the Reader(读者练习)
******************************************************************************
((("inheritance, avoiding use of with wrapper class")))
@@ -714,9 +853,14 @@ Harry around the head with a plushie snake"? Hey, our code listings are
only meant to be examples, not the perfect solution! Why not go see if you
can do better?
+你是否觉得所有那些 `._add()` 和 `._commit()` 方法“超级恶心”?正如我们尊敬的技术审阅者 Hynek 所说的那样,
+它是否“让你想拿一条软绵绵的玩具蛇去揍 Harry 一顿”?嘿,我们的代码示例仅仅是为了演示,而不是完美的解决方案!为什么不去看看你是否能做得更好呢?
+
One _composition over inheritance_ way to go would be to implement a
wrapper class:
+一种采用_组合优于继承_的方式是实现一个包装类:
+
[[tracking_repo_wrapper]]
.A wrapper adds functionality and then delegates (src/adapters/repository.py)
====
@@ -744,15 +888,21 @@ class TrackingRepository:
<1> By wrapping the repository, we can call the actual `.add()`
and `.get()` methods, avoiding weird underscorey methods.
+通过包装仓储,我们可以调用实际的 `.add()` 和 `.get()` 方法,从而避免使用那些奇怪的以下划线开头的方法。
((("Unit of Work pattern", "getting rid of underscorey methods in UoW class")))
See if you can apply a similar pattern to our UoW class in
order to get rid of those Java-y `_commit()` methods too. You can find the code
on https://github.com/cosmicpython/code/tree/chapter_08_events_and_message_bus_exercise[GitHub].
+试试看能否将类似的模式应用到我们的 UoW 类中,从而去掉那些有点像 Java 风格的 `_commit()` 方法。
+你可以在 https://github.com/cosmicpython/code/tree/chapter_08_events_and_message_bus_exercise[GitHub] 找到对应的代码。
+
((("abstract base classes (ABCs)", "switching to typing.Protocol")))
Switching all the ABCs to `typing.Protocol` is a good way to force yourself to
avoid using inheritance. Let us know if you come up with something nice!
+
+将所有的抽象基类(ABCs)切换为 `typing.Protocol` 是一个很好的方法,可以迫使你避免使用继承。如果你想出了一些不错的方案,请告诉我们!
******************************************************************************
You might be starting to worry that maintaining these fakes is going to be a
@@ -761,36 +911,51 @@ it's not a lot of work. Once your project is up and running, the interface for
your repository and UoW abstractions really don't change much. And if you're
using ABCs, they'll help remind you when things get out of sync.
+你可能开始担心维护这些虚拟实现(fakes)会成为一个维护负担。毫无疑问,这确实需要一些工作,但根据我们的经验,这并不会耗费太多精力。
+一旦你的项目启动并运行起来,仓储和工作单元(UoW)抽象的接口实际上变化不大。而且,如果你使用抽象基类(ABCs),它们会在接口不同步时提醒你。
+
=== Wrap-Up
+总结
Domain events give us a way to handle workflows in our system. We often find,
listening to our domain experts, that they express requirements in a causal or
temporal way—for example, "When we try to allocate stock but there's none
available, then we should send an email to the buying team."
+领域事件为我们提供了一种方式来处理系统中的工作流。我们经常发现,倾听领域专家时,他们会以因果或时间顺序的方式表达需求——例如,
+“当我们尝试分配库存但没有库存可用时,我们应该向采购团队发送一封电子邮件。”
+
The magic words "When X, then Y" often tell us about an event that we can make
concrete in our system. Treating events as first-class things in our model helps
us make our code more testable and observable, and it helps isolate concerns.
+“当 X,然后 Y”这样的魔法词语通常暗示我们可以在系统中实现的一个事件。在模型中将事件视为一等公民有助于我们使代码更加可测试和可观察,
+同时也有助于隔离关注点。
+
((("message bus", "pros and cons or trade-offs")))
((("events and the message bus", "pros and cons or trade-offs")))
And <> shows the trade-offs as we
see them.
+而 <> 展示了我们所看到的权衡。
+
[[chapter_08_events_and_message_bus_tradeoffs]]
[options="header"]
-.Domain events: the trade-offs
+.Domain events: the trade-offs(领域事件:权衡分析)
|===
-|Pros|Cons
+|Pros(优点)|Cons(缺点)
a|
* A message bus gives us a nice way to separate responsibilities when we have
to take multiple actions in response to a request.
+当我们需要对一个请求采取多个动作时,消息总线为我们提供了一种很好的方式来分离职责。
* Event handlers are nicely decoupled from the "core" application logic,
making it easy to change their implementation later.
+事件处理器与“核心”应用逻辑很好地解耦,这使得以后更改其实现变得容易。
* Domain events are a great way to model the real world, and we can use them
as part of our business language when modeling with stakeholders.
+领域事件是建模现实世界的一种绝佳方式,在与利益相关者进行建模时,我们可以将它们作为业务语言的一部分使用。
a|
@@ -798,6 +963,8 @@ a|
in which the unit of work raises events for us is _neat_ but also magic. It's not
obvious when we call `commit` that we're also going to go and send email to
people.
+消息总线是一个需要额外理解的组件;让工作单元为我们触发事件的实现方式虽然很_巧妙_,但也有些“魔法”感。当我们调用 `commit` 时,
+并不直观地让人联想到我们还会去给人们发送电子邮件。
* What's more, that hidden event-handling code executes _synchronously_,
meaning your service-layer function
@@ -805,15 +972,19 @@ a|
could cause unexpected performance problems in your web endpoints
(adding asynchronous processing is possible but makes things even _more_ confusing).
((("synchronous execution of event-handling code")))
+此外,这些隐藏的事件处理代码是_同步_执行的,这意味着你的服务层函数在任何事件的所有处理器完成之前都不会结束。
+这可能会在你的 Web 端点中引发意想不到的性能问题(添加异步处理是可能的,但会让事情变得更加_复杂_)。
* More generally, event-driven workflows can be confusing because after things
are split across a chain of multiple handlers, there is no single place
in the system where you can understand how a request will be fulfilled.
+更普遍地说,事件驱动的工作流可能会令人困惑,因为当处理被分散到多个处理器链中后,系统中就没有一个单一的位置可以让你清楚地了解一个请求是如何被完成的。
* You also open yourself up to the possibility of circular dependencies between your
event handlers, and infinite loops.
((("dependencies", "circular dependencies between event handlers")))
((("events and the message bus", startref="ix_evntMB")))
+你还可能会面临事件处理器之间出现循环依赖和无限循环的风险。
a|
|===
@@ -825,43 +996,55 @@ boundaries where we guarantee consistency. People often ask, "What
should I do if I need to change multiple aggregates as part of a request?" Now
we have the tools we need to answer that question.
+不过,事件的用途远不限于发送电子邮件。在 <> 中,我们花费了大量时间来说服你应该定义聚合,
+或者说定义那些我们可以保证一致性的边界。人们经常会问,“如果我需要在一个请求中修改多个聚合,我该怎么办?” 现在我们有了回答这个问题所需的工具。
+
If we have two things that can be transactionally isolated (e.g., an order and a
[.keep-together]#product#), then we can make them _eventually consistent_ by using events. When an
order is canceled, we should find the products that were allocated to it
and remove the [.keep-together]#allocations#.
+如果我们有两个可以在事务上隔离的对象(例如,一个订单和一个[.keep-together]#产品#),那么我们可以通过使用事件使它们_最终一致_。
+当一个订单被取消时,我们应该找到分配给它的产品并移除这些[.keep-together]#分配#。
+
[role="nobreakinside less_space"]
-.Domain Events and the Message Bus Recap
+.Domain Events and the Message Bus Recap(领域事件和消息总线总结)
*****************************************************************
((("events and the message bus", "domain events and message bus recap")))
((("message bus", "recap")))
-Events can help with the single responsibility principle::
+Events can help with the single responsibility principle(事件可以帮助贯彻单一职责原则)::
Code gets tangled up when we mix multiple concerns in one place. Events can
help us to keep things tidy by separating primary use cases from secondary
ones.
We also use events for communicating between aggregates so that we don't
need to run long-running transactions that lock against multiple tables.
+当我们将多个关注点混杂在一起时,代码就会变得复杂。事件可以通过将主要用例与次要用例分离来帮助我们保持代码简洁。
+我们还使用事件在聚合之间进行通信,这样就不需要运行会锁定多个表的长时间事务。
-A message bus routes messages to handlers::
+A message bus routes messages to handlers(消息总线将消息路由到处理器)::
You can think of a message bus as a dict that maps from events to their
consumers. It doesn't "know" anything about the meaning of events; it's just
a piece of dumb infrastructure for getting messages around the system.
+你可以将消息总线看作一个从事件映射到其消费者的字典。它并不“了解”事件的含义;它只是一个将消息在系统中分发的简单基础设施。
-Option 1: Service layer raises events and passes them to message bus::
+Option 1: Service layer raises events and passes them to message bus(选项 1:服务层触发事件并将其传递到消息总线)::
The simplest way to start using events in your system is to raise them from
handlers by calling `bus.handle(some_new_event)` after you commit your
unit of work.
((("service layer", "raising events and passing them to message bus")))
+在系统中开始使用事件的最简单方法是从处理器中触发它们,即在提交工作单元后调用 `bus.handle(some_new_event)`。
-Option 2: Domain model raises events, service layer passes them to message bus::
+Option 2: Domain model raises events, service layer passes them to message bus(选项 2:领域模型触发事件,服务层将它们传递到消息总线)::
The logic about when to raise an event really should live with the model, so
we can improve our system's design and testability by raising events from
the domain model. It's easy for our handlers to collect events off the model
objects after `commit` and pass them to the bus.
((("domain model", "raising events and service layer passing them to message bus")))
+关于何时触发事件的逻辑确实应该存在于模型中,因此通过从领域模型触发事件,我们可以改进系统的设计和测试性。在 `commit` 之后,
+处理器可以很容易地从模型对象中收集事件并将它们传递到消息总线。
-Option 3: UoW collects events from aggregates and passes them to message bus::
+Option 3: UoW collects events from aggregates and passes them to message bus(选项 3:工作单元从聚合中收集事件并将它们传递到消息总线)::
Adding `bus.handle(aggregate.events)` to every handler is annoying, so we
can tidy up by making our unit of work responsible for raising events that
were raised by loaded objects.
@@ -869,8 +1052,12 @@ Option 3: UoW collects events from aggregates and passes them to message bus::
and easy to use once it's set up.
((("aggregates", "UoW collecting events from and passing them to message bus")))
((("Unit of Work pattern", "UoW collecting events from aggregates and passing them to message bus")))
+在每个处理器中添加 `bus.handle(aggregate.events)` 会很繁琐,因此我们可以通过让工作单元(UoW)负责触发由已加载对象触发的事件来简化流程。
+虽然这是最复杂的设计,并且可能依赖于 ORM 的一些“魔法”,但一旦设置完成,它就会非常简洁且易于使用。
*****************************************************************
In <>, we'll look at this idea in more
detail as we build a more complex workflow with our new message bus.
+
+在 <> 中,我们将更详细地探讨这个想法,并使用我们的新消息总线构建一个更复杂的工作流。
From b3ded71e802eed9ca49eede8f7c68b0ac0d76396 Mon Sep 17 00:00:00 2001
From: fushall <19303416+fushall@users.noreply.github.com>
Date: Fri, 31 Jan 2025 11:31:01 +0800
Subject: [PATCH 15/75] update Readme.md chapter_09_all_messagebus.asciidoc
---
Readme.md | 4 +-
chapter_09_all_messagebus.asciidoc | 225 ++++++++++++++++++++++++++++-
2 files changed, 219 insertions(+), 10 deletions(-)
diff --git a/Readme.md b/Readme.md
index 25a37457..b1d623f2 100644
--- a/Readme.md
+++ b/Readme.md
@@ -28,8 +28,8 @@ O'Reilly 大方地表示,我们将能够以 [CC 许可证](license.txt) 发布
| [Chapter 7: Aggregates
第七章:聚合(已翻译)](chapter_07_aggregate.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [**Part 2 Intro
第二部分简介(已翻译)**](part2.asciidoc) | |
| [Chapter 8: Domain Events and a Simple Message Bus
第八章:领域事件与简单消息总线(已翻译)](chapter_08_events_and_message_bus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 9: Going to Town on the MessageBus
第九章:深入探讨消息总线(翻译中...)](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
-| [Chapter 10: Commands
第十章:命令(未翻译)](chapter_10_commands.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 9: Going to Town on the MessageBus
第九章:深入探讨消息总线(已翻译)](chapter_09_all_messagebus.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
+| [Chapter 10: Commands
第十章:命令(翻译中...)](chapter_10_commands.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 11: External Events for Integration
第十一章:集成外部事件(未翻译)](chapter_11_external_events.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 12: CQRS
第十二章:命令查询责任分离(未翻译)](chapter_12_cqrs.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
| [Chapter 13: Dependency Injection
第十三章:依赖注入(未翻译)](chapter_13_dependency_injection.asciidoc) | [](https://travis-ci.org/cosmicpython/code) |
diff --git a/chapter_09_all_messagebus.asciidoc b/chapter_09_all_messagebus.asciidoc
index 0ef9a65d..b4179fa6 100644
--- a/chapter_09_all_messagebus.asciidoc
+++ b/chapter_09_all_messagebus.asciidoc
@@ -1,5 +1,6 @@
[[chapter_09_all_messagebus]]
== Going to Town on the Message Bus
+大展身手应用消息总线
((("events and the message bus", "transforming our app into message processor", id="ix_evntMBMP")))
((("message bus", "before, message buse as optional add-on")))
@@ -8,8 +9,11 @@ structure of our application. We'll move from the current state in
<>, where events are an optional
side effect...
+在本章中,我们将使事件成为应用程序内部结构中更为基础的组成部分。我们将从 <> 的当前状态开始,
+在该状态下,事件只是一个可选的副作用...
+
[[maps_chapter_08_before]]
-.Before: the message bus is an optional add-on
+.Before: the message bus is an optional add-on(之前:消息总线是一个可选的附加功能)
image::images/apwp_0901.png[]
((("message bus", "now the main entrypoint to service layer")))
@@ -18,8 +22,11 @@ image::images/apwp_0901.png[]
everything goes via the message bus, and our app has been transformed
fundamentally into a message processor.
+...到 <> 中的情境,
+一切都通过消息总线,我们的应用程序从根本上被转换为一个消息处理器。
+
[[map_chapter_08_after]]
-.The message bus is now the main entrypoint to the service layer
+.The message bus is now the main entrypoint to the service layer(消息总线现在是服务层的主要入口点)
image::images/apwp_0902.png[]
@@ -28,6 +35,9 @@ image::images/apwp_0902.png[]
The code for this chapter is in the
chapter_09_all_messagebus branch https://oreil.ly/oKNkn[on GitHub]:
+本章的代码位于
+chapter_09_all_messagebus 分支 https://oreil.ly/oKNkn[在 GitHub 上]:
+
----
git clone https://github.com/cosmicpython/code.git
cd code
@@ -39,6 +49,7 @@ git checkout chapter_08_events_and_message_bus
[role="pagebreak-before less_space"]
=== A New Requirement Leads Us to a New Architecture
+一个新需求引导我们走向新架构
((("situated software")))
((("events and the message bus", "transforming our app into message processor", "new requirement and new architecture")))
@@ -46,16 +57,24 @@ Rich Hickey talks about _situated software,_ meaning software that runs for
extended periods of time, managing a real-world process. Examples include
warehouse-management systems, logistics schedulers, and payroll systems.
+Rich Hickey 谈到了 _situated software_(情境化软件),指的是运行较长时间并管理现实世界过程中事务的软件。
+例如,仓储管理系统、物流调度程序和薪资系统。
+
This software is tricky to write because unexpected things happen all the time
in the real world of physical objects and unreliable humans. For example:
+这种软件很难编写,因为在充满物理对象和不可靠的人工操作的现实世界中,总会发生意想不到的事情。例如:
+
* During a stock-take, we discover that three pass:[SPRINGY-MATTRESS]es have been
water damaged by a leaky roof.
+在盘点时,我们发现有三个 pass:[SPRINGY-MATTRESS] 因屋顶漏水而受损。
* A consignment of pass:[RELIABLE-FORK]s is missing the required documentation and is
held in customs for several weeks. Three pass:[RELIABLE-FORK]s subsequently fail safety
testing and are destroyed.
+一批 pass:[RELIABLE-FORK] 缺少必要的文件,被海关扣留了几周。随后,三件 pass:[RELIABLE-FORK] 未通过安全测试而被销毁。
* A global shortage of sequins means we're unable to manufacture our next batch
of pass:[SPARKLY-BOOKCASE].
+全球亮片短缺导致我们无法生产下一批 pass:[SPARKLY-BOOKCASE]。
((("batches", "batch quantities changed means deallocate and reallocate")))
In these types of situations, we learn about the need to change batch quantities
@@ -68,9 +87,12 @@ model elaboration.]
((("event storming")))
we model the situation as in <>.
+在这些类型的情境中,我们了解到需要在批次已经进入系统时修改其数量。可能是有人在清单上填写的数量有误,或者可能有些沙发从卡车上掉了下来。通过与业务部门的交流,脚注:[
+事件驱动建模非常流行,因此一种称为 _event storming_(事件风暴)的实践已经被开发出来,用于促进基于事件的需求收集和领域模型详解。]
+我们如同在 <> 中对情境进行建模。
[[batch_changed_events_flow_diagram]]
-.Batch quantity changed means deallocate and reallocate
+.Batch quantity changed means deallocate and reallocate(批次数量的变更意味着需要取消分配并重新分配)
image::images/apwp_0903.png[]
[role="image-source"]
----
@@ -90,6 +112,10 @@ quantity drops to less than the total already allocated, we need to
_deallocate_ those orders from that batch. Then each one will require
a new allocation, which we can capture as an event called `AllocationRequired`.
+一个我们称为 `BatchQuantityChanged` 的事件,应该让我们修改批次的数量,是的,但也需要应用一个 _业务规则_:
+如果新的数量减少到小于已分配总量的情况下,我们需要从该批次中 _取消分配_ 这些订单。然后,每个订单都将需要重新分配,
+我们可以将其记录为一个名为 `AllocationRequired` 的事件。
+
Perhaps you're already anticipating that our internal message bus and events can
help implement this requirement. We could define a service called
`change_batch_quantity` that knows how to adjust batch quantities and also how
@@ -99,17 +125,27 @@ service, in separate transactions. Once again, our message bus helps us to
enforce the single responsibility principle, and it allows us to make choices about
transactions and data integrity.
+或许你已经预想到,我们的内部消息总线和事件可以帮助实现这一需求。我们可以定义一个名为 `change_batch_quantity` 的服务,
+该服务既知道如何调整批次数量,也知道如何 _取消分配_ 多余的订单行。然后,每次取消分配都可以触发一个 `AllocationRequired` 事件,
+该事件可以在单独的事务中转发到现有的 `allocate` 服务中。再一次地,我们的消息总线帮助我们遵循了单一职责原则,
+并让我们能够对事务和数据完整性做出选择。
+
==== Imagining an Architecture Change: Everything Will Be an [.keep-together]#Event Handler#
+设想架构变更:一切都将成为事件处理器
((("event handlers", "imagined architecture in which everything is an event handler")))
((("events and the message bus", "transforming our app into message processor", "imagined architecture, everything will be an event handler")))
But before we jump in, think about where we're headed. There are two
kinds of flows through our system:
+但在我们开始之前,先思考一下我们的目标。我们的系统中有两种流程:
+
* API calls that are handled by a service-layer function
+由服务层函数处理的 API 调用
* Internal events (which might be raised as a side effect of a service-layer function)
and their handlers (which in turn call service-layer functions)
+内部事件(可能是服务层函数的副作用引发的)及其处理器(它们反过来调用服务层函数)。
((("service functions", "making them event handlers")))
Wouldn't it be easier if everything was an event handler? If we rethink our API
@@ -117,8 +153,12 @@ calls as capturing events, the service-layer functions can be event handlers
too, and we no longer need to make a distinction between internal and external
event handlers:
+如果一切都是事件处理器,那岂不是更简单?如果我们将 API 调用重新构想为捕获事件,那么服务层函数也可以是事件处理器,
+我们就不再需要区分内部和外部事件处理器了:
+
* `services.allocate()` could be the handler for an
`AllocationRequired` event and could emit `Allocated` events as its output.
+`services.allocate()` 可以作为 `AllocationRequired` 事件的处理器,并将 `Allocated` 事件作为其输出。
* `services.add_batch()` could be the handler for a `BatchCreated`
event.footnote:[If you've done a bit of reading about event-driven
@@ -127,18 +167,26 @@ event handlers:
In the <>, we'll introduce the distinction
between commands and events.]
((("BatchCreated event", "services.add_batch as handler for")))
+`services.add_batch()` 可以作为 `BatchCreated` 事件的处理器。脚注:[如果你对事件驱动架构有一些了解,你可能会觉得,
+“这里的一些事件听起来更像是命令!” 请耐心些!我们正在尝试一次引入一个概念。在 <> 中,
+我们将介绍命令与事件之间的区别。]
Our new requirement will fit the same pattern:
+我们的新需求也将符合相同的模式:
+
* An event called `BatchQuantityChanged` can invoke a handler called
`change_batch_quantity()`.
((("BatchQuantityChanged event", "invoking handler change_batch_quantity")))
+一个名为 `BatchQuantityChanged` 的事件可以调用一个名为 `change_batch_quantity()` 的处理器。
* And the new `AllocationRequired` events that it may raise can be passed on to
`services.allocate()` too, so there is no conceptual difference between a
brand-new allocation coming from the API and a reallocation that's
internally triggered by a deallocation.
((("AllocationRequired event", "passing to services.allocate")))
+而它可能引发的新 `AllocationRequired` 事件也可以传递给 `services.allocate()`,这样从概念上来说,
+来自 API 的全新分配和因取消分配而内部触发的重新分配之间就没有区别了。
((("preparatory refactoring workflow")))
@@ -146,26 +194,36 @@ All sound like a bit much? Let's work toward it all gradually. We'll
follow the https://oreil.ly/W3RZM[Preparatory Refactoring] workflow, aka "Make
the change easy; then make the easy change":
+听起来有点多?让我们逐步实现这一切。我们将遵循 https://oreil.ly/W3RZM[预备性重构] 的工作流程,也称为“让变更变得简单;然后进行简单的变更”:
+
1. We refactor our service layer into event handlers. We can
get used to the idea of events being the way we describe inputs to the
system. In particular, the existing `services.allocate()` function will
become the handler for an event called `AllocationRequired`.
+我们将服务层重构为事件处理器。我们可以逐渐适应使用事件来描述系统输入的方式。特别是,
+现有的 `services.allocate()` 函数将变成名为 `AllocationRequired` 的事件的处理器。
2. We build an end-to-end test that puts `BatchQuantityChanged` events
into the system and looks for `Allocated` events coming out.
+我们编写一个端到端测试,将 `BatchQuantityChanged` 事件输入系统,并检查输出的 `Allocated` 事件。
3. Our implementation will conceptually be very simple: a new
handler for `BatchQuantityChanged` events, whose implementation will emit
`AllocationRequired` events, which in turn will be handled by the exact same
handler for allocations that the API uses.
+我们的实现从概念上讲将非常简单:一个用于处理 `BatchQuantityChanged` 事件的新处理器,
+其实现将触发 `AllocationRequired` 事件,而这些事件将由与 API 使用的完全相同的分配处理器来处理。
Along the way, we'll make a small tweak to the message bus and UoW, moving the
responsibility for putting new events on the message bus into the message bus itself.
+在此过程中,我们将对消息总线和工作单元(UoW)进行一个小调整,将将新事件放入消息总线的职责转移到消息总线本身。
+
=== Refactoring Service Functions to Message Handlers
+将服务函数重构为消息处理器
((("events and the message bus", "transforming our app into message processor", "refactoring service functions to message handlers")))
((("service functions", "refactoring to message handlers")))
@@ -174,6 +232,8 @@ responsibility for putting new events on the message bus into the message bus it
We start by defining the two events that capture our current API
inputs—++AllocationRequired++ and `BatchCreated`:
+我们首先定义两个捕获当前 API 输入的事件——++AllocationRequired++ 和 `BatchCreated`:
+
[[two_new_events]]
.BatchCreated and AllocationRequired events (src/allocation/domain/events.py)
====
@@ -200,6 +260,10 @@ Then we rename _services.py_ to _handlers.py_; we add the existing message handl
for `send_out_of_stock_notification`; and most importantly, we change all the
handlers so that they have the same inputs, an event and a UoW:
+接着我们将 _services.py_ 重命名为 _handlers.py_;
+添加现有的 `send_out_of_stock_notification` 消息处理器;
+最重要的是,修改所有的处理器使它们具有相同的输入——一个事件和一个工作单元(UoW):
+
[[services_to_handlers]]
.Handlers and services are the same thing (src/allocation/service_layer/handlers.py)
@@ -237,6 +301,8 @@ def send_out_of_stock_notification(
The change might be clearer as a diff:
+这个更改通过差异(diff)可能会更清晰:
+
[[services_to_handlers_diff]]
.Changing from services to handlers (src/allocation/service_layer/handlers.py)
====
@@ -275,6 +341,8 @@ The change might be clearer as a diff:
Along the way, we've made our service-layer's API more structured and more consistent. It was a scattering of
primitives, and now it uses well-defined objects (see the following sidebar).
+在此过程中,我们使服务层的 API 更加结构化和一致化。原本是一些散乱的原始数据,现在则使用了定义良好的对象(请参见以下侧栏)。
+
[role="nobreakinside less_space"]
.From Domain Objects, via Primitive Obsession, to [.keep-together]#Events as an Interface#
*******************************************************************************
@@ -286,31 +354,48 @@ Some of you may remember <>, in which we changed our servic
from being in terms of domain objects to primitives. And now we're moving
back, but to different objects? What gives?
+你们中的一些人可能还记得 <>,当时我们将服务层 API 从基于领域对象改为使用原始类型。
+而现在我们又改回去了,但这次使用的是不同的对象?这意味着什么?
+
In OO circles, people talk about _primitive obsession_ as an antipattern: avoid
primitives in public APIs, and instead wrap them with custom value classes, they
would say. In the Python world, a lot of people would be quite skeptical of
that as a rule of thumb. When mindlessly applied, it's certainly a recipe for
unnecessary complexity. So that's not what we're doing per se.
+在面向对象(OO)圈子里,人们将 _primitive obsession_(原始类型痴迷)视为一种反模式:他们会建议在公共 API 中避免使用原始类型,
+而是用自定义的值类将其封装。在 _Python_ 世界中,许多人对这种经验法则持怀疑态度。不加思考地应用它,无疑会导致不必要的复杂性。
+所以,这并不是我们要做的事情。
+
The move from domain objects to primitives bought us a nice bit of decoupling:
our client code was no longer coupled directly to the domain, so the service
layer could present an API that stays the same even if we decide to make changes
to our model, and vice versa.
+从领域对象转向原始类型为我们带来了一点不错的解耦效果:我们的客户端代码不再直接与领域耦合,
+因此服务层可以提供一个即使我们决定更改模型也能保持不变的 API,反之亦然。
+
So have we gone backward? Well, our core domain model objects are still free to
vary, but instead we've coupled the external world to our event classes.
They're part of the domain too, but the hope is that they vary less often, so
they're a sensible artifact to couple on.
+那么我们是不是倒退了?其实不然:我们的核心领域模型对象依然可以自由变化,但我们将外部世界与事件类耦合在了一起。
+事件类也属于领域的一部分,但希望它们的变化频率较低,因此将它们用作耦合的目标是合理的选择。
+
And what have we bought ourselves? Now, when invoking a use case in our application,
we no longer need to remember a particular combination of primitives, but just a single
event class that represents the input to our application. That's conceptually
quite nice. On top of that, as you'll see in <>, those
event classes can be a nice place to do some input validation.
+
+那么我们得到了什么好处呢?现在,当在我们的应用中调用一个用例时,我们不再需要记住一组特定的原始类型组合,而只需处理一个代表应用输入的事件类。
+从概念上讲,这相当不错。除此之外,正如你将在 <> 中看到的,这些事件类也是一个很好的地方,用于进行输入验证。
*******************************************************************************
==== The Message Bus Now Collects Events from the UoW
+消息总线现在从工作单元(UoW)中收集事件
((("message bus", "now collecting events from UoW")))
((("Unit of Work pattern", "message bus now collecting events from UoW")))
@@ -322,6 +407,10 @@ between the UoW and message bus until now, so this will make it one-way. Instea
of having the UoW _push_ events onto the message bus, we will have the message
bus _pull_ events from the UoW.
+我们的事件处理器现在需要一个工作单元(UoW)。此外,随着消息总线在我们的应用中变得更加核心,将其明确负责收集和处理新事件也是合理的。
+到目前为止,工作单元和消息总线之间存在一定的循环依赖,这次修改将使其变为单向。与其让工作单元 _推送_ 事件到消息总线,
+我们将改为让消息总线从工作单元中 _拉取_ 事件。
+
[[handle_has_uow_and_queue]]
.Handle takes a UoW and manages a queue (src/allocation/service_layer/messagebus.py)
@@ -343,16 +432,23 @@ def handle(
====
<1> The message bus now gets passed the UoW each time it starts up.
+现在,每次消息总线启动时,都会将工作单元(UoW)传递给它。
<2> When we begin handling our first event, we start a queue.
+当我们开始处理第一个事件时,我们会启动一个队列。
<3> We pop events from the front of the queue and invoke their handlers (the
[.keep-together]#`HANDLERS`# dict hasn't changed; it still maps event types to handler functions).
+我们从队列的前端弹出事件并调用其处理器([.keep-together]#`HANDLERS`# 字典没有变化,它仍然将事件类型映射到处理器函数)。
<4> The message bus passes the UoW down to each handler.
+消息总线将工作单元(UoW)传递给每个处理器。
<5> After each handler finishes, we collect any new events that have been
generated and add them to the queue.
+每个处理器处理完成后,我们会收集所有已生成的新事件,并将它们添加到队列中。
In _unit_of_work.py_, `publish_events()` becomes a less active method,
`collect_new_events()`:
+在 _unit_of_work.py_ 中,`publish_events()` 变成了一个更少主动的方法,`collect_new_events()`:
+
[[uow_collect_new_events]]
.UoW no longer puts events directly on the bus (src/allocation/service_layer/unit_of_work.py)
@@ -381,10 +477,13 @@ In _unit_of_work.py_, `publish_events()` becomes a less active method,
====
<1> The `unit_of_work` module now no longer depends on `messagebus`.
+现在,`unit_of_work` 模块不再依赖于 `messagebus`。
<2> We no longer `publish_events` automatically on commit. The message bus
is keeping track of the event queue instead.
+我们不再在提交时自动调用 `publish_events`。消息总线现在负责跟踪事件队列。
<3> And the UoW no longer actively puts events on the message bus; it
just makes them available.
+工作单元(UoW)不再主动将事件放入消息总线;它只是提供了这些事件。
//IDEA: we can definitely get rid of _commit() now right?
// (EJ2) at this point _commit() doesn't serve any purpose, so it could be deleted.
@@ -392,12 +491,15 @@ In _unit_of_work.py_, `publish_events()` becomes a less active method,
[role="pagebreak-before less_space"]
==== Our Tests Are All Written in Terms of Events Too
+我们的测试现在也都是基于事件编写的
((("events and the message bus", "transforming our app into message processor", "tests writtern to in terms of events")))
((("testing", "tests written in terms of events")))
Our tests now operate by creating events and putting them on the
message bus, rather than invoking service-layer functions directly:
+我们的测试现在通过创建事件并将其放入消息总线来运行,而不是直接调用服务层函数:
+
[[handler_tests]]
.Handler tests use events (tests/unit/test_handlers.py)
@@ -434,6 +536,7 @@ class TestAddBatch:
[[temporary_ugly_hack]]
==== A Temporary Ugly Hack: The Message Bus Has to Return Results
+一个临时的丑陋解决方案:消息总线必须返回结果
((("events and the message bus", "transforming our app into message processor", "temporary hack, message bus returning results")))
((("message bus", "returning results in temporary hack")))
@@ -441,6 +544,9 @@ Our API and our service layer currently want to know the allocated batch referen
when they invoke our `allocate()` handler. This means we need to put in
a temporary hack on our message bus to let it return events:
+我们目前的 API 和服务层在调用 `allocate()` 处理器时需要知道已分配批次的引用。
+这意味着我们需要在消息总线上加入一个临时的解决方案,以使其能够返回事件:
+
[[hack_messagebus_results]]
.Message bus returns results (src/allocation/service_layer/messagebus.py)
====
@@ -470,8 +576,11 @@ a temporary hack on our message bus to let it return events:
It's because we're mixing the read and write responsibilities in our system.
We'll come back to fix this wart in <>.
+这是因为我们在系统中混合了读取和写入职责。我们会在 <> 中回过头来修复这个缺陷。
+
==== Modifying Our API to Work with Events
+修改我们的 API 以支持事件
[[flask_uses_messagebus]]
.Flask changing to message bus as a diff (src/allocation/entrypoints/flask_app.py)
@@ -497,29 +606,40 @@ We'll come back to fix this wart in <>.
<1> Instead of calling the service layer with a bunch of primitives extracted
from the request JSON...
+我们不再通过从请求 JSON 中提取的一堆原始数据来调用服务层...
<2> We instantiate an event.
+我们实例化一个事件。
<3> Then we pass it to the message bus.
+然后我们将其传递给消息总线。
And we should be back to a fully functional application, but one that's now
fully event-driven:
+这样我们就回到了一个完全功能性的应用程序,但现在它已经完全事件驱动了:
+
* What used to be service-layer functions are now event handlers.
+以前是服务层函数的部分现在变成了事件处理器。
* That makes them the same as the functions we invoke for handling internal events raised by
our domain model.
+这使得它们与我们在领域模型中处理内部事件时调用的函数相同。
* We use events as our data structure for capturing inputs to the system,
as well as for handing off of internal work packages.
+我们使用事件作为数据结构来捕获系统的输入,同时也用于传递内部工作包。
* The entire app is now best described as a message processor, or an event processor
if you prefer. We'll talk about the distinction in the
<>.
+整个应用程序现在最好被描述为一个消息处理器,或者如果你愿意的话,可以称为事件处理器。
+我们将在 <> 中讨论两者的区别。
=== Implementing Our New Requirement
+实现我们的新需求
((("reallocation", "sequence diagram for flow")))
((("events and the message bus", "transforming our app into message processor", "implementing the new requirement", id="ix_evntMBMPreq")))
@@ -529,9 +649,13 @@ inputs some new `BatchQuantityChanged` events and pass them to a handler, which
turn might emit some `AllocationRequired` events, and those in turn will go
back to our existing handler for reallocation.
+我们的重构阶段已经完成了。让我们看看是否真的“让变更变得简单”。
+现在来实现我们的新需求,如 <> 中所示:我们将接收一些新的 `BatchQuantityChanged` 事件作为输入,
+并将它们传递给处理器,而该处理器可能会触发一些 `AllocationRequired` 事件,而这些事件又将传递给我们现有的重新分配处理器。
+
[role="width-75"]
[[reallocation_sequence_diagram]]
-.Sequence diagram for reallocation flow
+.Sequence diagram for reallocation flow(重新分配流程的序列图)
image::images/apwp_0904.png[]
[role="image-source"]
----
@@ -562,15 +686,21 @@ WARNING: When you split things out like this across two units of work,
See <