开发人员指南:域驱动设计食谱(第2部分,结构和交互)

ddd-header


引言


在第一篇文章中,我们重点介绍了指定实践的范围,可以将其应用到哪些项目中以及不应该将它们应用于哪些项目。


在本文中,我将简要概述DDD的基本原理,并分享我在DDD应用中的个人经验。 我们将更详细地讨论通信和结构化方法,并举例说明其实现方式。


在下一篇文章中,我将考虑到它们的实现,写下应用的设计模式的可能组合,最后,我将给出一个小型微服务的特定实现的示例。


DDD


回顾之前的定义:


域驱动设计(DDD)是一种软件开发方法,它通过将实现与不断发展的主要业务模型紧密联系在一起,从而全面满足需求。

用来描述构建复杂系统的实践的参考书是Eric Evans的《 领域驱动的定义》 (大蓝皮书)。 如果您阅读有关此主题的任何评论文章,则已经知道。 在实际使用DDD时,您将必须阅读它。 这不是最容易阅读的书:


DDD的规范来源是Eric Evans的书。 在软件文献中,这不是最容易阅读的书,但它却是其中一本可以充分回报巨额投资的书之一。

马丁·福勒(Martin Fowler):2014年1月15日

如果您滚动浏览这本书的内容,那么您似乎就不太习惯了。 但是地图会帮助我们。
DDD图


我们今天将考虑的是这些做法。


本书涵盖的实践范围非常广泛。 在本书之外可以应用的实践范围更大。 在至少使用其中一部分之前,请确定您的目标。 我将以自己为例。


  • 提高生产力。
  • 编写易于理解的代码。
  • 在软件开发级别进行扩展。

单一语言


软件开发很少会导致创建新的事物,通常,这是对现有事物的模拟。


模型是真实对象的表示,仅包含必要的属性和功能。

我们无法创建涵盖整个主题领域的软件产品。 只能复制其中将复制必要功能的那部分。


模型的一个很好的例子是地形图。 她是地形模型。 该地图不包含田野和河流的草地,仅反映了真实对象之间的相对位置。


要为每个人建立清晰明确的模型,您需要说相同的语言。 埃里克·埃文斯(Eric Evans)不仅告诉我们这一点,而且还告诉我们常识。 如果程序员使用他们的术语,并且使用他们自己的“ s语”,那么前者根本不会理解需要做什么。 在这种情况下,企业将无法实现开发一项或另一项“功能”的实际成本。 您听过多少次:“是的,这只是一个添加按钮”?


作为系统设计师,您的目标应该是使整个团队彼此之间有最大的了解。 如何实现呢? 开始讲话。 如果人们开始在任何亲密的小组中进行交流,那么他们将具有某些普遍接受的术语。 在不同的公司中,引入通用语言的过程可能会有所不同。 这可以是一个意志坚定的决定,也可以是一个民主程序。 可以明确指出该语言,而不会明确输入,在这种情况下,他们只是开始讲这种语言。 通用文档是介绍通用语言的一种好方法。


如何保留项目文件


  1. 业务与开发之间的任何沟通都应该改善您的模型。
  2. 会议结束后,以文档(Scrum工件)的形式记录结果,并向开发过程中的所有参与者显示此文档。
  3. 在文档中使用一种语言。
  4. 最重要的是:不要在文档上浪费时间。 您仍然必须编写代码,并且文档将被多次重写,从而花费大量资源。 不要在手机上长时间摆弄UML图表应用程序,而要使用餐巾纸,笔和相机。
  5. 文档需要纪律;您不能不时编写。
  6. 分隔文档:
    • 代码中的注释-在代码中直接描述难以理解的时刻,保留#ODO:将代码合并到主代码时删除)。 在注释中表达您的意见,例如,在使用旧代码时必须使用一个或另一个拐杖。
    • 项目根目录中的README.md项目的注释应包含技术信息:如何启动项目,如何运行测试等。 最好获得一张地图,其中包含所有项目以及它们在哪些服务器上运行。 单独记录所有接受的协议。
    • 最重要的是知识库。 描述业务流程的文档的集合,这是您和企业都可以使用的文档的一部分。
  7. 编写文档的人的主要错误是冗余。 不要试图覆盖所有内容,仅传达一般含义。 该文档应补充您的项目,但不能以任何方式替代它。 不要写下所有模棱两可的术语。 如果一个定义使用两个以上的句子,则定义很差。

