paint-brush
领域驱动设计实用介绍经过@tobi
1,576 讀數
1,576 讀數

领域驱动设计实用介绍

经过 Piotr Tobiasz15m2023/07/22
Read on Terminal Reader

太長; 讀書

DDD 术语、构建块和其他入门概念。
featured image - 领域驱动设计实用介绍
Piotr Tobiasz HackerNoon profile picture
0-item
1-item

说编写软件不仅仅是编写代码——而是解决特定问题,这不应该是创新的。尽管最终实施解决方案的是开发人员,但首先定义问题所在的并不是开发人员。该任务由不同的业务人员执行,他们考虑流程、风险和结果来描述问题是什么、为什么存在以及应该如何解决。在领域驱动的环境中,这些业务人员被称为领域专家


从工程角度来看,领域专家似乎拥有一项宝贵的资产:他们对该领域的了解。然而,这些知识很少以其原始形式被分享。相反,它通常被转化为需求,以便开发人员能够理解和实现它。这种方法的问题在于业务人员和开发人员的领域知识可能存在分歧。这意味着定义问题的人和致力于解决问题的人的观点可能不一致,从而导致误解和冲突。


那么,出路何在?确保业务和技术人员使用相同的语言和术语。

什么是领域驱动设计?

领域驱动设计 (DDD) 是一种方法,强调在领域专家和技术利益相关者之间建立共识并使软件解决方案与底层业务需求保持一致的重要性。这似乎是一个高级的、非技术性的定义,但它也可以分解为对开发人员更友好的东西:


DDD 通过培养和使用通用语言来在代码中表示现实世界的概念及其结构,这是通过对业务领域进行建模而构建的。


还有一些术语需要介绍,所以现在可能还不是 100% 清楚。最重要的是,DDD 提供了允许编写和构建与业务愿景一致的代码的工具和活动。这不仅涉及沟通,还涉及做出真正塑造共同语言的设计决策。

术语

DDD 世界中最重要的术语是,这应该不足为奇。 《学习领域驱动设计》一书的作者 Vlad Khononov 是这样描述的:


业务领域定义了公司的主要活动领域。


这意味着业务领域也可以被视为:


  • 公司收入的主要来源,
  • 该公司最出名的是,
  • 公司比竞争对手做得更好的任何事情。


该域还可以分为子域——更具体的活动范围。虽然存在三种不同类型的子域,但最重要的一种是核心。它描述了公司如何实现业务优势。另外两个是关于更常见、通用的问题,例如身份验证系统或内部管理面板。


深入了解公司的业务领域对于充分利用领域驱动设计的优势绝对至关重要。这种理解的最佳来源不是别人,正是领域专家。这些人的问题正在通过软件得到解决——利益相关者、各种业务人员,甚至用户。这并不是说工程师对他们正在研究的领域一无所知,而是说专家是该领域知识的真实来源。通过与领域专家合作,开发人员可以确保领域模型保持准确和最新。


这导致了另一个关键但可能含糊不清的术语:模型。 Eric Evans 在他关于 DDD 的书中对该模型进行了如下描述:


它是对现实的一种解释,抽象了与解决当前问题相关的方面并忽略了无关的细节。


弗拉德·霍诺诺夫接着用更相关的术语解释了这个想法:


模型不是现实世界的复制品,而是帮助我们理解现实世界系统的人类构造。


总之,模型是业务概念或流程的表示,有助于理解和交流领域的潜在复杂性。


Vlad 使用地图有效地说明了领域模型的概念。地图是一个完美的例子,说明它们如何仅显示与地图类型相关的信息,例如地形、道路或边界。一张同时显示所有细节的地图会让人不知所措,而且几乎毫无用处。领域模型还可以有其他形式,例如:


  • 客户订单,代表后台发生的所有流程的简化版本,
  • 餐厅菜单,菜单上列出的项目是最终产品,而不是列出准备过程中的每一种成分和步骤,
  • 旅行预订,预订的旅行仅突出最关键的细节,尽管计划旅行、酒店等还有更多内容。


领域驱动设计 (DDD) 术语难题的最后一块是通用语言。它是指项目中技术和业务利益相关者使用的共享语言。在 DDD 中,拥有一种通用语言来描述从领域模型派生的业务领域至关重要。它有助于确保所有团队成员都清楚地了解问题空间、其概念及其关系。这可以更好地协调并减少误解的风险。通过使用Ubiquitous Language,软件解决方案可以准确反映底层业务需求,使其成为DDD的关键组成部分。


涵盖了大部分术语后,应该更容易理解什么是领域驱动设计。现在是时候深入研究 DDD 的实际运作方式了。

建筑模块

DDD 构建块是创建有效且高效的领域模型的基础。 Vlad Khononov 按以下方式定义领域模型:


领域模型是包含行为和数据的领域对象模型。


领域模型由各种构建块和结构组成。最重要的是:


  • 值对象,
  • 实体,
  • 骨料,
  • 领域事件,
  • 存储库,
  • 域服务。

值对象

