引言
因此,我们已经决定了范围 , 方法和体系结构 。 让我们从理论转向实践,再到编写代码。 我想从描述业务逻辑的设计模式开始- 服务和交互器 。 但是在开始使用它们之前,我们将研究结构模式-ValueObject和Entity 。 我们将以红宝石语言进行开发。 在其他文章中,我们将分析使用Variable Architecture开发所需的所有模式。 本系列文章的所有应用开发都将收集在一个单独的框架中。

而且我们已经选择了一个合适的名称-LunaPark 。
当前的发展发布在Github上 。
检查完所有模板后,我们将组装一个功能完善的微服务。
从历史上看
需要重构用Ruby on Rails编写的复杂企业应用程序。 有一个现成的红宝石开发团队。 域驱动开发方法非常适合这些任务,但是没有使用所用语言的交钥匙解决方案。 尽管事实上语言的选择主要是由我们的专业决定的,但事实证明它是相当成功的。 在我看来,在所有用于Web应用程序的语言中,ruby最具表现力。 因此,比其他更适合对真实对象建模的对象。 这不仅是我的观点。
那就是Java世界。 然后,您将拥有像Ruby这样的新手。 Ruby具有非常有表现力的语法,在基本的层次上,它应该是DDD的很好的语言(尽管我还没有听说过在这类应用程序中实际使用它的情况)。 Rails引起了很多兴奋,因为它终于使创建Web UI变得像1990年代初Web之前的UI一样容易。 目前,此功能主要用于构建大量Web应用程序,这些Web应用程序背后没有太多的域丰富性,因为在过去,即使这些应用程序都非常困难。 但是我希望,随着问题的UI实现部分减少,人们将看到这是一次将更多注意力集中在领域上的机会。 如果Ruby的使用开始朝这个方向发展,我认为它可以为DDD提供一个出色的平台。 (可能需要填写一些基础架构。)
埃里克·埃文斯(Eric Evans)2006
不幸的是,在过去的13年中,没有发生太大变化。 在Internet上可以找到尝试使Rails适应这种情况的尝试,但是它们看起来都很糟糕。 Rails框架笨拙,缓慢,而且不是SOLID。 不知不觉地看着有人试图基于ActiveRecord来描述存储库模式的实现是非常困难的。 我们决定采用微框架并将其修改为我们的需求。 我们尝试了Grape ,带有自动文档编制的想法似乎成功了,但后来被放弃了,我们很快就放弃了使用它的想法。 几乎立即,他们开始使用另一种解决方案-Sinatra 。 我们仍然继续将其用于REST 控制器和端点 。
REST?如果您开发了Web应用程序,那么您已经对这项技术有所了解。 它有其优点和缺点,其完整列表不在本文讨论范围之内。 但是对我们来说,作为企业应用程序的开发人员,最重要的缺点将是REST(即使从名称上也很明显)不是反映过程而是反映过程的状态。 优势在于它的易懂性-后端开发人员和前端开发人员都清楚该技术。
但是,然后也许不专注于REST,而是实现您的http + json解决方案? 即使您设法开发服务API,然后将其描述提供给第三方,您也会收到很多问题。 比您提供熟悉的REST要多得多。
我们将考虑使用REST作为一种折衷解决方案。 为了简洁起见,我们使用JSON和jsonapi标准,以便不浪费开发人员有关请求格式的时间。
将来,当我们分析Endpoint时 ,我们将看到为了摆脱休息,仅重写一个类就足够了。 因此,如果对REST存有疑问,则REST完全不应该打扰。
在编写多个微服务的过程中,我们已经获得了基础-一组抽象类。 每个此类课程都可以在半小时内完成,如果您知道此代码的用途,则其代码很容易理解。
这里出现了主要困难。 不处理DDD做法和干净架构的新员工无法理解代码及其目的。 如果我自己在阅读Evans之前第一次看到此代码,我会将其视为遗留的,过度设计的代码。
为了克服这一障碍,决定编写一份文档(指南)来描述所使用方法的原理。 该文档的概述似乎很成功,因此决定将它们放在Habré上。 从项目到项目重复的抽象类,决定放入单独的gem中。
经营理念