文档示例:


 #         . # :  : -    - email -  ## : ###        ,     email  ,    1  (      email  ). ###           .          email . ###  email  email     .    ,    email.   . ###       ,     ,    .   . ###      email,    ,       .   2 . ###                  .    ,     ,             . 

请注意,在此示例中,我们没有指定显式词典,但是固定了UserAuthorizationRegistration的概念。 撰写此类文档不会花费专家超过20分钟的时间。


对于不是该领域专家的人来说,编写文档的过程被认为是复杂的。 有必要将知识的收集与收集的知识的记录分开。 != + .


第四点说:“实际上,你所说的是宇宙的积累,就像弓的皮肤一样,它们彼此叠置,并逐渐彼此分离。”

-非常清楚地说明! -敬佩阿贝里德。 -太清楚了! “他们认为他们了解哲学家,因为他们非常了解洋葱是什么。”

阿伯丁故事,克里斯托夫·马丁·维兰德

有限的上下文和域


想象一下,我们担任过一家渐进式创业公司的设计师。 我们都喜欢冷却的比萨饼,喜欢用快递员诅咒他们,并在网站上填写表格数小时。 因此,我们提出了一个很棒的创业公司“四只乌龟和一只老鼠”:


  • 有一个在其上注册比萨店并张贴其实际菜肴的网站。
  • 这些比萨店没有自己的快递服务。
  • 有些客户无法到达比萨店,但他们准备通过网站或移动应用程序下订单。
  • 杀手'的特征:快递员不是专门雇用的人员,而是通过移动应用程序注册的普通人
  • 快递员收到订单后,便会收到已完成工作的付款。
  • 等待这样的快递员需要更长的时间,但是送货以及相应的比萨更便宜。

让我们回想一下上一章中描述的文档。 在那里,在我们的单本词典中,使用了注册一词 。 但是在这个项目中,我们有几个:


  • 客户注册
  • 比萨店注册
  • 快递注册
  • 订单注册

统一语言属于有限的上下文。 以上文档的域是“授权系统”。 让我们尝试为我们的启动分配域。


但是在开始之前,我们先来看一下有关什么是域以及什么是有限上下文的一些术语。


域(域)-解决特定问题的真实业务结构的表示。

例如:物流系统,付款系统,授权系统,订单管理系统。


该域分为多个子域,这些子域描述了较小的结构,例如:一篮子订单,用于构建路线的系统。


每个域的责任范围都有限-功能有限。


有界上下文-一组域限制,可帮助域仅将一项任务集中于最佳解决方案。

我喜欢将此术语表示为一种抽象。 域是一个圆。 受限制的上下文是一个圆圈。


域部份


即使使用DDD术语,也会分配内核。


核心(核心域)-最能代表您的业务特征的最重要域。

因此,项目“四只乌龟和一只老鼠”的领域:


与比萨店合作 (Pizzarias)


上下文 :b2b与比萨店有关的一切


子域名


  • 新比萨店的注册
  • 添加分类
  • 更新产品可用性的状态

与客户 (客户)合作


上下文 :b2c,与与披萨客户合作有关的一切


子域名


  • 查看分类
  • 信息资料

与快递员合作 (送货系统)


背景 :b2e,与与快递员合作有关的一切


子域名


  • 快递注册
  • 任务分配
  • 登记提取快递员赚取的资金的申请。

订单系统


