Skip to content

Latest commit

 

History

History
1308 lines (1004 loc) · 60.8 KB

File metadata and controls

1308 lines (1004 loc) · 60.8 KB

Domain Modeling

领域模型

This chapter looks into how we can model business processes with code, in a way 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)。

A placeholder illustration of our domain model(我们领域模型的占位符示意图) 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.

A placeholder illustration of our domain model(我们领域模型的占位符示意图) 是我们领域模型模式的一个简单视觉占位符。 在本章中我们会填充一些细节,随着进入其他章节,我们会围绕领域模型构建内容, 但你始终应该能够在核心找到这些小形状。

apwp 0101
Figure 1. A placeholder illustration of our domain model(我们领域模型的占位符示意图)

What Is a Domain Model?

什么是领域模型?

In the introduction, we used the term business logic layer to describe the central layer of a three-layered architecture. For the rest of 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).

introduction 中,我们使用了术语 business logic layer(业务逻辑层)来描述三层架构的中心层。 在本书的其余部分,我们将改用术语 domain model(领域模型)。这是DDD(领域驱动设计)社区的一个术语, 它更能准确表达我们的意图(有关DDD的更多信息,请参阅下一个边栏)。

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 which system you’re talking about, the domain might be purchasing and 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”(领域)是一个较为花哨的说法,意思是“你试图解决的问题”。本书的作者目前为一家在线家具零售商工作。 根据你所讨论的系统不同,领域可能是采购与供应、产品设计,或者物流与交付。大多数程序员每天的工作是试图改进或自动化业务流程; 领域就是这些流程所支持的一组活动。

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 example, when someone throws a ball toward you, you’re able to predict its movement almost unconsciously, because you have a model of the way objects move in space. Your model isn’t perfect by any means. Humans have terrible 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 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.

几周之内,随着你们采用新的词汇来描述飞船的功能,你们的表达会变得更加精确:“增加三号货舱的氧气水平”或“启动小型推进器”。 再过几个月,你们可能已经为整个复杂的流程采用了新的语言:“启动着陆程序”或“准备跳跃”。 这一过程会非常自然地发生,而无需正式构建一个共享术语表的努力。

This Is Not a DDD Book. You Should Read a DDD Book.(这不是一本关于 DDD 的书。你应该读一本关于 DDD 的书。)

Domain-driven design, or DDD, popularized the concept of domain modeling,[1] and it’s been a hugely successful movement in transforming the way people design software by focusing on the core business domain. Many of the architecture patterns that we cover in this book—including Entity, Aggregate, Value Object (see [chapter_07_aggregate]), and Repository (in the next chapter)—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年代中期就已存在。] 通过专注于核心业务领域,DDD 在彻底改变人们的软件设计方式方面取得了巨大的成功。 本书中涵盖的许多架构模式——包括实体(Entity)、聚合(Aggregate)、值对象(Value Object, 详见 [chapter_07_aggregate])以及仓储(Repository,详见 the next chapter)——都源于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 所著的《领域驱动设计》(艾迪生-韦斯利专业出版社)。

  • The "red book," Implementing Domain-Driven Design by Vaughn Vernon (Addison-Wesley Professional) “红皮书”,Vaughn Vernon 所著的《实现领域驱动设计》(艾迪生-韦斯利专业出版社)。

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 Context diagram for the allocation service(分配服务的上下文图).

从宏观上看,我们有独立的系统分别负责采购库存、向客户销售库存以及向客户运输商品。 而中间的一个系统需要通过将库存分配给客户的订单来协调整个流程;详见 Context diagram for the allocation service(分配服务的上下文图)

apwp 0102
Figure 2. Context diagram for the allocation service(分配服务的上下文图)
[plantuml, apwp_0102]
@startuml Allocation Context Diagram
!include images/C4_Context.puml
scale 2

System(systema, "Allocation", "Allocates stock to customer orders")

Person(customer, "Customer", "Wants to buy furniture")
Person(buyer, "Buying Team", "Needs to purchase furniture from suppliers")

System(procurement, "Purchasing", "Manages workflow for buying stock from suppliers")
System(ecom, "Ecommerce", "Sells goods online")
System(warehouse, "Warehouse", "Manages workflow for shipping goods to customers")

