开发人员手册:DDD食谱(第3部分,应用程序体系结构)

引言


在之前的文章中,我们确定了方法的范围,并研究了域驱动设计基本方法论原理


在本文中,我将概述构建公司系统体系结构的主要现代方法:柔软,尖叫,干净,并以完整的交钥匙解决方案的形式对它们进行清晰的解释。


仓库管理


将来,我们将详细考虑每种设计模式:我们指出范围,给出代码示例,突出推荐的做法。 结果,我们将编写一个现成的微服务。


灵活的架构


上一篇文章中,我们关注DDD包括通过模型实现的实践。 主题区域应通过您的代码进行描述。 让我们尝试找出方法。


在他的书中,埃里克·埃文斯(Eric Evans)提供了一系列推荐的设计模式,并将这种方法指定为灵活的:


以架构灵活性为名,程序中堆积了许多不必要的构造。 过多的抽象和间接链接比对此有所帮助的可能性更大。 看一下真正激发程序员对其进行改进的体系结构,您通常会看到一些非常简单的东西。 但是简单并不意味着易于执行。 为了创建可以组装到复杂系统中的元素,同时又易于理解,有必要将“奉献”与根据严格的体系结构样式进行模型设计结合起来。 不仅需要某种设计技巧,而且还要使用完成的作品。

Eric Evans,域驱动设计:解决软件核心问题的复杂性

提出的一组设计模式不是严格的体系结构或现成的解决方案,而是值得深思的。


华丽的建筑


在复杂系统的许多开发人员和设计师的脑海中也产生了类似的想法。


在2011年,Robert Martin- Screaming Architecture发表了一篇文章,其中说您的代码不仅应该描述主题领域,还应该大声疾呼,最好是ob亵。


那么,您的应用程序体系结构会发出什么尖叫呢? 当您查看顶级目录结构以及最高级别软件包中的源文件时; 他们会尖叫:医疗保健系统,会计系统或库存管理系统吗? 还是他们尖叫:Rails,Spring / Hibernate或ASP?

罗伯特·C·马丁,2011年9月30日

罗伯特说,您的应用程序代码应反映应用程序的活动,而不是适应框架规则。 框架结构不应限制您的体系结构。 反过来,不应将应用程序附加到数据库或http协议,它们只是存储和交付机制。 边界框是一个工具。 您不应该成为该框架的拥护者。 应用程序的测试是对其操作逻辑的测试,而不是对http协议的测试。


干净的建筑


一年后,罗伯特·马丁(Robert Martin)的下一篇文章- 清洁建筑The Clean Architecture) 。 在其中,作者讲述了如何使代码尖叫。 在研究了几种架构之后,他确定了基本原理:


  1. 框架独立性。 该体系结构不依赖于任何现有库。 这使您可以将框架用作工具,而不是束缚双手的约束。
  2. 可测试性。 无需用户界面,数据库,Web服务器或任何其他技术手段,即可测试业务规则。
  3. UI独立性。 用户界面可以轻松更改,而无需更改系统的其余部分。 例如,可以在不更改业务逻辑的情况下将Web界面替换为控制台界面。
  4. 数据库独立性。 您可以将Oracle或SQL Server换成Mongo,BigTable,CouchDB或其他东西。 您的应用程序逻辑不应与数据库绑定。
  5. 不受环境影响。 实际上,您的业务规则根本不了解外界。

在哈伯杂志上已经发表了一篇非常好的文章《 误解清洁建筑》 。 它的作者Jeevuz很好地理解了这种方法的复杂性。 我强烈建议您熟悉它和原始材料。


可变架构


上述方法的描述看起来并不那么简单。 作为许多复杂公司系统的体系结构开发的一部分,我和我的同事已经对所描述的方法做出了相当清晰的解释,我打算在下面进行介绍。


在计算机和编程语言问世之前,书面工作流程曾用于构建和管理具有复杂业务逻辑的系统。 任何过程的结果都是最终描述特定业务对象的文档。 结果,文书工作归结为三个简单的操作


  1. 文件制作
  2. 文件处理
  3. 处理文档存档
  4. 文件提交

文档-记录有关特定实际业务对象的经济活动的信息。

请注意,文档本身不是真正的业务对象,而只是其Model 。 当前,纸质文件正被电子文件取代。 文档可以是表格中的记录,图片,文件,已发送的信件或任何其他信息。
我不想在将来使用“文档”一词,因为它会更加混乱,因此我们将使用DDD术语中的“实体”概念。 但是您可以想象现在您的整个系统是一个执行四个简单操作的电子文档管理系统。


  1. 收集中
  2. 处理中
  3. 贮藏
  4. 代表权

行动-业务模型活动的结构单位; 有意识的目标的相对完整的独立行为,即业务对象的个人活动的任意性和故意性,由最终用户加以区分。

动作的一个很好的例子是戏剧表演。 剧院模拟现实生活中的事件。 表演是戏剧中有意义的一部分。 但是,要使故事完整,您需要按照严格指定的顺序进行多种动作。 在我们的体系结构中,这样的命令称为模式


模式(实施)-一组具有一定含义的动作 ,具有完整的含义,对最终用户有利。

传导


