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.我们的领域模型占位图是领域模型模式的一个简单视觉占位符。在本章中,我们将填充一些细节,随着后续章节的推进,我们将围绕领域模型构建各种功能,但你应该始终能在核心位置找到这些小形状。
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.几周之内,随着你采用特定的词汇来描述飞船的功能,指令会变得更加精确:“增加三号货舱的氧气水平”或“打开小型推进器”。几个月后,你就会采用描述整个复杂流程的语言:“启动着陆程序”或“准备曲速飞行”。这个过程会非常自然地发生,无需任何正式的努力去建立共享词汇表。
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.从高层来看,我们有独立的系统负责采购库存、向客户销售库存以及向客户发货。中间的一个系统需要通过将库存分配给客户订单来协调这一流程;请参阅分配服务的上下文图。
[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.下面的侧边栏展示了我们在与领域专家讨论分配问题时可能做的一些笔记。
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 的工作原理,但我们想展示如何根据这次业务对话构建模型。
Here’s what one of our first tests might look like:这是我们第一个测试的样子:
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:以下是满足我们要求的领域模型:
@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
-
OrderLineis an immutable dataclass with no behavior.[2]OrderLine 是一个不可变的 dataclass,没有任何行为[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.Optionalanddatetime.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)。 -
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:让我们编写一些新的失败测试:
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 方法来直接实现这一点:
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() 测试时,我们将被迫采用更智能的解决方案:
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 需要了解哪些行已被分配。让我们看看实现:
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 中的模型。
[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 使我们能够轻松处理最后一个测试,因为集合中的项是唯一的:
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、数据库和电子表格的真实世界中。我们将看到,严格遵守封装和仔细分层的原则将如何帮助我们避免陷入“大泥球”架构。
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_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.)请注意,虽然订单有一个唯一标识它的参考号,但行却没有。(即使我们向 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:每当我们有一个包含数据但没有身份的业务概念时,我们通常选择使用值对象模式来表示它。值对象是任何由其持有的数据唯一标识的领域对象;我们通常使它们不可变:
@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."dataclasses(或 namedtuples)带给我们的好处之一是值相等性,这是一种时髦的说法,即“两个具有相同 orderid、sku 和 qty 的行是相等的”。
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 英镑的钞票并不重要,因为它们具有相同的价值。同样,如果名字和姓氏都匹配,那么两个名字就是相等的;如果两个行具有相同的客户订单、产品代码和数量,它们就是等价的。不过,我们仍然可以在值对象上实现复杂的行为。事实上,支持对值进行操作是很常见的;例如,数学运算符:
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 类上实现一些魔法方法:
@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 和数量唯一标识;如果我们更改其中一个值,我们就得到了一个新的行。这就是值对象的定义:任何仅由其数据标识且没有长效身份的对象。那么批次呢?它由参考号标识。
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:
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 这个人呢?人们确实会改变他们的名字、婚姻状况,甚至是性别,但我们仍然认为他们是同一个人。这是因为人类,不像名字,具有持久的身份:
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.实体与值对象不同,具有身份相等性。我们可以改变它们的值,它们仍然可以被识别为同一个事物。在我们的示例中,批次就是实体。我们可以将行分配给批次,或者更改预计到达日期,它仍然是同一个实体。
We usually make this explicit in code by implementing equality operators on entities:我们通常通过在实体上实现相等运算符来在代码中明确这一点:
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.有时,它根本就不是一个“东西”。
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:让我们看看如何测试驱动这样的函数:
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:我们的服务可能看起来像这样:
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__:
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:我们还有一个最终概念要涵盖:异常也可以用来表达领域概念。在与领域专家的对话中,我们了解到订单可能因为缺货而无法分配,我们可以通过使用领域异常来捕捉这一点:
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])
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:我们不会用实现细节来让你感到厌烦,但需要注意的主要一点是,我们像对待实体、值对象和服务一样,在通用语言中小心地命名我们的异常:
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.本章末尾的领域模型是我们最终成果的视觉表示。
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…现在这样应该就可以了!我们有一个可以用于第一个用例的领域服务。但首先我们需要一个数据库……