DDD 领域驱动设计-在动手之前,先把你的脑袋清理干净

时间:2023-03-08 16:48:57
DDD 领域驱动设计-在动手之前,先把你的脑袋清理干净

惨不忍睹的翻译

  英文原文:http://www.codeproject.com/Articles/339725/Domain-Driven-Design-Clear-Your-Concepts-Before-Yo

  作者:Mahmud Hasan(我认识他,他不认识我)

  翻译这篇老外文章的两个原因:

  • 首先,这是一篇关于领域驱动设计的文章(I love DDD)。
  • 其次,我想看看我英语烂到什么程度。

  不可否认的是,翻译出来的结果,确实惨不忍睹,惨到我都不想发布了,发布的原因就是让大家看看,学不好英语,是什么样的情况(就是我这样)?

  在阅读下面“译文”之前,需要明确下,我大学英语水平是3.5级B(注意上面还有个A)。

  所以,英语好的,请阅读英文原文;英语不好的,请自备词典,阅读英文原文,强烈建议不要阅读下面内容。

开始一个新的应用程序

当你开始一个商业项目的时候,你应该首先设计你的对象模型,DDD(领域驱动设计)就是这样指引你的。

  当我们开始一个业务应用程序,我们通常怎么做的呢?我们阅读其业务规则和查找相应的功能,我们分解任务,在大多数的情况下,项目出现问题的缘由是项目的估计和计划。我们做了估计,然后给团队成员分发任务,我们设计的数据库架构在领导和开发者之间徘徊,然后我们开始编码。

  所以呢?这样做有什么不对的地方吗?我们一直都是这样的做的啊,而且做的还不错!难道不是吗?答案是 YES 和 NO!是的,在我们项目的开发过程中,我们做的是不错。但是,我们没有做很好的维护和扩展我们的项目!

  想想在这些年中,所有你用传统开发模式开发过的项目,你有没有遇到以下几个问题?

  1. 你的项目在不同的地方用相同的方式,实现相同的功能。
  2. 你的同一项目有多个对象。
  3. 你的项目存在不属于该对象实际属性的对象。
  4. 你的相关的项目存在非常差的关系。
  5. 只看你的对象是不可能明白你整个应用程序的全部。

  我相信你在开发的时候,经常面临这些问题。但是,你知道为什么吗?其原因是,传统的开发模式没有引导我们在设计系统中从上到下的方式。 相反,它诱惑我们来设计系统从下到上的方式。你看,当你设计一个系统,你需要知道一个整体的应用程序需要做什么?客户端试图达到什么目的?然后,从顶层目标,你想出了不同的小功能,最终允许用户实现顶层目标。

  但是,当你使用从下到上的设计方法,首先设计颗粒状的功能,你并不知道这个功能满足什么样的需求,而且还是在实际应用中。

  你有没有听说过,你的团队开发人员谈论,但是并不清楚整个应用的领域知识?或许是的!我觉得你可以明白其中的道理。原因是,应用程序的设计并不代表系统的领域。因此,开发人员只知道他们工作的部分。这是可悲的!是不是?

  那么,传统的开发模式-“应用程序的设计从数据库开始”,这一概念?并不真正正确!如果你有一个复杂的应用程序,这种自下而上的设计方法,并没有真正意义上的面向对象设计。

  该如何解决呢?

  解决的办法是 DDD(领域驱动设计)。

DDD 是什么?

  领域驱动设计不是一种技术或方法,DDD 提供了一种实践理论指导,用于复杂性业务场景的设计。

本文中包含的内容

  1. 了解领域
  2. 无所不在的语言
  3. 上下文和界定上下文
  4. Entity(实体)和 Value Object(值对象)
  5. Aggregates(聚合)和 Aggregates root(聚合根)
  6. 忽略持久化
  7. Repository(仓储)
  8. Domain Service(领域服务)

  在这篇文章中,我会尽量避免使用太多技术,而我会通过 DDD 尝试去贴近现实世界的概念。我会尽量在这里不展示任何代码。因为我相信,如果你理解这个概念,并开始在使用这种思维方式,很容易实现。最困难的部分是调整你的思维过程!

了解领域