对于这种操作模式,发明了一种选择导体或选择器。 更准确地说,获得了专利“ US2870278A ”的“用于执行多个操作序列中的一个选择的时序机构”。 我们将此设备称为洗衣机的“扭曲”。 本文开头给出了体系结构上的改动。


该方法的可变性体现在以下事实上:使用这种体系结构,您可以选择四种模式中的任何一种,通过这些模式 ,您将不会执行不必​​要的操作


启动洗衣机时,可以选择以下模式:洗涤,漂洗或旋转。 如果选择洗涤,则机器仍将冲洗衣物,然后将其挤压。 使用套件中的漂洗剂,您一定可以旋转。 旋转-洗涤过程中的最后动作 ,是最“简单”的动作。 在我们的体系结构中,最简单的ActionRepresentation ,我们将从它开始。


表现形式


如果我们在不借助数据库或外部资源的情况下谈论纯演示文稿,那么我们将给出一些静态信息:html页面,文件,以json形式存在的目录。 我们甚至可以给出代码响应 -200:


让我们来编写最简单的“健康检查器”


module Health class Endpoints < Sinatra::Base get '/check' do; end end end 

以其最原始的形式,我们的方案将如下所示:


代表权


抒情离题

我想请您注意,在Sinatra框架中, Endpoints类将RouterController组合在一个类中。 这是否违反了唯一责任原则? 实际上,端点不是一个类,而是通过一个类表示的层,它在更高级别上的职责范围。


好的, 路由器控制器呢? 它们不是通过一组类来表示,而是通过函数的名称和实现来表示。 静态文件通常是文件。 一个班级负责一项责任,但不要试图通过一班级来表达每一项责任。 使用实用性,而不是教条主义。


使用存储系统


业务要求您的应用程序具有可用性。 如果我们不能在正确的时间使用您的服务,为什么有人需要您? 为了确保数据完整性,我们在每次处理后记录业务对象状态的变化。


从存储中检索对象不需要访问业务逻辑。 想象一下,我们可以自动执行连锁酒店的活动,并且接待处有客人杂志。 我们决定查看有关访客的信息。


  module Reception class Endpoints < Sinatra::Base # Show item get '/residents/:id', provides: :json do resident = Repository::Residents.find params[:id] status 200 serialize(resident) end end end 

以图形形式使用存储系统


贮藏


如我们所见,负责存储的层和负责呈现数据的层之间的通信是通过Response模型实现的。 该模型不属于任何这些层。 实际上,这是一个业务对象,它位于负责业务逻辑的层上。


处理中


如果涉及到对象模型根据其属性更改而没有引入新数据的事实,那么我们直接转向Interactor层。 Interactor层是我们应用程序中的关键,它以单独的用例的形式描述了整个业务逻辑,并且实体在此上进行了更改。


考虑这个用例。 访客已经在我们的酒店注册,但是我们会庆祝他的每次到达或离开。


  module Reception class Endpoints < Sinatra::Base # Register resident arrival post '/residents/:uid/arrival', provides: :json do result = Interactors::Arrival.call(resident_id: params[:id]) check!(result) do status 201 serialize result.data end end # Register resident departure post '/residents/:uid/departure', provides: :json do result = Interactors::Departure.call(resident_id: params[:id]) check!(result) do status 201 serialize result.data end end end end 

让我们停一下。 为什么不将实现作为带有status参数的单个方法呢? ArrivalDeparture 交互器根本不同。 如果有客人来找我们,那么我们必须检查打扫是否结束,是否有新消息给他等。 相反,在他离开后,我们必须在必要时开始清洁。 反过来,我们甚至都不记得这些消息,因为如果他在旅馆里,我们会立即给他打电话。 我们在Interactor层上规定的就是所有这些业务逻辑。


处理中


但是,如果我们有外部数据怎么办? 此处连接了数据收集操作


收集资料


最初在酒店登记客人时,他会填写登记表。 此表格正在验证中。 如果数据正确,那么将进行注册业务流程。 该过程返回数据-创建的“租户”业务模型。 我们以易读的形式向访客展示此模型:


 module Reception class Endpoints < Sinatra::Base # Register new resident post '/residents', provides: [:json] do form = Forms::Registration.new(params) complete! form do check! form.result do status 201 serialize form.result.data end end end end end 

从示意图上看,它看起来像这样:


收集中


游戏规则(规则)


  • 从过程的角度来看,变异系统分为动作(Actions)
  • 动作顺序由Mode决定。
  • 模式是增量的。
  • 更为“复杂”的模式是严格执行一项操作的“更简单” 模式的补充。
  • 每个动作都在一个Layer的框架内进行。
  • 每层由一个Class表示。
  • 在层内部,可能存在“ 类”和“责任类”
  • 通信仅在LayerInterlayer类之间进行
  • 表示模型是例外。
  • 错误处理应该在Class-Layer级别进行

树


一般方案


该方法具有较高的进入阈值。 它的应用需要设计人员丰富的经验,以清楚地了解要解决的任务。 复杂性还代表所需工具的多种选择。 但是,尽管结构复杂,但是在代码级别的实现却非常简单和富有表现力。 尽管其中包含许多约定和授权书。 将来,我们将分别分析每个设计模板,描述如何创建,测试和指定范围。 为了避免混淆他们的多样性,我们提供了完整的地图:


高分辨率地图




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


All Articles