buy the book ribbon

1: Domain Modeling第 1 章:领域建模

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 高度兼容。我们将讨论为什么领域建模很重要,并介绍几种关键的建模模式:实体(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.我们的领域模型占位图是领域模型模式的一个简单视觉占位符。在本章中,我们将填充一些细节,随着后续章节的推进,我们将围绕领域模型构建各种功能,但你应该始终能在核心位置找到这些小形状。

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

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).在引言中,我们使用“业务逻辑层”来描述三层架构中的核心层。在本书接下来的部分,我们将改用“领域模型”这一术语。这是 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.“领域”是一个时髦的说法,指的是你试图解决的问题。作者目前为一家在线家具零售商工作。根据你所讨论的系统,领域可能是采购与供应、产品设计,或者是物流与配送。大多数程序员每天都在努力改进或自动化业务流程;领域就是这些流程所支持的一系列活动。

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.“模型”是对流程或现象的一种映射,它捕捉了某个有用的属性。人类非常擅长在脑海中构建事物的模型。例如,当有人向你扔球时,你几乎可以在下意识中预测它的运动轨迹,因为你拥有一个关于物体在空间中运动的模型。你的模型绝非完美。人类对于物体在接近光速或真空中如何表现的直觉非常糟糕,因为我们的模型从未被设计用于涵盖这些情况。这并不意味着模型是错误的,但确实意味着某些预测超出了它的领域范围。

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.领域驱动设计(DDD)普及了领域建模的概念[1],通过关注核心业务领域,它在改变人们设计软件的方式方面取得了巨大的成功。我们在本书中介绍的许多架构模式——包括实体(Entity)、聚合(Aggregate)、值对象(Value Object,参见 [chapter_07_aggregate])和仓库(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 所著的《领域驱动设计》(Addison-Wesley Professional 出版)

  • The "red book," Implementing Domain-Driven Design by Vaughn Vernon (Addison-Wesley Professional)“红皮书”:Vaughn Vernon 所著的《实现领域驱动设计》(Addison-Wesley Professional 出版)

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.从高层来看,我们有独立的系统负责采购库存、向客户销售库存以及向客户发货。中间的一个系统需要通过将库存分配给客户订单来协调这一流程;请参阅分配服务的上下文图。

apwp 0102
Figure 2. Context diagram for the allocation service图 2. 分配服务的上下文图
[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 术语中的通用语言)来表达这些规则。我们为对象选择易于记忆的标识符,以便更容易讨论这些示例。

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:产品通过 SKU(发音为“skew”,即库存单位的缩写)进行标识。客户下单。订单由订单参考号标识,包含多个订单行,每行都有一个 SKU 和数量。例如:

  • 10 units of RED-CHAIR10 个 RED-CHAIR(红色椅子)

  • 1 unit of TASTELESS-LAMP1 个 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:我们需要将订单行分配给批次。当我们把一个订单行分配给一个批次后,我们将从该特定批次发送库存到客户的送货地址。当我们向一个批次分配 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领域模型的单元测试

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:这是我们第一个测试的样子:

A first test for allocation (test_batches.py)分配的第一个测试 (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:以下是满足我们要求的领域模型:

First cut of a domain model for batches (model.py)批次领域模型的初步版本 (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,没有任何行为[2]。

  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.Optional 和 datetime.date。如果你想仔细检查任何内容,可以在对应章节的分支中查看完整的可运行代码(例如 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,我们在分配时会递减该值。为了实现两个数字相减,我们写了相当多的代码,但我们认为精确地对领域进行建模是值得的[3]。

Let’s write some new failing tests:让我们编写一些新的失败测试:

Testing logic for what we can allocate (test_batches.py)测试可分配逻辑 (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:我们也可以通过编写 Batch 的 can_allocate 方法来直接实现这一点:

A new method in the model (model.py)模型中的一个新方法 (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() 测试时,我们将被迫采用更智能的解决方案:

This test is going to require a smarter model (test_batches.py)这个测试将需要一个更智能的模型 (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 需要了解哪些行已被分配。让我们看看实现:

The domain model now tracks allocations (model.py)领域模型现在跟踪分配 (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

Our model in UML shows the model in UML.我们的 UML 领域模型展示了 UML 中的模型。

apwp 0103
Figure 3. Our model in 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:顺便说一下,使用集合来存储 ._allocations 使我们能够轻松处理最后一个测试,因为集合中的项是唯一的:

Last batch test! (test_batches.py)最后一个批次测试! (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.但是,将这个简单的领域模型作为更复杂事物的占位符,我们将在本书的其余部分扩展它,并将其插入到 API、数据库和电子表格的真实世界中。我们将看到,严格遵守封装和仔细分层的原则将如何帮助我们避免陷入“大泥球”架构。

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 来包装原始类型:

Just taking it way too far, BobBob,这有点太过分了

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]你认为这很棒还是糟糕,这还有待商榷[4]。

Dataclasses Are Great for Value ObjectsDataclasses 非常适合值对象

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?在我们的业务语言中,订单有多个行项目,每行都有一个 SKU 和数量。我们可以想象一个包含订单信息的简单 YAML 文件可能如下所示:

Order info as YAML

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.)请注意,虽然订单有一个唯一标识它的参考号,但行却没有。(即使我们向 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:每当我们有一个包含数据但没有身份的业务概念时,我们通常选择使用值对象模式来表示它。值对象是任何由其持有的数据唯一标识的领域对象;我们通常使它们不可变:

OrderLine is a value objectOrderLine 是一个值对象

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."dataclasses(或 namedtuples)带给我们的好处之一是值相等性,这是一种时髦的说法,即“两个具有相同 orderid、sku 和 qty 的行是相等的”。

More examples of value objects值对象的更多示例

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 英镑的钞票并不重要,因为它们具有相同的价值。同样,如果名字和姓氏都匹配,那么两个名字就是相等的;如果两个行具有相同的客户订单、产品代码和数量,它们就是等价的。不过,我们仍然可以在值对象上实现复杂的行为。事实上,支持对值进行操作是很常见的;例如,数学运算符:

Testing Math with value objects测试值对象的数学运算

To get those tests to actually pass you’ll need to start implementing some magic methods on our Money class:要使这些测试真正通过,你需要开始在我们的 Money 类上实现一些魔法方法:

Implementing Math with value objects用值对象实现数学运算

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 和数量唯一标识;如果我们更改其中一个值,我们就得到了一个新的行。这就是值对象的定义:任何仅由其数据标识且没有长效身份的对象。那么批次呢?它由参考号标识。

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.我们使用“实体”一词来描述具有长效身份的领域对象。在上一页,我们引入了一个 Name 类作为值对象。如果我们取 Harry Percival 这个名字并更改一个字母,我们就得到了一个新的 Name 对象 Barry Percival。

It should be clear that Harry Percival is not equal to Barry Percival:显而易见,Harry Percival 不等于 Barry Percival:

A name itself cannot change…​名字本身是不能改变的……

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 这个人呢?人们确实会改变他们的名字、婚姻状况,甚至是性别,但我们仍然认为他们是同一个人。这是因为人类,不像名字,具有持久的身份:

But a person can!但人是可以改变的!

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.实体与值对象不同,具有身份相等性。我们可以改变它们的值,它们仍然可以被识别为同一个事物。在我们的示例中,批次就是实体。我们可以将行分配给批次,或者更改预计到达日期,它仍然是同一个实体。

We usually make this explicit in code by implementing equality operators on entities:我们通常通过在实体上实现相等运算符来在代码中明确这一点:

Implementing equality operators (model.py)实现相等运算符 (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__ 魔法方法定义了类对于 == 运算符的行为[5]。

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 用来控制对象在添加到集合或用作字典键时的行为的魔法方法;你可以在 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.对于值对象,哈希应该基于所有值属性,并且我们应该确保对象是不可变的。通过在 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,这意味着该对象不可哈希,例如不能用于集合中。如果由于某种原因你确实决定要对实体使用集合或字典操作,哈希应该基于定义实体随时间变化的唯一身份的属性(例如 .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.

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
—— Eric Evans,《领域驱动设计》

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 讨论了领域服务操作的概念,这些操作在实体或值对象中没有自然的归宿[6]。给定一组批次来分配订单行的操作,听起来非常像一个函数,我们可以利用 Python 是一种多范式语言的事实,直接将其作为一个函数。

Let’s see how we might test-drive such a function:让我们看看如何测试驱动这样的函数:

Testing our domain service (test_allocate.py)测试我们的领域服务 (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:我们的服务可能看起来像这样:

A standalone function for our domain service (model.py)领域服务的独立函数 (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 PythonPython 的魔法方法让我们能用符合 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__:

Magic methods can express domain semantics (model.py)魔法方法可以表达领域语义 (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:我们还有一个最终概念要涵盖:异常也可以用来表达领域概念。在与领域专家的对话中,我们了解到订单可能因为缺货而无法分配,我们可以通过使用领域异常来捕捉这一点:

Testing out-of-stock exception (test_allocate.py)测试缺货异常 (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 是一种多范式语言,所以让代码中的“动词”成为函数。对于每一个 FooManager、BarBuilder 或 BazFactory,往往都有一个更具表现力和可读性的 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 与 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:我们不会用实现细节来让你感到厌烦,但需要注意的主要一点是,我们像对待实体、值对象和服务一样,在通用语言中小心地命名我们的异常:

Raising a domain exception (model.py)抛出领域异常 (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图 4. 本章末尾的领域模型

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.