Rel(buyer, procurement, "Uses")
Rel(procurement, systema, "Notifies about shipments")
Rel(customer, ecom, "Buys from")
Rel(ecom, systema, "Asks for stock levels")
Rel(ecom, systema, "Notifies about orders")
Rel_R(systema, warehouse, "Sends instructions to")
Rel_U(warehouse, customer, "Dispatches goods to")

@enduml

For the purposes of this book, we’re imagining that the business decides to implement an exciting new way of allocating stock. Until now, the 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

探索领域语言

Understanding the domain model takes time, and patience, and Post-it notes. We 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 术语中称为 通用语言(ubiquitous language) )来表达这些规则。我们为对象选择易于记忆的标识符,这样可以更方便地讨论这些示例。

The following sidebar shows some notes we might have taken while having a conversation with our domain experts about allocation.

以下侧边栏 展示了我们在与领域专家讨论分配时可能记录的一些笔记。

Some Notes on 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:

产品(product) 通过 SKU(读作“思 硌优”,是库存管理单元的缩写)进行标识。客户(Customer) 会下达 订单(order) 。一个订单通过一个 订单引用(order reference) 来标识, 并包含多个 订单项(order line) ,每个订单项都有一个 SKU数量(quantity) 。例如:

  • 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.

采购部门会订购小的 批次(batch) 库存。一个 批次(batch) 库存具备一个名为 引用(reference) 的唯一 ID、一个 SKU 和一个 数量(quantity)

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:

我们需要将 订单项(order line) 分配(allocate)批次(batch) 。当我们将某条订单项分配到某个批次时,我们会从该特定批次发送库存到客户的配送地址。 当我们将 x 单位的库存分配到一个批次时,该批次的 可用数量(available quantity) 会减少 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(预计到达时间) ,否则可能在 仓库库存(warehouse stock) 中。 我们优先将订单分配给仓库库存,而不是运输批次。对于运输批次,我们按预计到达时间最早的顺序进行分配。

Unit Testing Domain Models

领域模型的单元测试

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的工作原理,但我们想向你展示我们如何从这场业务对话中构建模型。

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 placeholder unit tests on GitHub, but you could just start from scratch, or combine/rewrite them however you like.

你会在 GitHub 上找到一些占位单元测试, 但你也可以从头开始,或者随意组合/重写它们。

Here’s what one of our first tests might look like:

以下是我们最初的一个测试可能的样子:

Example 1. A first test for allocation (test_batches.py)(一个关于分配的初步测试)
def test_allocating_to_a_batch_reduces_the_available_quantity():
    batch = Batch("batch-001", "SMALL-TABLE", qty=20, eta=date.today())
    line = OrderLine("order-ref", "SMALL-TABLE", 2)

    batch.allocate(line)

    assert batch.available_quantity == 18

The name of our unit test describes the behavior that we want to see from the system, and the names of the classes and variables that we use are taken from the business jargon. We could show this code to our nontechnical coworkers, and they would agree that this correctly describes the behavior of the system.

我们的单元测试名称描述了我们期望系统表现出的行为,而我们使用的类名和变量名来源于业务术语。 我们可以将这段代码展示给我们的非技术同事,他们会认可这段代码正确地描述了系统的行为。

And here is a domain model that meets our requirements:

以下是一个符合我们需求的领域模型:

Example 2. First cut of a domain model for batches (model.py)(批次领域模型的初步构建)
@dataclass(frozen=True)  #(1)(2)
class OrderLine:
    orderid: str
    sku: str
    qty: int


