领域驱动设计 -在开始之前先理清您的概念






4.90/5 (89投票s)
当你开始一个复杂的应用程序时,你应该总是先设计你的对象模型。领域驱动设计会引导你朝着这个方向前进。
开始开发一个新应用程序
当我们开始一个商业应用程序时,我们传统上是如何做的?我们阅读需求文档,找出功能。我们分解任务。在大多数情况下,分解的目的是为了得到一个估算和工作计划。我们进行估算。我们在团队成员之间分配工作。我们设计数据库模式——有时由团队领导设计,有时由相应的开发人员设计。我们开始编码。
那么? 这种方法有什么问题吗?我们做得很好!难道不是吗?
答案是“是”和“否”!是的,我们在交付项目方面做得很好。但是“否”!我们在维护和扩展项目方面做得不好!
想想你在过去几年里用传统方法工作过的所有项目。你是否遇到过以下任何问题?
- 你的项目在不同的地方以相同或不同的方式实现了相同的功能。
- 同一个物品有多个对象。
- 你的对象包含实际上不是该对象属性的属性。
- 相关项之间几乎没有或只有非常糟糕的关系。
- 从你的对象来看,无法理解整个应用程序到底是什么。
但是当你采用自底向上的方法进行设计时,你首先为细粒度功能设计,而你几乎不知道这个功能将如何从顶层使用,以及顶层功能实际上会是什么样子。
你是否听过你的团队中的某个开发人员说他没有整个应用程序的领域知识?也许吧!我认为你可以理解原因。因为应用程序的设计并不代表系统的领域。因此,开发人员只知道他们工作过的部分。这真是令人难过!不是吗?
那么,传统方法——“从数据库开始设计应用程序”——是否是一个可以抛弃的概念? 并非如此!但是,如果你要开发一个复杂的应用程序,这种自底向上的设计方法并不能指导你进行恰当的面向对象设计。
那么解决方案是什么?
解决方案是**DDD(领域驱动设计)**。
什么是DDD?
领域驱动设计不是一种技术或方法论。DDD提供了一套实践和术语的结构,用于做出设计决策,以聚焦和加速处理复杂领域的软件项目。
本文将涵盖的概念
- 理解领域。
- 通用语言。
- 上下文和限界上下文。
- 实体和值对象。
- 聚合体和聚合根。
- 持久化无关。
- 仓库。
- 领域服务。
在本文中,我将尽量避免过于技术化,而是尝试用更贴近现实世界的例子来讲解DDD的各种概念。 我尽量不展示任何代码。因为我相信,如果你理解了概念并开始以DDD的方式思考,实现起来就很简单。最难的部分是调整你的思维过程!
理解领域
一个知识、影响或活动的领域。用户应用于程序的领域是软件的领域。- 维基百科
你从这个定义中能感受到什么是领域吗? 你能说出你现在正在工作的项目的领域是什么吗?你能说出著名的网站YouTube的领域是什么吗?
在本文中,我想通过一个真实世界的例子来让你感受如何开始驱动你的领域来分析你的项目。这个例子可能与应用程序开发无关,但由于目标是以自顶向下的方式调整我们的思维,它将是有用的。但再次强调,我们也会涉及DDD的技术术语!
假设你被委托设计一栋建筑。要求是
- 你有一块有限的土地
- 你的建筑将有 6 层。
- 每层将有 4 套公寓。
这里的领域是什么?
领域是建筑(?)。可能是。但是请注意,如果你将建筑视为你的领域,你可能会忽略需求中的一些细节。你要设计的建筑必须有供人们居住的公寓的设计。所以,“建筑”这个笼统的词可能会让我们忽略一些细节。因此,我们可以将领域缩小到“住宅建筑”。
现在,当你与工程师以及委托你设计建筑的人谈论你的工作时,“住宅建筑”这个术语对所有相关人员都更有意义。你在这里注意到语言发生了多么微小的变化?承包商告诉你设计一栋建筑,其中有 6 层,每层有 4 套公寓。现在,如果你派一个工程师到现场告诉他我们将在那里建造一座建筑,他们可能不会考虑住宅建筑必须具备的许多属性。另一方面,如果你使用“住宅建筑”这个词,他很可能会做出有效的分析。
这就是我们如何达到“通用语言”的。
通用语言
这个概念很简单,就是开发者和业务应该共享一种他们都能理解并具有相同含义的通用语言,更重要的是,这种语言应该使用业务术语,而不是技术术语。
通用语言的更多示例
示例 1
错误的语言
较小卧室的长度和宽度比例为 4:3。
正确的语言
儿童卧室的长度为 20 英尺,宽度为 15 英尺。请注意,对于房子的主人来说,“小房间”、“比例”——这些都可能是非常技术性的术语。相反,他更容易理解儿童房、客房、客厅等。明确的尺寸对他来说更有意义。
示例 2
让我们从软件角度来看一个例子。
错误的语言
在搜索功能中,我们将考虑 SQL Server 的词形变化和同义词功能,以使搜索更相关。此外,我们还将排除搜索中的停止词,以使其更准确。请注意,您的领域专家可能不是技术人员,因此他可能不理解“词形变化”、“同义词”、“停止词”等词的含义。
正确的语言
在搜索功能中,我们将考虑搜索短语的所有同义词,以便它不会排除相关结果。此外,我们不会区分任何搜索词的数量(单数或复数)、时态、分词等,以使结果更准确。此外,正如任何搜索所期望的那样,我们将忽略所有没有搜索价值的噪声词。这些噪声词可以是“am”、“but”、“where”、“about”等。你看到这里的语言差异了吗?真正正确的语言可以使所有相关方以相同的方式思考和理解。
让我们回到“住宅建筑”领域。看,你可以将住宅建筑设计作为一个单一任务来处理,并一起解决所有问题。但这样做真的明智吗?请注意,如果你只将其视为一项工作,你可能会忽略很多事情。设计建筑涉及许多方面。例如:你需要考虑通风、水电、停车位、社区空间等。
现在你看,其他不同的上下文出现了。这就是“上下文”和“限界上下文”在领域驱动设计中的概念。
上下文和限界上下文
限界上下文可以被视为一个微型应用程序,包含其自己的领域、自己的代码和持久化机制。在限界上下文内,应该存在逻辑一致性;每个限界上下文应该独立于任何其他限界上下文。更多限界上下文的示例
考虑一个电子商务系统。最初你可以说它是一个购物上下文的应用程序。但是如果你仔细观察,你会发现还有其他上下文。例如:库存、配送、账户等。
将一个大型应用程序正确地划分为不同的限界上下文将使你的应用程序更具模块化,有助于你区分不同的关注点,并使应用程序易于管理和增强。这些限界上下文中的每一个都有特定的职责,并且可以半自治地运行。通过将它们分开,可以更清楚地找到逻辑应该放在哪里,并且你可以避免那种 BBOM(庞大的泥球)J
什么是 BBOM?
庞大的泥球是一个结构杂乱、蔓延、 Sloppy、用胶带和捆绑线连接起来的意大利面条式代码丛林。这些系统显示出不受管制增长和反复、权宜性修复的明显迹象。信息被任意地在系统的遥远元素之间共享,往往到了几乎所有重要信息都变成全局或重复的地步。系统的整体结构可能从未被很好地定义过。
我们始终的目标应该是避免 BBOM
再次以“住宅建筑领域”为例。因此,我们可以有几个限界上下文
- 电力供应
- 汽车停放
- 公寓
- 等等。
问题 1:你能想象一个没有房间的窗户吗?
问题 2:窗户如果没有它所在的房间,还有身份吗?
回答这些问题将揭示 DDD 的以下概念。
- 实体。
- 值对象。
- 聚合体 & 聚合根。
实体
“这是我的实体,有很多像它一样的,但这是我的。”
实体的关键定义特征是它具有身份——它在系统中是唯一的,并且没有其他实体,无论多么相似,如果它们的身份不同,就不是同一个实体。
示例:
- 你的公寓里的卧室。
- Facebook 上的合同。
- CodeProject 上的文章。
值对象
值对象的关键定义特征是它没有身份。好吧,可能有点过于简化,但值对象的目的是仅通过其属性来表示某物。两个值对象可能具有相同的属性,在这种情况下,它们是相同的。但是,它们除了其属性之外没有其他值。值对象的另一个常见方面是它们应该是不可变的,一旦创建就不能被更改或修改。你可以创建一个新的,由于它们没有身份,这就像改变另一个一样。
示例
- 房间里的窗户
- 你网站上任何人的地址。
- 你搜索的 SearchCriteria。
注意: 一个值对象在情况不同时可以变成实体。你能找到这样的场景吗?如果你的应用程序的搜索功能要求搜索条件需要保存在数据库中,并且用户可以从已保存的搜索条件列表中执行相同的搜索。在这种情况下,SearchCriteria 拥有自己的身份,因此它是实体而不是值对象。
现在你知道 DDD 中的实体是什么,值对象是什么。在领域驱动设计中,实体和值对象可以独立存在。但在某些情况下,这种关系可能是这样的,即一个实体或 VO 在没有其上下文的情况下没有价值。
示例:
- 只有当房间存在时,窗户才能被定义。
- 只有当订单被下达时,订单备注才能存在。
- 只有当问题被提出时,问题细节才能存在。
很简单,不是吗?相信我,现在你知道 DDD 中的聚合体和聚合根是什么了。
聚合体和聚合根
在上面的例子中——
- 房间、订单和问题是我们的聚合根。
- 另一方面,窗户、订单备注和问题细节是我们的聚合体。
“一组关联对象,它们在数据更改方面被视为一个整体。”
集群中的所有对象都应被视为聚合体。
集群的所有外部访问都通过一个根实体进行。这个根实体被定义为聚合根。
示例
- 除非相应的问答被保存,否则问题细节绝不能被保存。
- 除非检索到相应的问答,否则问题细节绝不能被检索。
在这里,问题是聚合根,问题细节是聚合体。聚合体和聚合根是 DDD 非常重要的概念。
到目前为止,我们已经讨论了领域、对象/实体、上下文、聚合体等。数据库呢?我们是否遗漏了什么?难道它不应该包含在设计中吗?
答案是“否”! DDD 是一种持久化无关的方法。
持久化无关
在领域驱动设计中,你的目标是创建一个领域模型。你需要识别为了实现应用程序所需的功能,你需要哪些项(对象)。你需要识别不同对象之间的关系以及它们如何相互交互。你需要找到使用你的领域模型是否可以实现你客户的业务目标。在这里面数据库在哪里? 在进行领域建模时,你不需要知道你的领域数据将如何、在哪里持久化,甚至数据是否需要持久化。
这种对持久化媒介的忽视将使你的领域模型摆脱与应用程序持久化层的任何耦合。这最终将把持久化及其通信机制的关注点与你的领域模型分离。结果是你的应用程序将摆脱与任何数据存储的耦合,并且非常容易进行单元测试。
但是是的!在实际应用中,你需要有一个数据库。但是你的领域模型将不会知道它。它所知道的就是“仓库”,它最终将管理你应用程序的持久化问题。
存储库
你能告诉我“Repository”这个英文单词的意思吗?
Repository 通常指存储位置,通常是为了安全或保存。
- 维基百科
正如我之前所说,你的领域模型不会知道任何数据库。它所知道的是,系统中有一个仓库,该仓库将负责存储你的数据并检索你的数据。它绝对不是你的领域模型如何以及在哪里持久化数据的关注点。所以,它可以是 Sql server、oracle、xml、文本文件或其他任何东西。我希望你现在对 DDD 中的仓库意味着什么有所了解。
让我们变得更技术化一些。
仓库通过类似集合的接口在领域和数据映射之间进行中介,以访问领域对象。它更像是你数据存储的一个外观,它假装是你领域的一个集合。
仓库不是数据访问层。
请注意,仓库不会用“数据”来交流,它会用聚合根来交流。你可以告诉你的仓库将一个聚合根添加到它的集合中,或者你可以向它请求一个特定的聚合根。当你记住聚合根可能包含一个或多个实体和值对象时,这与传统的 DAL(数据访问层)返回数据库表行集合的做法有很大的不同。
仓库的实现策略
正如我所说,仓库是 DDD 中用于处理持久化问题的设计模式。该模式的细节超出了本文的范围。但是,我将在此尝试最低限度地说明我们可能如何实现仓库。
- 首先,你将有一个接口——
IRepository
,它应该是通用的。 - 你将有一个
IRepository
接口的抽象实现。 - 你将有一个接口
INhRepository
用于你的持久化机制(例如Nhibernate
),它将继承自IReposiroty
。 - 你将在一个名为“
NhReposirory
”的类中有一个INhReposiroty
的实现。 - 最后,你可能有一个通用的仓库实现,它将包含仓库所有常用方法的默认实现。
- 例如
NHGenericRepository
,它继承自NhRepository
并实现IGenericNhReposirtory
。 - 你将为你的聚合根拥有特定的仓库,它将从
NHGenericRepository
继承。 - 你的应用程序将使用服务定位器来查找应用程序将使用的仓库。
领域服务
领域服务是 DDD 的另一个重要概念。如果实体和值对象是领域中的“事物”,那么服务就是处理动作、操作和活动的一种方式。
逻辑不应该直接放在实体上吗?
是的,确实应该。我们应该用与它们及其子项相关的逻辑来建模我们的实体。但是,有时我们需要处理复杂的操作或外部职责,或者我们可能需要向外部公开聚合根的操作。这就是为什么为不同的聚合根创建领域服务是一个好主意。你可以将领域服务视为领域业务逻辑和操作的 fachada 层。
结束语
在本文中,我尝试通过真实世界的例子来介绍领域驱动设计的基本概念和术语。目标是让你对 DDD 世界感到舒适。但真正用 DDD 开发应用程序是一项巨大的挑战。你在设计对象模型时越喜爱和实践 DDD 概念,你的设计就会越准确。正如我之前所说,最重要的事情是,你必须以领域驱动的方式思考。否则,当你的应用程序非常复杂时,你将遭受巨大的痛苦。
绝佳的 DDD 资源
http://www.infoq.com/minibooks/domain-driven-design-quickly
http://thinkddd.com/assets/2/Domain_Driven_Design_-_Step_by_Step.pdf
http://www.dddcommunity.org/library/young_2010
http://www.dddcommunity.org/library/evans_2010
http://blog.fossmo.net/post/Command-and-Query-Responsibility-Segregation-(CQRS).aspx
http://msdn.microsoft.com/en-us/library/ff649690.aspx