值对象是最基本的可用构建块。这些是由一组属性和值定义的对象。他们没有唯一的标识符——他们的价值观定义了他们的身份。它们是不可变的,因为不同的值已经代表了不同的值对象。值对象的示例包括:


  • 金额,
  • 日期范围,
  • 邮寄地址。


以下是如何在 Python 中实现简单的值对象:


 from pydantic import BaseModel class Address(BaseModel):  """Customer address."""  country: str  city: str  street: str  house_number: str  class Config:    frozen = True


这意味着仅当两个对象分配的值完全相同时,使用相等运算符 ( == ) 比较两个地址才会返回True

实体

实体是下一类型的构建块。实体代表域中具有独特身份的单个对象,例如一个人或一个订单。它们与值对象的相似之处在于它们也存储数据,但它们的属性可以并且预计会改变,因此它们需要唯一的标识符。订单和个人信息只是实体的两个简单实例:


 import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.value_objects import Address class Person(BaseModel):  """Personal data."""  id: uuid.UUID = Field(default_factory=uuid.uuid4)  first_name: str  last_name: str  address: Address class Order(BaseModel):  """Customer order."""  id: uuid.UUID = Field(default_factory=uuid.uuid4)  description: str  value: float


由于实例的值在这两种情况下都是可修改的,因此它们需要一个标识,可以是 UUID。更重要的是,在大多数情况下,实体并不意味着直接管理,而是通过聚合来管理。

总计的

聚合是一种实体类型,因为它是可变的并且需要唯一的标识符。然而,它的主要职责不是存储数据,而是将一组相关对象(实体和值对象)组合在一起作为一个一致性单元。聚合是根对象,具有明确定义的边界,封装其内部状态并强制执行不变量以确保整个组的一致性。聚合通过关注对象之间的关系而不是对象本身,允许以更自然和直观的方式推理领域。


根据前面的示例,聚合可以表示为客户:


 import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.entities import Person, Order from practical_ddd.building_blocks.value_objects import Address class Customer(BaseModel):  """Customer aggregate.  Manages personal information as well as orders.  """  id: uuid.UUID = Field(default_factory=uuid.uuid4)  person: Person  orders: list[Order] = Field(default_factory=list)  def change_address(self, new_address: Address) -> None:    self.person.address = new_address  def add_order(self, order: Order) -> None:    if self.total_value + order.value > 10000:      raise ValueError("Order cannot have value higher than 10000")    self.orders.append(order)  def remove_order(self, order_id: uuid.UUID) -> None:    order = next((order for order in self.orders if order.id == order_id), None)    if order is None:      raise IndexError("Order not found")    self.orders.remove(order)  @property  def total_value(self) -> float:    return sum(order.value for order in self.orders)


客户直接链接到个人数据,并存储所有订单。最重要的是,聚合公开了一个用于管理人员地址以及添加和删除订单的界面。这是因为聚合的状态只能通过执行相应的方法来更改。


虽然前面的示例相对简单,只有一个约束(订单值不能大于 10000),但它应该演示 DDD 构建块的使用及其关系。在实际系统中,聚合通常更复杂,具有更多约束、边界,并且可能有更多关系。毕竟,它们的存在就是为了管理这种复杂性。此外,在现实世界中,聚合通常会保存在数据存储中,例如数据库。这就是存储库模式发挥作用的地方。

存储库

聚合的状态更改应该全部在单个原子操作中以事务方式提交。然而,“坚持自己”并不是集体的责任。存储库模式允许抽象出数据存储和检索的细节,而是在更高的抽象级别上使用聚合。简单地说,存储库可以被视为聚合和数据存储之间的一层。JSON 文件是此类存储的一个相当简单的示例。客户聚合可以有一个对 JSON 文件进行操作的存储库:


 import json import uuid from practical_ddd.building_blocks.aggregates import Customer class CustomerJSONRepository:  """Customer repository operating on JSON files."""  def __init__(self, path: str) -> None:    self.path = path  def get(self, customer_id: uuid.UUID) -> Customer:    with open(self.path, "r") as file:      database = json.load(file)      customer = database["customers"].get(str(customer_id))      if customer is None:        raise IndexError("Customer not found")      person = database["persons"][str(customer["person"])]      orders = [database["orders"][order_id] for order_id in customer["orders"]]    return Customer(      id=customer["id"],      person=person,      orders=orders,    )  def save(self, customer: Customer) -> None:    with open(self.path, "r+") as file:      database = json.load(file)      # Save customer      database["customers"][str(customer.id)] = {        "id": customer.id,        "person": customer.person.id,        "orders": [o.id for o in customer.orders],      }      # Save person      database["persons"][str(customer.person.id)] = customer.person.dict()      # Save orders      for order in customer.orders:        database["orders"][str(order.id)] = order.dict()      file.seek(0)      json.dump(database, file, indent=4, default=str)


当然,这个类可以(也许应该)做更多的事情,但它并不是一个完美的多功能 ORM。它应该提供有关存储库职责的想法,在本例中是在JSON 文件中存储和检索客户聚合。还值得注意的是存储库如何处理与聚合关联的实体。由于个人数据和订单与客户生命周期紧密相关,因此必须在处理聚合时对其进行精确管理。

域名服务