class Batch:
    def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):  #(2)
        self.reference = ref
        self.sku = sku
        self.eta = eta
        self.available_quantity = qty

    def allocate(self, line: OrderLine):  #(3)
        self.available_quantity -= line.qty
  1. OrderLine is an immutable dataclass with no behavior.[2] OrderLine 是一个不可变的 dataclass,没有任何行为。脚注:[在早期版本的 Python 中, 我们可能会使用 namedtuple。你也可以去了解一下 Hynek Schlawack 出色的 attrs。]

  2. We’re not showing imports in most code listings, in an attempt to keep them clean. We’re hoping you can guess that this came via from dataclasses import dataclass; likewise, typing.Optional and datetime.date. If you want to double-check anything, you can see the full working code for each chapter in its branch (e.g., chapter_01_domain_model). 在大多数代码清单中,我们没有展示导入内容,以尽量保持简洁。我们希望你能猜到这是通过 from dataclasses import dataclass 引入的; 同样的还有 typing.Optionaldatetime.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. 类型提示在 Python 世界中仍然是一个有争议的话题。对于领域模型来说,它们有时可以帮助澄清或记录预期的参数是什么, 而使用 IDE 的人通常会对此表示感激。不过你可能会认为为此付出的可读性代价过高。

Our implementation here is trivial: a Batch just wraps an integer available_quantity, and we decrement that value on allocation. We’ve written quite a lot of code just to subtract one number from another, but we think that modeling our domain precisely will pay off.[3]

我们的实现非常简单: 一个 Batch 只是包装了一个整数 available_quantity, 我们在分配时对这个值进行递减。 我们写了相当多的代码,只是为了实现从一个数字中减去另一个数字, 但我们认为,精确地建模我们的领域会有所回报。脚注: [或者你认为代码还不够? 那是否应该加入某种检查,用于验证 OrderLine 中的 SKU 是否匹配 Batch.sku? 关于校验的一些想法,我们保存在了 [appendix_validation] 中。]

Let’s write some new failing tests:

让我们编写一些新的失败测试:

Example 3. Testing logic for what we can allocate (test_batches.py)(测试可分配内容的逻辑)
def make_batch_and_line(sku, batch_qty, line_qty):
    return (
        Batch("batch-001", sku, batch_qty, eta=date.today()),
        OrderLine("order-123", sku, line_qty),
    )

def test_can_allocate_if_available_greater_than_required():
    large_batch, small_line = make_batch_and_line("ELEGANT-LAMP", 20, 2)
    assert large_batch.can_allocate(small_line)

def test_cannot_allocate_if_available_smaller_than_required():
    small_batch, large_line = make_batch_and_line("ELEGANT-LAMP", 2, 20)
    assert small_batch.can_allocate(large_line) is False

def test_can_allocate_if_available_equal_to_required():
    batch, line = make_batch_and_line("ELEGANT-LAMP", 2, 2)
    assert batch.can_allocate(line)

def test_cannot_allocate_if_skus_do_not_match():
    batch = Batch("batch-001", "UNCOMFORTABLE-CHAIR", 100, eta=None)
    different_sku_line = OrderLine("order-123", "EXPENSIVE-TOASTER", 10)
    assert batch.can_allocate(different_sku_line) is False

There’s nothing too unexpected here. We’ve refactored our test suite so that we don’t keep repeating the same lines of code to create a batch and a line for 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:

我们也可以通过编写 Batchcan_allocate 方法来简单直接地实现这一点:

Example 4. A new method in the model (model.py)(模型中的一个新方法)
    def can_allocate(self, line: OrderLine) -> bool:
        return self.sku == line.sku and self.available_quantity >= line.qty

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() 测试时,我们将不得不采用一个更智能的解决方案:

Example 5. This test is going to require a smarter model (test_batches.py)(此测试将需要一个更智能的模型)
def test_can_only_deallocate_allocated_lines():
    batch, unallocated_line = make_batch_and_line("DECORATIVE-TRINKET", 20, 2)
    batch.deallocate(unallocated_line)
    assert batch.available_quantity == 20

In this test, we’re asserting that deallocating a line from a batch has no effect unless the batch previously allocated the line. For this to work, our Batch needs to understand which lines have been allocated. Let’s look at the implementation:

在这个测试中,我们断言从批次中解除一个订单项分配没有任何效果,除非该批次之前已经分配了该订单项。为了实现这一点, 我们的 Batch 需要了解哪些订单项已被分配。让我们来看一下实现:

Example 6. The domain model now tracks allocations (model.py)(领域模型现在能够跟踪分配情况)
class Batch:
    def __init__(self, ref: str, sku: str, qty: int, eta: Optional[date]):
        self.reference = ref
        self.sku = sku
        self.eta = eta
        self._purchased_quantity = qty
        self._allocations = set()  # type: Set[OrderLine]

    def allocate(self, line: OrderLine):
        if self.can_allocate(line):
            self._allocations.add(line)

    def deallocate(self, line: OrderLine):
        if line in self._allocations:
            self._allocations.remove(line)

    @property
    def allocated_quantity(self) -> int:
        return sum(line.qty for line in self._allocations)

    @property
    def available_quantity(self) -> int:
        return self._purchased_quantity - self.allocated_quantity

    def can_allocate(self, line: OrderLine) -> bool:
        return self.sku == line.sku and self.available_quantity >= line.qty
apwp 0103
Figure 3. Our model in UML(我们的模型以 UML 表示)
[plantuml, apwp_0103, config=plantuml.cfg]
@startuml
scale 4

left to right direction
hide empty members

class Batch {
    reference
    sku
    eta
    _purchased_quantity
    _allocations
}

class OrderLine {
    orderid
    sku
    qty
}

Batch::_allocations o-- OrderLine

Now we’re getting somewhere! A batch now keeps track of a set of allocated OrderLine objects. When we allocate, if we have enough available quantity, we 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 使我们可以轻松处理最后一个测试,因为集合中的元素是唯一的:

Example 7. Last batch test! (test_batches.py)(最后一个批次测试!)
def test_allocation_is_idempotent():
    batch, line = make_batch_and_line("ANGULAR-DESK", 20, 2)
    batch.allocate(line)
    batch.allocate(line)
    assert batch.available_quantity == 18

At the moment, it’s probably a valid criticism to say that the domain model is too trivial to bother with DDD (or even object orientation!). In real life, any number of business rules and edge cases crop up: customers can ask for delivery on specific future dates, which means we might not want to allocate them to the earliest batch. Some SKUs aren’t in batches, but ordered on demand directly from suppliers, so they have different logic. Depending on the customer’s location, we can allocate to only a subset of warehouses and shipments that are in their region—except for some SKUs we’re happy to deliver from a 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 see how sticking rigidly to our principles of encapsulation and careful layering will help us to avoid a ball of mud.

不过,我们将把这个简单的领域模型作为更复杂事物的占位符,并在本书的其余部分扩展这个简单的领域模型, 将其融入真实世界中的 APIs、数据库和电子表格。我们会看到,坚持封装原则和精心设计的分层结构,将如何帮助我们避免陷入一团混乱。

More Types for More 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 将原始类型包装起来:

Example 8. Just taking it way too far, Bob(这也太过分了,Bob)
from dataclasses import dataclass
from typing import NewType

Quantity = NewType("Quantity", int)
Sku = NewType("Sku", str)
Reference = NewType("Reference", str)
...

class Batch:
    def __init__(self, ref: Reference, sku: Sku, qty: Quantity):
        self.sku = sku
        self.reference = ref
        self._purchased_quantity = qty

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.[4]

你认为这是绝妙的还是糟糕的,这方面见仁见智。脚注:[这是糟糕的,拜托,千万别这么做。——Harry]

Dataclasses Are Great for Value Objects

数据类非常适合作为值对象

We’ve used line liberally in the previous code listings, but what is a 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 文件可能如下所示:

Example 9. Order info as YAML(以YAML格式表示的订单信息)
Order_reference: 12345
Lines:
  - sku: RED-CHAIR
    qty: 25
  - sku: BLU-CHAIR
    qty: 25
  - sku: GRN-CHAIR
    qty: 25

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 类中,它也无法唯一标识订单项本身。)

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)模式来表示它。 一个 值对象 是能够由其持有的数据唯一标识的领域对象;我们通常将它们设计为不可变的:

Example 10. OrderLine is a value object(OrderLine 是一个值对象)
@dataclass(frozen=True)
class OrderLine:
    orderid: OrderReference
    sku: ProductReference
    qty: Quantity

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),这是一个高大上的说法, 用来表达:“两个具有相同 orderidskuqty 的订单项是相等的。”

