域驱动设计工具

蓝鲸是一个复杂项目的设计出错的很好的例子。鲸鱼看起来像鱼,但它是哺乳动物:它用牛奶喂养小熊,有毛,而且前臂的骨头和用手指的手指(像陆地上的手指一样)仍保留在鳍中。他生活在海洋中,但无法在水下呼吸,因此即使他在睡觉,他也定期升到水面吞咽空气。鲸鱼是世界上最大的动物,有一个九层楼的房子,重达75辆大众途锐汽车,但不是捕食者,而是以浮游生物为食。

当开发人员研究鲸鱼时,他们并没有从头开始编写所有内容,而是使用了旧项目中的经验。它似乎是由未经测试的代码的不兼容部分拼凑而成的,所有设计都归结为选择框架和已经投入生产的紧急“循环”。结果,该项目看上去很漂亮,但里面有许多密集的遗留物和拐杖。



为了创建项目来帮助企业赚钱,而不是看起来像不能在水下呼吸的海洋动物,这里有DDD。这种方法的重点不在于工具或代码,而在于对主题领域,单个业务流程以及代码或工具如何用于业务逻辑的研究。

什么是DDD以及其中包含哪些工具,我们将根据报告在一篇文章中讲述阿尔乔姆MalyshevPython中的DDD方法,工具,陷阱,合同编程以及围绕要解决的问题(而不是所使用的框架)的产品设计都已被削减。

报告的完整介绍

Artem Malyshev证明404)-使用Python编写了5年的独立开发人员,在Django Channels 1.0方面提供了积极的帮助。后来他专注于架构方法:他研究了Python架构师缺少的工具,并开始了一个干Python项目Drylabs的联合创始人。

复杂


什么是编程?
编程人员一直在与开发人员尝试解决问题时创建的复杂性作斗争。
复杂度分为两种:引入的和自然的。引入的内容与编程语言,框架,OS,异步模型一起扩展。这是一项技术挑战,不适用于企业。自然的复杂性隐藏在产品中,并简化了用户的生活-为此,人们需要花钱。
好的工程师应该减少增加的复杂性并增加自然性以增加产品的实用性。
但是我们程序员是复杂的人,我们喜欢为项目增加技术复杂性。例如,我们不用担心编码标准,没有使用linter,模块化设计实践,并且在项目中收到了很多样式代码if c==1

如何使用这样的代码?阅读大量文件,了解变量,条件以及何时以及如何工作。很难记住此代码-绝对是技术上的复杂性。

增加复杂性的另一个例子是我最喜欢的“回调地狱”。



当我们在面向事件的体系结构(EDA)的框架中编写并选择一个不太好的现代框架时,我们得到的代码不清楚何时发生什么。很难阅读这样的代码-这又是增加的复杂性。

程序员不仅喜欢技术上的困难,而且还争论哪一个更好:

  • AsyncIO或Gevent;
  • PostgreSQL或MongoDB;
  • Python或Go;
  • Emacs或Vim;
  • 制表符或空格;

一个好的程序员对所有这些问题的正确答案是:“没关系!” 好的开发人员不会在真空中争论球形的马,而是要解决业务问题并致力于产品的实用性。他们中的一些人已经建立了一套实践,可以减少引入的复杂性并帮助您更多地考虑业务。

其中之一是埃里克·埃文斯Eric Evans)2004年,他撰写了《域驱动设计》一书。她“开枪”并一时冲动,想更多地考虑业务,并将技术细节推向后台。



什么是DDD?


首先是解决问题的方法,然后是工具。首先,埃文斯投资了DDD的概念,这不是技术,而是一种哲学。在哲学上,您首先需要考虑如何解决问题,然后才需要借助哪些工具。

与主题专家和软件开发人员一起建立模型。我们必须与企业界人士进行沟通:寻找一种通用语言,建立一个世界模型,我们的产品将在其中发挥作用并解决问题。

编写明确表达模型的软件DDD与团队中的简单协作之间最重要的区别在于,我们应该以与领域专家交流的风格来编写软件。所有的术语,讨论和决策方法都应存储在源代码中,这样即使是非技术人员也可以理解那里发生的事情。

与商业交流DDD是一种关于如何与特定领域的业务专家说同一语言并将术语应用于该领域的哲学。在绑定的上下文中,我们有一种共同的语言或方言,我们认为这是真的。我们围绕架构解决方案创建边界。

DDD与技术无关。

首先是技术部分,然后是DDD。从石头上雕刻雕像的雕刻家没有阅读有关如何握住锤子和凿子的手册-他已经知道如何使用它们。要将DDD引入您的项目,请精通技术部分:学习Django到最后,阅读教程并停止争论是否使用PostgreSQL或MongoDB。

大多数设计模式和模式都是技术噪声。我们知道和使用的大多数模式都是技术性的。他们说了如何重用代码,如何构造代码,但没有说如何将代码用于用户,企业和为外部世界建模。因此,工厂或抽象类被松散地绑定到DDD。

第一本“蓝皮书”大约在20年前问世。人们试图以这种风格写作,走了耙,意识到这种哲学是好的,但实际上却难以理解。因此,出现了第二本书-“红色”,关于程序员如何在DDD中进行思考和编写。


