Статьи

DDD для разработчиков Rails. Часть 2: сущности и ценности

В моей предыдущей статье о DDD для разработчиков Rails я говорил об использовании многоуровневой архитектуры для решения проблемы сложности домена. Я показал несколько типичных нарушений многоуровневой архитектуры и дал несколько советов о том, как их исправить.

Строительные блоки доменного дизайна

На этот раз я хотел бы поговорить о строительных блоках доменно-управляемого проектирования и о том, как их можно использовать для моделирования.

Сущности и ценности

В доменно-управляемом дизайне проводится важное различие между сущностями и объектами значений.

  • «Сущность — это объект, определяемый не своими атрибутами, а потоком непрерывности и идентичности». Примером сущности может быть банковский счет. Многие банковские счета могут существовать в нашей системе одновременно. Некоторые из них могут быть отнесены к одному филиалу или иметь одного и того же владельца, но для нашей системы важно рассматривать их как разные учетные записи, если они имеют разные идентификаторы. В случае приложения на Rails идентичность сущности обычно представляется автоматически сгенерированным первичным ключом.

  • «Объект значения — это объект, который описывает некоторую характеристику или атрибут, но не несет в себе понятия идентичности». Поскольку идентичности нет, два объекта значения равны, когда все их атрибуты равны. Примером объекта Value может быть Money.

Подробнее о сущностях

В сообществе Rails мы хорошо понимаем, что такое сущности. Практически почти каждый объект, расширяющий ActiveRecord :: Base, является сущностью.

Объекты имеют следующие характеристики:

  • Сущности заботятся о своей идентичности. Идентичность обычно представлена ​​автоматически сгенерированным первичным ключом, который используется для сравнения двух сущностей.
  • Они изменчивы. Единственное поле, которое нельзя изменить, — это первичный ключ.
  • У них долгая жизнь. Большинство сущностей никогда не удаляются из базы данных.
  • Поскольку сущности изменчивы и долговечны, они обычно имеют сложный жизненный цикл :
    * Объект Entity создан.
    * Сохраняется в базе данных.
    * Это читается из базы данных.
    * Обновляется.
    * Он удален (или помечен как удаленный).

Из-за изменчивости и сложного жизненного цикла, работа с сущностями сложна. Поэтому каждый раз, когда вы определяете сущность, продумывайте, как вы собираетесь ее сохранять, какие атрибуты вы должны сделать изменяемыми, какие агрегаты (подробнее об агрегатах в следующем посте) должны содержать их и т. Д.

Подробнее о ценностях

Объекты Value, с другой стороны, недостаточно используются в сообществе Rails. В результате большинство Rails-приложений страдают от Primitive Obsession:

  • Примитивные значения, такие как целые числа и строки, используются для представления важных концепций домена.

Во-первых, поскольку логика работы с группой атрибутов распределена по десяткам классов, Primitive Obsession обычно является источником дублирования кода. Во-вторых, использование примитивов вместо доменных абстракций мешает высокоуровневым сервисам ненужными деталями и делает цель вашего кода неясной. Ценные объекты предлагают хорошее средство от Примитивной Одержимости

Объекты значения имеют следующие характеристики:

  • Объекты значения не имеют идентичности.
  • Они неизменны. Например, добавление 3 к 5 не меняет ни одно из этих значений. Вместо этого возвращается новое значение. В идеале работа с объектами-значениями должна выглядеть как работа с примитивами.
  • Объекты значения не имеют сложного жизненного цикла.

Создание объектов значения в Rails

Существует много способов создания и управления объектами-значениями в Rails, и я хотел бы показать три из них.

Используйте composed_of

Представьте, мы пишем еще одно приложение для блога. Мы решили в пользу этой модели:

  • В блоге много постов.
  • Каждый пост имеет много комментариев.
  • Сообщения и комментарии имеют атрибуты местоположения, связанные с ними.

Мы можем создавать сообщения и комментарии следующим образом:

blog = Blog.create
post = blog.make_post text: ‘great post’, location_country: ‘Canada’, location_city: ‘Toronto’
post.make_comment text: ‘great comment’, location_country: ‘Canada’, location_city: ‘Toronto’

Мы также можем искать их по месту нахождения:

class Blog < ActiveRecord::Base
def all_posts_from country, city
end
def all_comments_from country, city
end
end

view raw
blog.rb
hosted with ❤ by GitHub

В дополнение к этому у нас есть докладчик для отображения атрибутов местоположения:

class LocationPresenter
def initialize country, city
end
end

Как видите, мы всегда используем атрибуты countrycity Даже когда я объяснял поведение приложения, я писал: «по месту нахождения». Помимо дублирования мы пропустили важную часть нашего домена. Существует понятие местоположения, которое наша модель (наш код) не отражает. Давайте это исправим.

Давайте начнем с определения класса, который будет инкапсулировать атрибуты местоположения:

class Location < Struct.new(:country, :city)
end

view raw
location.rb
hosted with ❤ by GitHub

Теперь нам нужно настроить Post для переноса location_countrylocation_city

class Post < ActiveRecord::Base
composed_of :location, mapping: [%w(location_country country),
%w(location_city city)]
def self.all_posts_from location
Post.where location: location
end
end

Это результат нашего рефакторинга:

blog.make_post text: ‘great post 2’, location: Location.new(‘Canada’, ‘Toronto’)

view raw
making_post.rb
hosted with ❤ by GitHub

Самым большим преимуществом после этого рефакторинга стало то, что важная концепция нашего домена явно указана в исходном коде. Кроме того, извлечение объекта Value помогло нам поднять уровень абстракции, что приводит к более читаемому коду:

def Toronto
Location.new(‘Canada’, ‘Toronto’)
end
blog.make_post text: ‘great post’, location: Toronto

Объекты значения, расширяющие ActiveRecord :: Base

Некоторые люди говорят, что все, что расширяет ActiveRecord :: Base, является сущностью. Я не согласен с этим мнением. На мой взгляд, на самом деле не имеет значения, как вы реализуете свои объекты-ценности, если они не имеют ни состояния, ни идентичности.

Давайте определим класс Location:

class Location < ActiveRecord::Base
validates :city, :uniqueness => {:scope => :country}
def self.get country, city
location = Location.find_by_country_and_city(country, city)
raise «There is no ‘#{city}‘ in ‘#{country}‘» unless location
location.readonly!
location
end
end

view raw
location_ar.rb
hosted with ❤ by GitHub

Использование Location остается почти таким же:

toronto = Location.get(‘Canada’, ‘Toronto’)
blog.make_post text: ‘great post 2’, location: toronto

Такие требования, как динамический контроль списка всех возможных местоположений или добавление некоторой дополнительной информации к каждому объекту (например, ссылка на статью в википедии), могут подтолкнуть ваше решение в пользу этого подхода.

Обычные Старые Рубиновые Объекты

Те разработчики, которые изучили Ruby через Rails, обычно решают все свои проблемы, используя строительные блоки Rails. Нужно что-то упорствовать? Это только ActiveRecord. Нужен объект стоимости? Используй составе Все, что не использует Rails, кажется им грязным. Например, все модели, не расширяющие ActiveRecord :: Base, попадают в папку lib. Даже при том, что это может работать для небольших приложений, построение сложной модели потребует использования Фабрики, Сервисов, Объектов Значения и т. Д. Поэтому не стоит бояться вообще реализовывать Объект Значения без Rails.

Резюме

Подводя итог, сущности и объекты значения чрезвычайно важны. Они являются основными элементами объектных моделей. Таким образом, разработчики программного обеспечения должны иметь четкое понимание различий между ними.