Example 11. More examples of value objects(更多值对象的示例)
from dataclasses import dataclass
from typing import NamedTuple
from collections import namedtuple

@dataclass(frozen=True)
class Name:
    first_name: str
    surname: str

class Money(NamedTuple):
    currency: str
    value: int

Line = namedtuple('Line', ['sku', 'qty'])

def test_equality():
    assert Money('gbp', 10) == Money('gbp', 10)
    assert Name('Harry', 'Percival') != Name('Bob', 'Gregory')
    assert Line('RED-CHAIR', 5) == Line('RED-CHAIR', 5)

These value objects match our real-world intuition about how their values work. It doesn’t matter which £10 note we’re talking about, because they all have the same value. Likewise, two names are equal if both the first and last names match; and two lines are equivalent if they have the same customer order, 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英镑纸币并不重要,因为它们的面值是相同的。 同样地,如果名字和姓氏都相同,那么两个姓名就是相等的;而如果两个订单项具有相同的客户订单、产品代码和数量,它们也是等价的。 不过,值对象仍然可以具有复杂的行为。事实上,支持基于值的操作是很常见的,比如数学运算符操作:

Example 12. Testing Math with value objects(使用值对象测试数学运算)
fiver = Money('gbp', 5)
tenner = Money('gbp', 10)

def can_add_money_values_for_the_same_currency():
    assert fiver + fiver == tenner

def can_subtract_money_values():
    assert tenner - fiver == fiver

def adding_different_currencies_fails():
    with pytest.raises(ValueError):
        Money('usd', 10) + Money('gbp', 10)

def can_multiply_money_by_a_number():
    assert fiver * 5 == Money('gbp', 25)

def multiplying_two_money_values_is_an_error():
    with pytest.raises(TypeError):
        tenner * fiver

To get those tests to actually pass you’ll need to start implementing some magic methods on our Money class:

为了让那些测试真正通过,你需要开始在我们的 Money 类上实现一些魔术方法:

Example 13. Implementing Math with value objects(使用值对象实现数学运算)
@dataclass(frozen=True)
class Money:
    currency: str
    value: int

    def __add__(self, other) -> Money:
        if other.currency != self.currency:
            raise ValueError(f"Cannot add {self.currency} to {other.currency}")
        return Money(self.currency, self.value + other.value)

Value Objects and Entities

值对象与实体

An order line is uniquely identified by its order ID, SKU, and quantity; if we change one of those values, we now have a new line. That’s the definition of a 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)标识的。

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:

Example 14. A name itself cannot change…​(名字本身无法改变…​)
def test_name_equality():
    assert Name("Harry", "Percival") != Name("Barry", "Percival")

But what about Harry as a person? People do change their names, and their 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 呢?人可以改变他们的名字、婚姻状况,甚至性别,但是我们仍然将他们视为同一个个体。 这是因为人类与名字不同,拥有一个持久的 身份

Example 15. But a person can!(但一个人可以!)
class Person:

    def __init__(self, name: Name):
        self.name = name


def test_barry_is_harry():
    harry = Person(Name("Harry", "Percival"))
    barry = harry

    barry.name = Name("Barry", "Percival")

    assert harry is barry and barry is harry

Entities, unlike values, have identity equality. We can change their values, 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)是实体。我们可以将订单项分配到一个批次,或者更改我们期望它到达的日期,但它仍然是同一个实体。

We usually make this explicit in code by implementing equality operators on entities:

我们通常通过在实体上实现相等运算符来在代码中显式表达这一点:

Example 16. Implementing equality operators (model.py)(实现等价运算符)
class Batch:
    ...

    def __eq__(self, other):
        if not isinstance(other, Batch):
            return False
        return other.reference == self.reference

    def __hash__(self):
        return hash(self.reference)

Python’s __eq__ magic method defines the behavior of the class for the == operator.[5]

Python__eq__ 魔术方法定义了类在 == 运算符下的行为。 脚注:[__eq__ 方法的发音是“dunder-EQ”(双下划线 EQ),至少对某些人来说是这样的。]

For both entity and value objects, it’s also worth thinking through how __hash__ will work. It’s the magic method Python uses to control the behavior of objects when you add them to sets or use them as dict keys; you can find more info in the Python docs.