如果您想起一些有关武术的经典电影,那么就会有一个很酷的家伙,他非常精明地操纵杆子。 六分之一本质上是一根棍子,一种非常原始的工具,是最早落入人类手中的工具之一。 但是在主人的掌握下,他成为了强大的武器。
您可以花时间创建不会射击您腿部的手枪,也可以花时间学习如何射击。 我们确定了4个基本原则:
- 您需要使复杂的事情变得简单。
- 知识比技术更重要。 一个人比一个代码更容易理解文档;一个不应该用另一个代替。
- 实用主义比教条主义更重要。 标准应指导方法,而不是设置边界框。
- 体系结构的结构性,解决方案选择的灵活性。
例如,可以在ArchLinux OS- The Arch Way中找到类似的哲学。 在我的笔记本电脑上,Linux很久没有扎根,它早晚崩溃了,我经常不得不重新安装它。 这造成了许多问题,有时是严重的问题,例如工作截止日期的中断。 但是在安装Arch之后花了2-3天之后,我弄清楚了我的操作系统是如何工作的。 之后,她开始工作更加稳定,没有失败。 我的笔记帮助我在几个小时内将其安装在新PC上。 大量的文档帮助我解决了新问题。
该框架具有绝对的高级特征。 描述它的类负责应用程序的结构。 第三方解决方案用于与数据库交互,实现http协议和其他低级事物。 我们希望程序员在出现问题时调查代码,并了解此类的工作原理,而文档则使我们能够了解如何进行管理。 了解引擎设计将使您无法开车。
构架
通常,很难将LunaPark称为框架。 框架-框架,工作-工作。 我们敦促不要限制自己的范围。 我们声明的唯一框架是告诉该类应在其中描述该逻辑的框架。 而是一组带有大量说明的工具。
每个类都是抽象的,分为三个级别:
module LunaPark
如果要实现创建单个元素的表单,请从此类继承:
module Forms class Create < LunaPark::Forms::Single
如果有多个元素,我们将使用另一个Implementation 。
module Forms class Create < LunaPark::Forms::Multiple
目前,并非所有开发都处于完美的状态,gem处于alpha状态。 根据文章的发布,我们将分阶段进行引用。 即 如果您看到有关ValueObject
和Entity
的文章,则这两个模板已经实现。 到周期结束时,所有这些都将适合在项目中使用。 由于没有链接到sinatra \ roda的框架本身很少使用,因此将创建一个单独的存储库,该存储库显示如何“整理所有内容”以快速启动您的项目。
该框架主要是文档的应用程序。 不要将这些文章视为该框架的文档。
所以,让我们开始做生意。
价值对象
-你的女朋友多高?
-151
-你开始遇到自由女神像?
这样的事情可能在印第安纳州发生了。 人类的成长不仅是一个数字,而且是一个计量单位。 并非总是只能通过基元(整数,字符串,布尔值等)来描述对象的属性,有时需要它们的组合:
- 金钱不仅是数字,而且是数字(金额)+货币。
- 日期由一天,一个月和一年组成。
- 要测量体重,单数对我们来说还不够,它还需要一个测量单位。
- 护照号码由一系列号码组成,实际上是由号码组成。
另一方面,这并不总是组合,也许这是基元的一种扩展。
电话号码通常被视为数字。 另一方面,他不太可能有加法或除法的方法。 也许有一种方法可以发布国家/地区代码,并且可以定义城市代码。 也许会有一种装饰性的方法,它将不仅以数字字符串79001231212
的形式79001231212
,而且以可读字符串的形式显示: 7-900-123-12-12
。
也许是装饰师?基于教条,这是无可争议的-是的。 如果我们从常识的角度解决这个难题,那么当我们决定拨打该号码时,我们会将对象本身转移到手机上:
phone.call Values::PhoneNumber.new(79001231212)
如果我们决定将其显示为字符串,那么显然是为一个人完成的。 那么,为什么我们不立即使这一行对人可读呢?
Values::PhoneNumber.new(79001231212).to_s
想象一下,我们正在创建“三轴”在线赌场网站并出售纸牌游戏。 我们将需要“扑克牌”类。
module Values class PlayingCard < Lunapark::Values::Compound attr_reader :suit, :rank end end
因此,我们的类具有两个只读属性:
这些属性仅在创建地图时设置,而在使用时不能更改。 当然,您可以拿一张纸牌并划掉8 ,写Q,但这是不可接受的。 在一个体面的社会中,您很可能会被枪杀。 创建对象后无法更改属性决定了值对象的第一个属性-不变性。
值对象的第二个重要属性是我们如何比较它们。
module Values RSpec.describe PlayingCard do let(:card) { described_class.new suit: :clubs, rank: 10 } let(:other) { described_class.new suit: :clubs, rank: 10 } it 'should be eql' do expect(card).to eq other end end end
这样的测试将失败,因为它们将在地址处进行比较。 为了使测试通过,我们必须按值比较Value-Obects ,为此,我们将添加一个比较方法:
def ==(other) suit == other.suit && rank == other.rank end
现在我们的测试将通过。 我们还可以添加负责比较的方法,但是如何比较10和K? 您可能已经猜到了,我们还将以Value Objects的形式展示它们。 好的,所以现在我们必须像这样发起十大俱乐部:
ten = Values::Rank.new('10') clubs = Values::Suits.new(:clubs) ten_clubs = Values::PlayingCards.new(rank: ten, clubs: clubs)
三行红宝石就足够了。 为了规避此限制,我们引入对象值的第三个属性-营业额。 让我们使用.wrap
类的特殊方法,该方法可以获取各种类型的值并将其转换为正确的值。
class PlayingCard < Lunapark::Values::Compound def self.wrap(obj) case obj.is_a? self.class
这种方法具有很大的优势:
ten = Values::Rank.new('10') clubs = Values::Suits.new(:clubs) from_values = Values::PlayingCard.wrap rank: ten, suit: clubs from_hash = Values::PlayingCard.wrap rank: '10', suit: :clubs from_obj = Values::PlayingCard.wrap from_values from_str = Values::PlayingCard.wrap '10C'
所有这些卡将彼此相等。 如果将wrap
方法扩展为良好做法,则将其放在单独的类中。 从教条主义的观点来看,另外一个班级也是必修的。
嗯,甲板上的空间呢? 如何确定此卡是否为王牌? 这不是纸牌。 这就是纸牌的价值 。 这恰好是您在纸板角上书写的题词10。
有必要将Object-Value和基元相关联,由于某种原因,它没有在ruby中实现。 从这里开始,出现最后一个属性- 对象值未绑定到任何域。
推荐建议
在每个过程的每一个时刻使用的各种方法和工具中,总有一种方法比其他方法更快更好地工作。
弗雷德里克·泰勒(Frederick Taylor) 1914
算术运算必须返回一个新对象
值对象的属性只能是基元或其他值对象
在类方法中保留简单的操作
如果“转换”操作很大,那么将其移至单独的类也许很有意义
仅在隔离服务的情况下, 才有可能将逻辑删除到单独的服务中 :它不使用任何外部来源的数据。 此服务应受“值对象”本身的上下文限制。
对象值对域逻辑一无所知
假设我们正在编写一个在线商店,并且我们对商品进行了评级。 要获取它,您需要通过Repository向数据库发出请求。
实体
Entity类负责某些实际对象。 它可以是合同,椅子,房地产经纪人,派,铁,猫,冰箱等。 您可能需要为业务流程建模的任何对象都是Entity 。
对于埃文斯(Evans)和马丁(Martin), 实体的概念是不同的。 从埃文斯的角度来看,实体是一个具有某种特征的物体,强调了其个性。
Zvans的精华如果对象是由唯一的个体存在而不是由一组属性确定的,则在模型中定义对象时,应将此属性作为主要属性。 类的定义应该简单,并围绕对象存在周期的连续性和唯一性来构建。 找到一种区分每个对象的方法,而不管其形状或存在的历史。 要特别注意与根据对象的属性比较对象有关的技术要求。 定义一个操作,该操作必须为每个这样的对象提供唯一的结果-可能有必要为此将某个符号与保证的唯一性相关联。 这样的识别手段可以具有外部来源,但是也可以是系统出于其自身的便利性而生成的任意标识符。 但是,这样的工具必须遵守在模型中区分对象的规则。 该模型应该给出相同对象的确切定义。
从马丁的角度来看, 实体不是对象,而是一层。 该层将合并对象和用于更改对象的业务逻辑。
马丁的荒凉我对实体的看法是它们包含与应用程序无关的业务规则。 它们不仅仅是数据对象。 它们可能包含对数据对象的引用; 但是它们的目的是实现可被许多不同应用程序使用的业务规则方法。
网关返回实体。 该实现(在行下方)从数据库中获取数据,并使用它来构造数据结构,然后将其传递给实体。 这可以通过包含或继承来完成。
例如:
公共类MyEntity {私有MyDataStructure数据;}
或
公共类MyEntity扩展MyDataStructure {...}
记住,我们都是天生的海盗。 我在这里谈论的规则实际上更像是准则...
本质上,我们仅指结构。 以最简单的形式, Entity类将如下所示:
module Entities class MeatBag < LunaPark::Entities::Simple attr_accessor :id, :name, :hegiht, :weight, :birthday end end
描述业务模型结构的可变对象可以包含原始类型和值 。
LunaPark::Entites::Simple
类非常简单,您可以看到其代码,它仅给我们带来一件事-易于初始化。
LunaPark ::实体::简单 module LunaPark module Entities class Simple def initialize(params) set_attributes params end private def set_attributes(hash) hash.each { |k, v| send(:"
您可以写:
john_doe = Entity::MeatBag.new( id: 42, name: 'John Doe', height: '180cm', weight: '80kg', birthday: '01-01-1970' )
您可能已经猜到了,我们希望将体重,身高和出生日期包装在Value Objects中 。
module Entities class MeatBag < LunaPark::Entites::Simple attr_accessor :id, :name attr_reader :heiht, :wight, :birthday def height=(height) @height = Values::Height.wrap(height) end def weight=(height) @height = Values::Weight.wrap(weight) end def birthday=(day) @birthday = Date.parse(day) end end end
为了不浪费时间在此类构造函数上,我们准备了更复杂的 LunaPark::Entites::Nested
:
module Entities class MeatBag < LunaPark::Entities::Nested attr :id attr :name attr :heiht, Values::Height, :wrap attr :weight, Values::Weight, :wrap attr :birthday, Values::Date, :parse end end
顾名思义,此实现允许您创建树结构。
让我们满足我对大型家用电器的热情。 在上一篇文章中,我们在洗衣机的“扭曲”和体系结构之间进行了类比。 现在,我们将描述像冰箱这样重要的业务对象:

class Refregerator < LunaPark::Entites::Nested attr :id, attr :brand attr :title namespace :fridge do namespace :door do attr :upper, Shelf, :wrap attr :lower, Shelf, :wrap end attr :upper, Shelf, :wrap attr :lower, Shelf, :wrap end namespace :main do namespace :door do attr :first, Shelf, :wrap attr :second, Shelf, :wrap attr :third, Shelf, :wrap end namespace :boxes do attr :left, Box, :wrap attr :right, Box, :wrap end attr :first, Shelf, :wrap attr :second, Shelf, :wrap attr :third, Shelf, :wrap attr :fourth, Shelf, :wrap end attr :last_open_at, comparable: false end
这种方法使我们不必创建不必要的实体 ,例如冰箱的门。 如果没有冰箱,它应该是冰箱的一部分。 这种方法对于编译相对较大的文档(例如,购买保险的应用程序)非常方便。
LunaPark::Entites::Nested
类具有2个其他重要属性:
可比性:
module Entites class User < LunaPark::Entites::Nested attr :email attr :registred_at end end u1 = Entites::User.new(email: 'john.doe@mail.com', registred_at: Time.now) u2 = Entites::User.new(email: 'john.doe@mail.com', registred_at: Time.now) u1 == u2
这两个指定的用户不相等,因为 它们是在不同的时间创建的,因此registred_at
属性的值将不同。 但是,如果我们从比较对象列表中剔除该属性:
module Entites class User < LunaPark::Entites::Nested attr :email attr :registred_at, comparable: false end end
然后我们得到两个可比较的对象。
此实现还具有营业额的属性-我们可以使用class方法
Entites::User.wrap(email: 'john.doe@mail.com', registred_at: Time.now)
您可以将Hash,OpenStruct或任何您喜欢的gem用作Entity ,这将帮助您实现实体的结构。
实体是业务对象的模型,简单起见。 如果您的企业未使用某些属性,请不要对其进行描述。
实体变更
如您所见, Entity类没有自己更改的方法。 所有更改都是从外部进行的。 值对象也是不可变的。 总体上讲,其中存在的所有功能都可以装饰本质或创建新对象。 本质本身保持不变。 对于Ruby on Rails开发人员来说,这种方法将是不寻常的。 从外部看来,我们通常将OOP语言用于其他用途。 但是,如果您看起来更深一些,事实并非如此。 窗户可以自己打开吗? 开车去上班,预定酒店,可爱的猫咪得到新的订户? 这些都是外部影响。 现实世界中发生了一些事情,我们在自己身上反映了这一点。 对于每个请求,我们都会对模型进行更改。 因此,我们将其保持最新状态,足以完成我们的业务任务。 有必要将模型的状态和导致该状态更改的过程分开。 如何执行此操作,我们将在下一篇文章中考虑。