A sphere of knowledge, influence, or activity. The subject area to which the user applies a program is the domain of the software.
- Wikipedia

  从上面的定义中,你感觉领域是什么?在这个时刻,你能谈一下,你现在工作项目中的领域概念吗?你能说下著名网站 YouTube 的领域吗?

  在这篇文章中,我想通过一个现实生活中真实的例子,来带给你感觉,如何使用领域驱动设计,开始分析你的项目。 这个例子不能与应用程序开发相关的,但由于我们的目标是调整我们自上而下的思维方式方式,这是有益的。但同样,我们也将使用 DDD 的技术术语!

  比方说,你要设计一个建筑。 要求是:

  • 你有一个明确的土地面积。
  • 你的建筑将有 6 层楼。
  • 每个楼层都会有 4 个公寓。

What is your domain here?

  建筑是领域?这可能是。不过需要注意的是,如果你考虑建筑作为你的领域,在你的需求中,你可能会错过一些颗粒​​状的细节。你要设计的建筑必须为公寓的人生活在哪设计。因此,一个通用术语“建筑”使我们错过一些细节。所以,我们可能要对我们的领域缩小到“住宅建筑”。

  现在,当你和工程师谈论你的工作,谁还要你去设计这个建筑,“住宅建筑”为大家关注,是更有意义的。在这个语言中,你有没有标记非常小的变化?承建商告诉你,设计一个六层并且每一层有四个公寓的建筑物。 现在,如果你派工程师到现场,告诉他我们需要在这里构造一个建筑,他们可能不会考虑一个住宅建筑必须具备的很多属性。另一方面,如果你使用“住宅建筑”的概念,最有可能,他会拿出一个有效的分析。

  这就是我们如何来描述一个“通用语言”。

无所不在的语言

  这个概念很简单,开发人员和企业应该共享,理解同样事物的一个通用语言,更重要的,那就是在商业术语,而不是技术相关的通用语言。

无所不在语言的更多示例

示例1

  错误语言:

  小床房间的长宽比例是 4:3。

  正确语言:

  儿童床的房间长度为20英尺,宽度为15英尺。

  对于建设“小房间”的主人,需要注意的是,“比例”概念是非常技术性的术语。相反,它更容易让他理解孩子们的房间,客房,客厅等,并明确测量是更有意义的。

示例2

  从软件角度,我们来看一个例子。

  错误语言:

  在搜索功能,我们会考虑的 SQL Server 的 inflectional and thesaurus 功能,使搜索更具相关性。此外,我们也将排除 stop word,从而使检索更加准确。
需要注意的是,你的领域专家可能不是一个技术人员,因此他可能不明白你的“inflectional”,“thesaurus”,“Stop word”等意思。

  正确语言:

  在搜索功能,我们会考虑搜索短语的所有同义词,使其不排除相关的结果。此外,我们将让结果变得更准确由其数字(单数或复数),时态,分词等不区分任何搜索词。另外在任何搜索如预期般的那样,我们将忽略所有在搜索没有任何意义的单词。这种干扰词可能是“am”,“but”,“where”,“about”等。

  你看到这,明白语言的区别了吗?一个正确的语言可以使所有的参与方,思考和理解的方式相同。

  回到我们的“住宅建筑”领域。 瞧,你可以继续把建筑设计作为一个单一的任务,然后一起解决整个事情。但它确实是非常明智的方式来做到?需要注意的是,如果你只是考虑这项工作的一个简单工作单元可能会错过很多东西。设计一个建筑是涉及到很多的东西。例如:你需要考虑通风,公共设施,停车位,社区空间等。

  现在你看,不同的其他情况下都上来了。“上下文”和“界定上下文”这是怎样的概念?该是他们登场的时候了。

上下文和界定上下文

  有界上下文可以看作是一个小型应用程序,包含它自己的领域,自己的代码和持久性机制。在一个有界上下文中,应该有逻辑上的一致性,每一个有界上下文应该是独立于任何其他有界上下文。

  界定上下文的更多示例:

  想想一个电子商务系统,最初,你可以告诉它是购物上下文的应用。但如果你更仔细,你会看到有其他情况下也是存在的,比如:库存,交货,账户等。

  正确划分不同界定上下文之间的一个大型应用程序,将使你的应用程序更加模块化,将帮助你区分不同的关注点,并将使应用程序易于管理和扩展。这些界定上下文都有特定的职责,并可以在半自动的方式下进行操作。通过拆分这些上下文,除了使其变得更加适合自身的逻辑之外,你还可以避免 BBOM(大混球,我自己的翻译)。

BBOM(大混球)是什么?