上下文 :内核。 允许您协调所有单个域,提供从接收订单到向用户发送披萨的完整周期。 它不是表演者,而是扮演指挥者的角色。


子域名


  • 订单接受
  • 订单执行
  • 订单状态跟踪

结算系统 (计费)


上下文 :包含所有财务交易。 它提供与处理中心的交互。


子域名


  • 接受订单钱
  • 给快递员做工作的钱

统计系统


上下文 :分析信息的收集和处理(而不是发布)。


子域名


  • 资金统计
  • 应用统计

管理系统 (管理面板)


上下文 :发布分析信息。 管理决策工具包。


  • 基于收集的统计数据进行分析
  • 预付快递员付款

根据域,让我们对其进行映射。


域映射(上下文映射)是一种图形工具,可让您描述各个域之间的关系。

侧面图


该地图显示了域之间的链接。 该地图非常肤浅,但是对主题领域的了解却很少。 这是第一个草图,重写它会得到预期的结果。


地图上最重要的是,我们看到了域之间的连接。 这样的结构非常适合微服务架构:


微服务架构的主要原理:弱连接性和强粘附性。

山姆·纽曼(Sam Newman)-《 创建微服务》(Create Microservices)一书中给出了该原理,这是您为了开始实际使用本文中介绍的方法而必须阅读的第二本书。 含义:域应松散耦合,但内部应紧密相连。


这些术语的翻译来自官方的俄语翻译,可能无法很好地反映所传达的含义。 原始术语是:低耦合(连接性,接合,抓地力,接合),高内聚性(连接性,强度)。


域共享实施实践


我想分享我的个人经验-一系列明智的决定。 我不敦促您使用这些解决方案。 但是,如果您不知道从哪里开始,那么它们可能是一个不错的选择。 凭着个人经验,这些工具将进一步适合您的需求。


指导我们的主要原则:


  • 解决方案的简单性。 使复杂的事情变得简单,而不是简单的复杂。
  • 实用主义 您始终需要查看情况,并且在没有现有解决方案的情况下,开发新的解决方案。 尽量代表一切,但要避免教条主义。
  • 代码!=文档。 代码是机器的说明,文档是人的说明。 无需混淆它们,一个接一个地发出。
  • 实心

如何实现域名?


选择域作为单独的微服务非常方便。


微服务是实现一个域的逻辑的独立应用程序。

在DDD开发中,将微服务分配到单独的应用程序中的原理将受到限制。 这并不会否定服务分离​​的技术原理(如果这是由于需要确保高性能)。 但是上下文原则将是主导性和约束性的。


如何突出域之间的关系?


域之间的关系始终是一个API。 可能是RESTful json API,gRPC,AMPQ。 在本文的框架内,我们不会将一种协议与另一种协议进行比较,并强调它们的优缺点;每种协议都有自己的应用领域。 但是,让我们继续关注一般建议:


选择协议时要灵活,执行协议的一致性要严格。


为每对域分别选择一个协议,不要尝试在所有地方都使用http,您可能需要在某个地方使用异步队列,并且AMPQ的优势对您来说显而易见。 不要忽略这个机会,因为到处都有RESTful。


另一方面,如果要实现RESTful json,请使用一个数据结构标准。 您可以准备就绪,例如jsonapi或openapi。 如果由于某种原因,现成的解决方案不适合您,并且您认为可以开发,描述和使用它。 但是到处使用它,不要滋生“动物”标准。 如果您需要与他们对标准一无所知的外部系统进行通信,请编写一个微服务适配器。


转接头


如何实现子域?


作为微服务中的独立模块。


通过将逻辑放入单个微服务内的单独命名空间(名称空间)中,模块是子域的实现。

看起来像什么? 让我们来看一个例子。 我们记得,我们有一个Delivery System域-该域有三个子域:


  • 快递注册
  • 任务(任务)的发布
  • 登记由快递员赚取的资金的提取申请(提取)
  • 检查您的微服务是否正常工作,辅助技术工具(healt_checker)