“红色”和“蓝色”书是所有DDD站立的支柱。

注意。红色和蓝色的书是有关DDD的唯一信息来源,但是它们很繁琐。书籍不易阅读:由于语言和术语复杂,一本为原始书,而由于翻译不善,一本为俄文。因此,从一本绿开始学习DDD 。这是前两个版本的简化版本,带有更简单的示例和一般说明。但这要比红色和蓝色的书胜过您学习和应用DDD的愿望要好。最好阅读原著。

红皮书跳过了如何最好地将DDD引入项目,如何围绕这种方法来组织工作的想法。出现了一个新术语-“模型驱动设计”,其中我们的外部模型放在第一位。



选择的唯一技术是智能UI。这是外部世界,用户和我们之间的一层(对Robert Martin及其干净的体系结构的引用)。如您所见,一切都归于模型。

什么是模特?这是任何建筑师的幻影之痛。每个人都认为这是UML,但事实并非如此。
模型是一组类,方法以及它们之间的链接,它们反映程序中的业务场景。
该模型反映了具有所有必要属性和功能的真实对象。这是从业务案例的角度进行决策的高级工具包。但是,方法和类是用于体系结构解决方案的低级工具包。

干蟒蛇


为了填补模型的空白,我开始了一个干Python项目,该项目已成长为用于构建模型驱动设计的高级体系结构库集合每个库都试图缩小体系结构中的一个圆圈,并且不干扰另一个圆圈。可以单独使用库,也可以根据需要一起使用。



叙述的顺序对应于DDD向项目中最佳添加的时间顺序(逐层)。第一层是服务,这是我们系统中业务场景(流程)的描述。故事库负责此层。

故事


业务场景分为三个部分:

  • 规范 -业务流程的描述;
  • 国家在其中的业务场景可能存在
  • 执行脚本每个步骤。

这些部分不得混用。Stories库将这些部分分开,并在它们之间划清了界限。

考虑使用示例介绍DDD和故事。例如,我们在Django上有一个项目,其中包含Django信号的混搭和模糊的“厚”模型。向其中添加一个空的服务包。局部使用Stories库,我们将此哈希重写为项目中一组清晰易懂的脚本。

DSL规范。该库允许您编写规范并为此提供DSL。这是逐步描述用户操作的方式。例如,要购买subscription,我需要执行几个步骤:我将找到一个订单,检查价格的相关性,检查用户是否负担得起。这是一个高级描述。

合同。在此类下,我们将针对业务场景的状态编写合同。为此,我们表示业务流程中出现的变量区域,并为每个变量分配一组验证器。

一旦有人尝试将变量分配给该区域作为业务流程的一部分,就会制定出一组验证器。我们将确保运行时进程的状态始终正常。但是,如果没有,它会痛苦地跌落并大声尖叫。

每个步骤的执行阶段。在同一类中,subscription我们编写了一组名称与业务步骤相对应的方法。每个输入方法都可以使用一种状态,但无权对其进行修改。该方法可能返回一些标记并报告:

  • , () ;
  • - , .


还有更复杂的标记:它们可以确认状态是否正常,建议删除或更改业务流程的某些部分。您也可以编写类。

启动故事。如何在执行时运行Story?这是一个用作方法的业务对象:我们将数据传输到输入,它会验证它们并解释步骤。正在运行的Story会记住执行历史,记录业务流程中发生的状态,并告诉我们是谁影响了该状态

调试工具栏。如果我们使用Django编写并使用调试面板,则可以看到每个请求中处理了哪些业务场景及其状态。

高温测试。如果我们用py.test编写,那么对于失败的测试,我们可以看到每行执行了哪些业务脚本以及出了什么问题。这很方便-我们无需阅读代码,而是阅读规范并了解发生了什么。



哨兵。更好的是,当我们收到错误500时。在常规系统中,我们忍受了并开始调查。在Sentry中,将显示有关用户为犯错所做的详细报告。在凌晨3点为您收集此类信息时,它既方便又愉快。


ELK。现在,我们正在积极开发一个插件,该插件将Elasticsearch中的所有内容写入Kibana堆栈并构建有效的索引。



例如,我们有一个关于业务流程状态的合同。例如,我们知道那里有什么relation ID报告。与其对曾经发生的事情进行过时的研究,我们在Kibana中写了一个请求。它将显示与特定用户相关的所有故事。接下来,我们检查业务流程和业务场景中的状态。我们没有写一行日志代码,但是项目是在我们感兴趣的抽象层进行日志记录的。

但是我想要更高层次的东西,例如轻的物体。例如,此类对象包含与业务决策的采用相关但与数据库无关的文化数据结构和方法。因此,我们继续进行模型驱动架构的下一部分-实体,集合和值对象。



实体,集合和价值对象


所有这些如何相互联系?例如,用户下了一个产品订单,然后我们开票。聚合的根源是什么,什么是简单对象?



强调的只是聚合的根源。我想直接与之合作:重要,有价值,全面。

从哪里开始?我们将在项目中创建一个空包,在其中放置我们的单元。聚合最好用诸如dataclasses或的声明性语言编写attrs