A Big Ball of Mud is a haphazardly structured, sprawling, sloppy, duct-tape-and-baling-wire, spaghetti-code jungle. These systems show unmistakable signs of unregulated growth, and repeated, expedient repair. Information is shared promiscuously among distant elements of the system, often to the point where nearly all the important information becomes global or duplicated. The overall structure of the system may never have been well defined.
- Brian Foote and Joseph Yoder, Big Ball of Mud. Fourth Conference on Patterns Languages of Programs (PLoP '97/EuroPLoP '97) Monticello, Illinois, September 1997

  译者注:上一段因为不知如何翻译,所有就没翻译,“A Big Ball of Mud”,我翻译为“大混球”,Mub 单词为“泥”的意思,我想也是“混乱”的意思,泥球,是指杂乱无章、错综复杂、邋遢不堪、随意拼贴的大堆代码。作者表达的意思就是,如果没有界定上下文,那整个应用程序就是一个 BBOM,我所理解的,也就是一个“大混球”。

  我们花费所有时间的目标就是应该避免 BBOM。

  再次回到“住宅建筑领域”,因此,我们可以有几个界定上下文:

  • 电力供应
  • 停车场
  • 公寓
  • 等等

  让我们来谈谈公寓。该公寓的根本是不同房间的组合,房间里面都有不同的元素,如门,窗等,关于房间的窗户,现在我有2个问题:

  • 问题1:你能想象一个没有窗户的房间吗?
  • 问题2:如果这个房间没有人居住,一个窗户是有意义的吗?(翻译出来感觉有点奇怪,原文:Does a window have any identity without the room it is residing in?)

  回答这些问题,将暴露 DDD 以下的概念。

  1. Entity(实体)
  2. Value Object(值对象)
  3. Aggregates(聚合)和 Aggregates root(聚合根)

Entity(实体)

“This is my Entity, there are many like it, but this one is mine.”
-“这是我的实体,有很多很像它,但是这一个才是我的。”

  定义一个实体的特征,关键是它有一个身份(Identity)-它是系统中是唯一的,并没有其他实体,无论多么相似,相同的实体,除非它具有相同的标识。

  示例(和我们平常的表述有所不同):

  1. 你的床在公寓的房间里。
  2. 合同在 Facebook 上。
  3. 文章在 CodeProject 上。

Value Object(值对象)

  一个值对象的定义,关键特征是它没有唯一标识。好吧,也许有点简单,但一个值对象的意图是通过它的属性,仅仅代表它是什么。两个值对象可能具有相同的属性,在这种情况下,它们是相同的。他们不不过有自己的 virtue 属性之外的其他值。常见值对象的另一个方面是,他们或许应该是一成不变的,一旦创建就不能改变或修改。您可以创建一个新的,他们没有身份,改变他们的方式就是再创建一个。

  示例:

  1. 房间里的窗户。
  2. 在你网站中,一些人的地址。
  3. 你的搜索条件。

Note:一个值对象可以成为一个实体视情况而定。你可以找到一个情况吗?如果你应用程序的搜索功能要求说,搜索条件应保存在数据库中,用户可以执行来自已保存的搜索条件的相同搜索结果。在这种情况下 SearchCriteria(搜索条件)有它自己的身份,并因此它是一个实体,而不是作为一个值对象。

  在 DDD 中,现在你知道什么是实体和什么值对象了吧。在领域驱动设计中,实体和值对象可以独立存在。但在某些情况下,关系可以是这样的,如果没有它的上下文,一个实体或值对象没有价值的。

  示例:

  1. 一个窗户只有在一个房间存在的情况下才能被定义。
  2. 下单了,这个订单才能存在。
  3. 一个问题只有被提出,这个问题的细节才有价值。

  很简单,不是吗?相信我,在 DDD 中,现在你将知道:什么是聚合?什么是聚合根?

Aggregates(聚合)和 Aggregates root(聚合根)

  在上面给出的例子中:

  • 房间、订单和问题是我们的聚合根。
  • 另一方面,订单详情和问题详情是我们的聚合。

  “A cluster of associated objects that are treated as a unit with regard to data changes.”

  All objects of the clusters(不知怎么翻译) 都应该被视为聚合。

  所有的外部访问聚合是通过一个单一的根实体,这根实体被定义为聚合根。

  示例:

  • 一个问题详情是没办法保存的,除非相应的问题被保存。
  • 一个问题详情是没有办法进行检索,除非相应问题被检索。

  在这边,问题是聚合根,问题详情是聚合。 聚集和聚合根是 DDD 的非常重要的概念。

  到目前为止,我们已经讲过领域、对象/实体、上下文、聚合等,那数据库是怎样的呢?是不是我们错过了什么?数据库应该在设计中?

  答案是 NO!DDD 是一个忽略持久化的。

