В моей предыдущей статье о 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 |
В дополнение к этому у нас есть докладчик для отображения атрибутов местоположения:
class LocationPresenter | |
def initialize country, city | |
… | |
end | |
end |
Как видите, мы всегда используем атрибуты country
city
Даже когда я объяснял поведение приложения, я писал: «по месту нахождения». Помимо дублирования мы пропустили важную часть нашего домена. Существует понятие местоположения, которое наша модель (наш код) не отражает. Давайте это исправим.
Давайте начнем с определения класса, который будет инкапсулировать атрибуты местоположения:
class Location < Struct.new(:country, :city) | |
end |
Теперь нам нужно настроить Post для переноса location_country
location_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’) |
Самым большим преимуществом после этого рефакторинга стало то, что важная концепция нашего домена явно указана в исходном коде. Кроме того, извлечение объекта 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 |
Использование Location остается почти таким же:
toronto = Location.get(‘Canada’, ‘Toronto’) | |
blog.make_post text: ‘great post 2’, location: toronto |
Такие требования, как динамический контроль списка всех возможных местоположений или добавление некоторой дополнительной информации к каждому объекту (например, ссылка на статью в википедии), могут подтолкнуть ваше решение в пользу этого подхода.
Обычные Старые Рубиновые Объекты
Те разработчики, которые изучили Ruby через Rails, обычно решают все свои проблемы, используя строительные блоки Rails. Нужно что-то упорствовать? Это только ActiveRecord. Нужен объект стоимости? Используй составе Все, что не использует Rails, кажется им грязным. Например, все модели, не расширяющие ActiveRecord :: Base, попадают в папку lib. Даже при том, что это может работать для небольших приложений, построение сложной модели потребует использования Фабрики, Сервисов, Объектов Значения и т. Д. Поэтому не стоит бояться вообще реализовывать Объект Значения без Rails.
Резюме
Подводя итог, сущности и объекты значения чрезвычайно важны. Они являются основными элементами объектных моделей. Таким образом, разработчики программного обеспечения должны иметь четкое понимание различий между ними.