数据类。如果dataclass我们指示某种聚合,则将使用NewType在其上编写注释。在注释中,我们指示一个显式引用,该引用以类型系统表示。如果dataclass只是数据结构(实体),则将其保存在集合中。

在“故事”的上下文中,只能聚集。只有通过公共方法和高级规则才能访问嵌入其中的内容。这使您可以逻辑地,有能力地建立模型,我们将与该领域的专家一起合作。这是同一种语言



问题立即出现-存储库。我有一个通过Django处理的数据库,附近是我向其发送请求的微服务,其中包含JSON和Django模型的实例。要接收数据并手动传输数据,仅仅是为了漂亮地调用或测试该方法?当然不是。Dry-python有一个Mappers库,可让您将高级抽象和域聚集映射到我们存储它们的地方。

映射器


我们向我们的项目中添加了另一个软件包-我们将在其中存储我们的存储库mappers。这就是我们将高级业务逻辑转移到现实世界的方式。

例如,我们可以描述如何将一个映射dataclass到Django模型。

Django ORM。我们将订单模型与Django ORM的描述进行比较-我们查看这些字段。

例如,我们可以通过可选配置重写某些字段。将发生以下情况:mapper在声明期间,它将比较dataclass模型的编写方式。例如,Django模型中的注释int(在带有注释Order dataclass的字段中与option 相对应。这将提供补充,或删除costintinteger fieldnullable="true"dataclassoptionaldataclassnullable来自field

通过Mappers,您可以添加读取或写入内容的函数。读取器是在输入处接收聚合并返回对其的引用的函数。作家做相反的-返回单位。例如,在后台,可能会通过Django向数据库发出请求。

昂扬的定义微服务可以执行相同的操作。您可以在其上编写部分swagger方案,并检查特定服务的swagger方案与您的域模型匹配多少。此外,从请求库返回的请求将透明地转换为dataclass

GraphQL查询。 GraphQL和微服务:GraphQL接口类型架构可以很好地应对dataclass。您可以将特定的GraphQL查询转换为内部数据结构。

为什么要打扰应用程序内部的内部高级数据模型?为了说明“为什么”,我将讲一个“有趣的”故事。

在我们的一个项目中,Web套接字通过Pusher服务工作。我们没有打扰,我们将其包装在一个接口中以免直接调用。该界面与所有故事相关,并很满意。

但是业务需求已经改变。事实证明,Pusher为Web套接字提供的保证还不够。例如,您需要保证最近2分钟的邮件传递和邮件历史记录。因此,我们决定移至Ably Realtime服务。它还有一个接口-我们将编写一个适配器并将其绑定到任何地方,一切都会很棒。并不是的。

Pusher使用的抽象(函数参数)被捕获在每个业务对象中。我必须修复大约100个故事,并修复我们要向其发送消息的用户渠道的形成。

回到测试。

测试与模拟


您通常如何通过外部服务测试此行为?我们正在弄些东西,正在观察如何调用第三方库,仅此而已-我们确信一切都很好。但是,当库更改时,参数格式也将更改。

如果您对内部模型的行为进行不同的测试,则可以节省一周的时间来重写数千个测试和数百个业务案例。例如,类似于集成测试的内容:我们写入用户流,并且已经在适配器,Pusher或Ably内部,我们将此流转换为普通通道的名称,以免将其全部写入业务逻辑。

依存关系


在这样的模型架构中,出现了许多多余的实体。以前,我们采用了某种Django函数并编写了该函数:请求,响应,最小身体移动。在这里,您需要初始化Mappers,放入Stories并进行初始化,处理HTTP请求的请求行,看看给出哪个答案。所有这些都会在Django-view中产生30-50行样板调用代码Stories。

另一方面,我们已经编写了接口和Mappers。我们可以检查它们与特定业务案例的兼容性,例如,使用“依赖关系”库。怎么样?通过Dependency注入模式,所有内容都声明性地粘贴到最小的样板上。

在这里,我们指示在服务包中上课的任务,将三个mappers,初始化Stories对象并将其提供给我们。通过这种方法,代码中的样板数量大大减少了。

重构图


使用我所说的一切,我们开发了一种方案,通过该方案,我们使用DDD将Django信号(隐式“回调地狱”)中的一个大型项目重写为Django。

没有DDD的第一步。最初我们没有DDD-我们编写了MVP。当他们赚到第一笔钱时,他们邀请了投资者,并说服他们转向DDD。

没有合同的故事。我们将项目分解成没有数据合同的逻辑业务案例。

合同和总额。然后,我们一个接一个地拖动每个模型的数据协定,这可以在我们的体系结构中进行跟踪。

映射器。映射器编写以摆脱数据仓库模板。

依赖注入。摆脱粘合模式。

如果您的项目的MVP超过了,并且迫切需要在体系结构中进行更改,以使其不会遗留在旧系统中-请看一下DDD。

legacy Python-, , , Moscow Python Conf++ 27 . Python . unconference, , , , Drylabs.

DDD Python, TechLead ConfIT-, DDD . 8 , Call for Papers 6 .

All Articles