忽略持久化

  在领域驱动设计中,你的目标是创建领域模型。你需要确定你的应用程序中完成功能所包含的所有对象,你需要确定不同的对象,以及他们彼此之间存在的关系。如果使用你的领域模型可以实现客户的业务需求,那数据库还存在吗?在你构建领域模型时,你并不需要知道你的领域数据是在哪里或怎样持久化的,甚至在数据确实需要持久化时,你也无须知道。

  忽略持久化将使你的领域模型在应用程序中真正解耦,从你的领域模型中,领域模型的持久化和沟通机制分离开,以获得更好的关注度。这样你的领域模型和持久化可以很容易的进行单元测试。

  但是,在实际的应用中,你确实需要有一个数据库,但你的域模型并不关注这些,所以就有了“仓储”的概念了。

Repository(仓储)

Can you tell me what the meaning of the English word “Repository” is?
Repository commonly refers to a location for storage, often for safety or preservation.
- Wikipedia

  正如我已经说过你的领域模型不知道任何数据库,它只知道的是,在系统中有一个仓储,并且这个仓储将负责存储数据和检索数据,你的领域模型对数据的持久化是没有确定方式的。因此,它可以在 SQL Server,Oracle,XML,文本文件或其他任何东西。在 DDD 中,现在我希望你对仓储有了一些感觉。
让我们变得更小的技术。

  Repository Mediates between the domain and data mapping using a collection-like interface for accessing domain objects. It is more like a facade to your data store that pretend like a collection of your domain.(第一句理解意思,但是组织不起来语言)

  仓储不是数据访问层。

  需要注意的是,存储库中不关注“数据”,它关注的是聚合根。你可以使用你的仓储添加一个聚合根到它的集合中,或者你可以要求它为特定的聚合根。如果你还记得,聚合根可包含一个或多个实体和值对象,这使得它非常不同于传统的 DAL 层,从数据库中返回一些行数据。

仓储的实施策略

  正如我所说的那样,仓储在 DDD 数据持久化中是一种设计模式,这个模式的细节部分超过了本文所涵盖的范围,不过,在这里用最短的时间,我想告诉你如何实现仓储。

  1. 第一步,有一个 IRepository 接口。
  2. 定义 IRepository 接口成员。
  3. 有一个 INhRepository 接口提供持久化,继承 IRepository。
  4. 实现 INhRepository 接口,比如 NhRepository。
  5. 最后,你可能有一个仓储的泛性实现,将为这个仓储提供所有默认的通用方法。
  6. 像 NhGenericRepository 继承自 NhRepository,实现 INhGenericRepository。
  7. 你将为你的聚合根约束仓储,这也将更好的扩展 NhGenericRepository。
  8. 你的应用程序将使用 ServiceLocator 找到应用曾许所使用的仓储。

Domain Service(领域服务)

  领域服务是 DDD 中另一个非常重要的概念,如果实体和值对象在领域中是“东西”的话?那服务就是应对行为,动作和活动的一种方式。

  Shouldn’t Logic Be on the Entities Directly?(逻辑,直接,不懂)

  原文:Yes, it really should. We should be modeling our Entities with the logic that relates to them and their children. But, there are occasions when we need to deal with complex operations or external responsibilities or maybe we need to expose the actions of the aggregate roots to the external world. This is why creating a domain service for different aggregate root is a good idea. You can consider the domain services as façade layer of the business logics and operations of your domain.

  有点难翻译出来,我用自己的话来表述一下,就是说实体都是內聚的,它描述了自身的业务逻辑和状态,如果有多个实体之间相互协调,我们应该怎么处理?难道需要向外部暴露这些过程吗?这时候就需要领域服务了,它的作用就是协调多个实体,完成一个业务功能的实现,领域服务的名称是动词,而且它没有自身的状态,更别说保存状态了,说简单一点,就是它是为实体服务的,实体自身完成不了的事,那就是领域服务的事,而且它事领域模型必不可少的组成部分。

结束语

  在这篇文章中,我将试图引入现实生活中的例子,来说明领域驱动设计中的基本概念,我们的目标就是让你感觉 DDD 和现实生活如此贴切,但真正应用 DDD 是一个很大的挑战。在你设计你的对象模型的时候,你脑袋中想着 DDD 的概念,那你的设计将更加准确。正如我之前所说的那样,你必须考虑领域驱动设计,如果你不这样做,如果你的应用程序是相当的复杂,你将损失更大。

DDD 参考资源