对于实体和值对象,同样值得深入思考 __hash__ 的工作原理。这是 Python 用来控制对象在被添加到 集合(sets)中或用作字典(dict)键时行为的魔术方法;更多信息可以参考 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 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. "Python Hashes and Equality" by our tech reviewer Hynek Schlawack is a good place to start. 这是一个棘手的领域;如果你修改了 __hash__,同时也需要修改 __eq__。 如果你不确定自己在做什么,建议进一步阅读相关内容。可以从我们的技术审阅者 Hynek Schlawack 所著的 《Python Hashes and Equality》 开始学习。

Not Everything Has to Be an Object: A Domain Service Function

并不是所有东西都必须是对象:领域服务函数

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. 我们已经创建了一个用于表示批次的模型,但我们实际需要做的是将订单项分配到表示我们所有库存的一组特定批次中。

Sometimes, it just isn’t a thing. 有时候,它根本就不需要是一个“东西”。

— Eric Evans
Domain-Driven Design

Evans discusses the idea of Domain Service operations that don’t have a natural home in an entity or value object.[6] A thing that allocates an order line, given a set of batches, sounds a lot like a function, and we can take advantage of the fact that Python is a multiparadigm language and just make it a function.

Evans 讨论了领域服务(Domain Service)的操作,这些操作在实体或值对象中没有一个自然的归宿。 脚注:[领域服务与服务层中的服务并不是同一个概念,尽管它们常常密切相关。 领域服务代表的是一个业务概念或流程,而服务层服务代表的是应用程序的一个用例。通常服务层会调用领域服务。] 一个用于在给定一组批次的情况下分配订单项的“东西”,听起来更像是一个函数。我们可以利用 Python 是一种多范式语言的特点, 直接将其实现为一个函数。

Let’s see how we might test-drive such a function:

让我们来看一下如何通过测试驱动的方式构建这样一个函数:

Example 17. Testing our domain service (test_allocate.py)(测试我们的领域服务)
def test_prefers_current_stock_batches_to_shipments():
    in_stock_batch = Batch("in-stock-batch", "RETRO-CLOCK", 100, eta=None)
    shipment_batch = Batch("shipment-batch", "RETRO-CLOCK", 100, eta=tomorrow)
    line = OrderLine("oref", "RETRO-CLOCK", 10)

    allocate(line, [in_stock_batch, shipment_batch])

    assert in_stock_batch.available_quantity == 90
    assert shipment_batch.available_quantity == 100


def test_prefers_earlier_batches():
    earliest = Batch("speedy-batch", "MINIMALIST-SPOON", 100, eta=today)
    medium = Batch("normal-batch", "MINIMALIST-SPOON", 100, eta=tomorrow)
    latest = Batch("slow-batch", "MINIMALIST-SPOON", 100, eta=later)
    line = OrderLine("order1", "MINIMALIST-SPOON", 10)

    allocate(line, [medium, earliest, latest])

    assert earliest.available_quantity == 90
    assert medium.available_quantity == 100
    assert latest.available_quantity == 100


def test_returns_allocated_batch_ref():
    in_stock_batch = Batch("in-stock-batch-ref", "HIGHBROW-POSTER", 100, eta=None)
    shipment_batch = Batch("shipment-batch-ref", "HIGHBROW-POSTER", 100, eta=tomorrow)
    line = OrderLine("oref", "HIGHBROW-POSTER", 10)
    allocation = allocate(line, [in_stock_batch, shipment_batch])
    assert allocation == in_stock_batch.reference

And our service might look like this:

我们的服务可能看起来像这样:

Example 18. A standalone function for our domain service (model.py)(为我们的领域服务创建一个独立函数)
def allocate(line: OrderLine, batches: List[Batch]) -> str:
    batch = next(b for b in sorted(batches) if b.can_allocate(line))
    batch.allocate(line)
    return batch.reference

Python’s Magic Methods Let Us Use Our Models with Idiomatic Python

Python 的魔法方法让我们可以用惯用的 Python 风格来使用我们的模型

You may or may not like the use of next() in the preceding code, but we’re pretty 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__