想象一下所有这些以文件夹结构的形式:


 $ tree --dirsfirst delivery_system delivery_system ├── app/ │ ├── health_checker/ │ │ └── endpoints.rb │ ├── registrations/ │ │ ├── entities/ │ │ ├── forms/ │ │ ├── repositories/ │ │ ├── interactor/ │ │ ├── services/ │ │ ├── validations/ │ │ ├── endpoints.rb │ │ └── helpers.rb │ ├── tasks │ │ ├── entities/ │ │ ├── queries/ │ │ ├── repositories/ │ │ ├── endpoints.rb │ │ └── helpers.rb │ └── withdrawals │ ├── entities/ │ ├── forms/ │ ├── repositories/ │ ├── interactor/ │ ├── services/ │ ├── validations/ │ ├── endpoints.rb │ └── helpers.rb ├── config/ ├── db/ ├── docs/ ├── lib/ │ ├── schemas/ │ └── values/ ├── public ├── specs ├── config.ru ├── Gemfile ├── Gemfile.lock ├── Rakefile └── README.md 

apps/目录中的每个文件夹都实现一个或另一个子域,每个域中都有多种模式: entitiesformsservices等。我们将在以后的文章中详细讨论每种应用的模式。


每个这样的模式都在相应的名称空间(名称空间)中实现。 例如,用于创建向快递公司付款的应用程序的表格:


 module Withdrawal #   module Forms #  class Create end end end 

如何实现子域之间的通信?


让我们看一个具体的例子。 我们有一个快递帐户: Registrations::Entities::Account 。 它指的是Registrations子域-因为我们认为此域不是注册过程,而是帐户表和注册簿,如业务文档中所示。


我们有两个执行流程 ,可以访问该帐户。


  • 创建一个帐户(注册)
  • 申请提取快递员赚取的资金(Wihtdrawal)

如我们所见,这两个过程属于不同的子域-注册和Wihtdrawal。


 module Registrations module Serivices class CreateAccount def call account = Entities::Account.new end end end end module Withdrwals module Serivices class CreateOrder def call account = Registrations::Entities::Account.new end end end end 

在第一种情况下,将通过对Entities::Account的调用来实现对该类的调用。 在第二种情况下,通过显式调用Registrations::Entities::Account 。 即 如果我们明确指定一个子域,则该类来自另一个子域,因此我们清楚地指出了连接。


如果该类未明确应用于任何子域,则将其移至lib/文件夹是有意义的。 通常,这些是实现“ ValueObject”模式的类。 在以下文章之一中,我们将更详细地研究这种模式。


通过模型执行。


引用埃里克·埃文斯的话:


如果程序的体系结构或其至少某些中心部分与领域模型的结构不符,则该模型实际上是无用的,还应质疑程序的正确操作。 同时,很难理解软件体系结构中模型和功能之间的关系太复杂,并且在实践中,随着体系结构的变化,很难维护它们。

让我们回想一下本文开头已经提到的一个好的模型的例子-地形图。 我们的目标是能够快速找到两个定居点之间的距离。 我们可以使用参考表来显示城市之间的两个点。 我们可以使用该卡。 那里和那里我们大约在同一时间获得相同的结果。 但是地图更紧凑,它更准确地显示主题区域,更通用。 地图作为模型具有极高的表现力。 而且,如果我们在此任务的框架内考虑它,那么在地图上测量距离比在其本身反映的领土上更方便。 反映主题区域的模型在某些属性上可能会超越它。 真的很棒。


模型的实施始终是一个具有不可预测结果的创新过程。 代码的质量不是其性能,也不是其复杂性,而是简单性和表达性。 通过不断的重构来改进它,使其变得灵活并切断所有不必要的东西。 将负责模型的业务逻辑的层与需要归因于技术实施的层分开。 我们将如何做到这一点将在后面描述。




Source: https://habr.com/ru/post/zh-CN428209/


All Articles