另一种需要考虑的情况是,业务逻辑根本不适合聚合或其任何实体或值对象。它可能是依赖于多个聚合或数据存储状态的逻辑。在这种情况下,称为域服务的结构可以派上用场。域服务必须能够管理聚合,例如通过使用存储库,然后它可以存储不属于聚合的域逻辑。例如,客户可能需要逻辑来避免丢失太多订单:


 import uuid from typing import Protocol from practical_ddd.building_blocks.aggregates import Customer class CustomerRepository(Protocol):  """Customer repository interface."""  def get(self, customer_id: uuid.UUID) -> Customer:    ...  def save(self, customer: Customer) -> None:    ... class CustomerService:  """Customer service."""  def __init__(self, repository: CustomerRepository) -> None:    self.repository = repository  def get_customer(self, customer_id: uuid.UUID) -> Customer | None:    try:      return self.repository.get(customer_id)    except IndexError:      return None  def save_customer(self, customer: Customer) -> None:    existing_customer = self.get_customer(customer.id)    # If customer is already in the database and has more than 2 orders,    # he cannot end up with half of them after a single save.    if (      existing_customer is not None      and len(existing_customer.orders) > 2      and len(customer.orders) < (len(existing_customer.orders) / 2)    ):      raise ValueError(        "Customer cannot lose more than half of his orders upon single save!"      )    self.repository.save(customer)


Aggregate 无法确保其状态与 JSON 文件中的状态有何不同,因为它一开始并不了解 JSON 文件。这就是为什么比较逻辑必须包含在领域服务中。同样重要的是要注意域服务应该与存储库抽象一起使用。这使得通过使用依赖注入将具体实现替换为替代实现变得简单。

把它们放在一起

现在所有的部分都已经涵盖了,它们现在可以被视为一个工作程序:


 import uuid from practical_ddd.building_blocks import aggregates, entities, value_objects from practical_ddd.database.repository import CustomerJSONRepository from practical_ddd.service import CustomerService # Initialize domain service with json repository srv = CustomerService(repository=CustomerJSONRepository("test.json")) # Create a new customer customer = aggregates.Customer(  person=entities.Person(    first_name="Peter",    last_name="Tobias",    address=value_objects.Address(      country="Germany",      city="Berlin",      street="Postdamer Platz",      house_number="2/3",    ),  ), ) srv.save_customer(customer) # Add orders to existing customer customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.add_order(entities.Order(description="Order 1", value=10)) customer.add_order(entities.Order(description="Order 2", value=210)) customer.add_order(entities.Order(description="Order 3", value=3210)) srv.save_customer(customer) # Remove orders from existing customer # If there are only 3 orders, it's gonna fail customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.remove_order(uuid.UUID("0f3c0a7f-67fd-4309-8ca2-d007ac003b69")) customer.remove_order(uuid.UUID("a4fd7648-4ea3-414a-a344-56082e00d2f9")) srv.save_customer(customer)


一切都有它的责任和界限。聚合负责管理其实体和值对象,并强制执行其约束。域服务使用注入的 JSON 存储库将数据保留在 JSON 文件中并强制执行额外的域边界。最后,每个组件在指定的领域内都有独特的功能和意义。

概括

毫无疑问,领域驱动设计是一个需要掌握的复杂概念。它提供实践、模式和工具,通过重点关注业务领域来帮助软件团队解决最具挑战性的业务问题。然而,DDD 不仅仅是一组构建块。这是一种需要技术和业务利益相关者之间协作和沟通的心态。通过通用语言表达的对领域的共同理解对于 DDD 项目的成功至关重要。如果做得好,DDD 可以使软件更好地满足业务需求,并更有效地解决复杂问题。

后记和后续步骤

本文的目的绝不是“DDD:从零到英雄”,而是作为对 DDD 世界的介绍。我想以非常简单实用的方式演示领域驱动设计最重要的概念。我相信学习领域驱动设计是提高编程专业知识的绝佳方法。然而,您不太经常听到它 - 至少没有“11 个疯狂的 JavaScript 提示和技巧 - 一个线程 🧵”那么多。


无论如何,如果您发现其中任何有趣的内容,您可以浏览来源部分,查找最初激发我撰写本文的书籍和文章。有一些概念我没有涉及,因为我认为它们超出了本介绍的范围,但它们非常值得研究:


  • 有界上下文
  • 领域事件
  • 事件风暴


毫无疑问,您会在下面列出的来源中找到它们。

来源

本文中使用的代码示例可以在这里找到:链接


学习 Vlad Khononov 的领域驱动设计。这是一本很棒的书,它是我灵感的主要来源。更深入地解释本文中讨论的所有概念。


Harry Percival 和 Bob Gregory 的《Python 架构模式》 。我大约两年前读过这本书,它对我作为一名开发人员产生了重大影响。我在写这篇文章时又回顾了它,它再次对我有帮助。


Python 中的 DDD,作者:Przemysław Górecki。我在写文章快结束时发现了这个博客,但它激起了我的兴趣,因为它非常专业。有趣的事实:我和普热梅斯瓦夫在同一家公司工作,但我完全不知道这一点。


也发布在这里