Example 19. Magic methods can express domain semantics (model.py)(魔术方法可以表达领域语义)
class Batch:
    ...

    def __gt__(self, other):
        if self.eta is None:
            return False
        if other.eta is None:
            return True
        return self.eta > other.eta

That’s lovely.

那真是太好了。

Exceptions Can Express Domain Concepts Too

异常也可以表达领域概念

We have one final concept to cover: exceptions can be used to express domain 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:

我们还有一个最后的概念需要探讨:异常也可以用来表达领域概念。在与领域专家的交流中,我们了解到订单可能无法分配的情况, 因为我们处于 缺货 状态,我们可以通过使用 领域异常 来捕获这种情况:

Example 20. Testing out-of-stock exception (test_allocate.py)(测试缺货异常)
def test_raises_out_of_stock_exception_if_cannot_allocate():
    batch = Batch("batch1", "SMALL-FORK", 10, eta=today)
    allocate(OrderLine("order1", "SMALL-FORK", 10), [batch])

    with pytest.raises(OutOfStock, match="SMALL-FORK"):
        allocate(OrderLine("order2", "SMALL-FORK", 1), [batch])
Domain Modeling Recap(领域建模回顾)
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. 这是你的代码中最贴近业务的部分,也是最有可能发生变化的地方,同时也是你为业务带来最大价值的地方。确保它易于理解和修改。

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, an entity has attributes that may vary over time and it will still be the same entity. It’s important to define what does uniquely identify an entity (usually some sort of name or reference field). 值对象由其属性定义。通常最好将其实现为不可变类型。如果你更改值对象的一个属性,它就代表了一个不同的对象。 相比之下,实体的属性可能会随时间变化,但它仍然是同一个实体。关键是要定义清楚是什么 确实 唯一标识一个实体(通常是某种名称或引用字段)。

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. Python 是一门多范式语言,所以让代码中的“动词”成为函数。对于每一个 FooManagerBarBuilderBazFactory, 通常可以找到更加具有表现力和可读性的 manage_foo()build_bar()get_baz() 来代替。

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. 重新审视 SOLID 原则以及其他优秀的设计启发,比如“有一个(Has-a) vs 是一个(Is-a)”、“优先使用组合而非继承”等等。

You’ll also want to think about consistency boundaries and aggregates(你还需要考虑一致性边界和聚合)

But that’s a topic for [chapter_07_aggregate]. 但这是 [chapter_07_aggregate] 的主题。

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:

我们不会通过过多的实现细节让你感到枯燥,但需要注意的主要一点是,我们在通用语言中命名异常时, 与命名我们的实体、值对象和服务一样,需格外用心:

Example 21. Raising a domain exception (model.py)(抛出领域异常)
class OutOfStock(Exception):
    pass


def allocate(line: OrderLine, batches: List[Batch]) -> str:
    try:
        batch = next(
        ...
    except StopIteration:
        raise OutOfStock(f"Out of stock for sku {line.sku}")

Our domain model at the end of the chapter(本章末尾的领域模型) is a visual representation of where we’ve ended up.

apwp 0104
Figure 4. Our domain model at the end of the chapter(本章末尾的领域模型)

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…​

到这里应该差不多了!我们已经有了一个可以用于首个用例的领域服务。但首先,我们需要一个数据库…​


1. DDD did not originate domain modeling. Eric Evans refers to the 2002 book Object Design by Rebecca Wirfs-Brock and Alan McKean (Addison-Wesley Professional), which introduced responsibility-driven design, of which DDD is a special case dealing with the domain. But even that is too late, and OO enthusiasts will tell you to look further back to Ivar Jacobson and Grady Booch; the term has been around since the mid-1980s.
2. In previous Python versions, we might have used a namedtuple. You could also check out Hynek Schlawack’s excellent attrs.
3. 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 [appendix_validation].
4. It is appalling. Please, please don’t do this. —Harry
5. The __eq__ method is pronounced "dunder-EQ." By some, at least.
6. Domain services are not the same thing as the services from the service layer, although they are often closely related. A domain service represents a business concept or process, whereas a service-layer service represents a use case for your application. Often the service layer will call a domain service.