value object что это
DTO vs POCO vs Value Object
Определения DTO, POCO и Value Object
Вначале небольшая ремарка по поводу Value Object. В C# существует похожая концепция, называемая Value Type. Это всего лишь деталь имплементации того, как объекты хранятся в памяти и мы не будем касаться этого. Value Object, о котором пойдет речь, — понятие из среды DDD (Domain-Driven Design).
Ок, давайте начнем. Вы возможно заметили, что такие понятия как DTO, Value Object и POCO часто используются как синонимы. Но действительно ли они означают одно и то же?
DTO — это класс, содержащий данные без какой-либо логики для работы с ними. DTO обычно используются для передачи данных между различными приложениями, либо между слоями внутри одного приложения. Их можно рассматривать как хранилище информации, единственная цель которого — передать эту информацию получателю.
С другой стороны, Value Object — это полноценный член вашей доменной модели. Он подчиняется тем же правилам, что и сущности (Entities). Единственное отличие между Value Object и Entity в том, что у Value Object-а нет собственной идентичности. Это означает, что два Value Object-а с одинаковыми свойствами могут считаться идентичными, в то время как две сущности отличаются друг от друга даже в случае если их свойства полностью совпадают.
Value Object-ы могут содержать логику и обычно они не используются для передачи информации между приложениями.
POJO был представлен Мартином Фаулером в качестве альтернативы для JavaBeans и других «тяжелых» enterprise-конструкций, которые были популярны в ранних 2000-х.
Основной целью POJO было показать, что домен приложения может быть успешно смоделирован без использования JavaBeans. Более того, JavaBeans вообще не должны быть использованы для этой цели.
Другой хороший пример анти-POCO подхода — Entity Framework до версии 4.0. Каждый класс, сгенерированный EF, наследовал от EntityObject, что привносило в домен логику, специфичную для EF. Начиная с версии 4, Entity Framework добавил возможность работать с POCO моделью — возможность использовать классы, которые не наследуются от EntityObject.
Таким образом, понятие POCO означает использование настолько простых классов насколько возможно для моделирования предметной области. Это понятие помогает придерживаться принципов YAGNI, KISS и остальных best practices. POCO классы могут содержать логику.
Корреляция между понятиями
Есть ли связи между этими тремя понятиями? В первую очередь, DTO и Value Object отражают разные концепции и не могут использоваться взаимозаменяемо. С другой стороны, POCO — это надмножество для DTO и Value Object:
Другими словами, Value Object и DTO не наследуют никаким сторонним компонентам и таким образом являются POCO. В то же время, POCO — это более широкое понятие: это может быть Value Object, Entity, DTO или любой другой класс в том случае если он не наследует компонентам, не относящимся напрямую к решаемой вами проблеме.
Вот свойства каждого из них:
Заметьте, что POCO-класс может и иметь, и не иметь собственной идентичности, т.к. он может быть как Value Object, так и Entity. Также, POCO может содержать, а может и не содержать логику внутри себя. Это зависит от того, является ли POCO DTO.
Заключение
Вышесказанное в статье можно суммировать следующим образом:
PHP Profi
Квест → Как хакнуть форму
DDD в PHP: Value Object или Объект-Значение Перевод
Определение Мартина Фаулера:
Небольшой простой объект, как деньги или диапазон дат, равенство которых не основано на идентичности
Объект-Значение (Value Object) — это объект, который представляет собой понятие из предметной области. В DDD (Domain Driven Development — разработка на основе предметной области, или предметно-ориентированное программирование) важно то, что Value Object поддерживает и обогащает Единый Язык вашей Предметной Области. Это не только примитивы, которые представляют собой некоторые значения, — они являются полноправными гражданами Предметной Области, которые формируют поведение вашего приложения.
Преимущества использования Value Objects
Самое главное заключается в том, что эти объекты отражают язык, на котором вы разговариваете с другими разработчиками — когда вы говорите «Место»(Location) все знают, что это значит. Второе преимущество заключается в том, что Value Object может валидировать значение — подходит оно или нет для того, чтобы создать такой объект.
Третьим преимуществом является то, что вы можете полагаться на тип — вы знаете, что если такой Value Object был принят в качестве аргумента, он будет всегда в допустимом состоянии и вам не нужно беспокоиться об этом. И также Value Object может содержать некоторые специализированные методы, которые имеют смысл только в контексте этого значения и могут быть расположены в этом объекте (не нужно создавать странные классы-утилиты).
Пример Value Object
В качестве примера Value Object-а, который является распространённым для всех веб-приложений, я создал EmailAddress:
Одержимость примитивами
Вы можете неохотно относиться к использованию объектов в качестве контейнеров для примитивных значений, но подобные вещи описаны как «код с запашком» под названием «Одержимость примитивами»:
Одержимость примитивами — это использование примитивных типов данных для представления сущностей Предметной области. Например, мы используем String для представления сообщения, Integer в качестве суммы денег, или Struct/Dictionary/Hash для представления конкретного объекта.
Использование Value Object-ов является одной из стратегий борьбы с этим запахом. Ключевая идея здесь — это собрать поведение данных вокруг своего объекта. В противном случае такие действия будут разбросаны по всему коду, что может привести к ненужной сложности и заставит вас относиться с опаской к значениям, переданным в методы.
Неизменяемость
Очень важно помнить и понимать, что Value Object является неизменяемым. Почему это такое важное понятие? Задумайтесь о реальности вокруг вас и какие значения вы используете — число один, красный цвет и так далее. Вы не можете изменить эти значения — это не имеет смысла, менять красный цвет на зеленый, и продолжать называть его красным — это больше не красный, и называние зеленого красным будет путать людей. Таким же образом Value Object в вашем приложении должны быть неизменными.
Резюме
Value Object-ы являются важными гражданами вашей Предметной Области, которые отражают его концепции. Убедитесь, что вы приложите соответствующее поведение к таким объектам, которое впоследствии будет делать код более организованным и лучше передавать реальность, потому что ключевым аспектом и целью объектно-ориентированного программирования является моделирование реального мира.
Entity vs Value Object: полный список отличий
Типы эквивалентности
Чтобы обозначить разницу между entities и value objects, нам необходимо определить три типа эквивалентности (equality), которые вступают в силу как только мы пытаемся сравнить два объекта друг с другом.
Reference equality (ссылочная эквивалентность) означает, что два объекта равны в случае если они ссылаются на один и тот же объект в куче:
Вот как мы можем проверить ссылочную эквивалентность в C#:
Identifier equality (эквивалентность идентификаторов) подразумевает, что у класса присутствует Id поле. Два объекта такого класса будут равны если они имеют одинаковый идентификатор:
И, наконец, струкрурная эквивалентность означает полную эквивалентность всех полей двух объектов:
Основное отличие между сущностями и объектами-значения лежит в том, как мы сравниваем их экземпляры друг с другом. Концепция эквивалентности идентификаторов относится к сущностям, в то время как структурая эквивалентность — к объектам-значениям. Другими словами, сущности обладают неотъемлемой идентичностью, в то время как объекты-значения — нет.
На практике это означает, что объекты-значения не имеют поля-идентификатора и если два экземпляра одного объекта-значения обладают одинаковым набором атрибутов, мы можем считать их взаимозаменяемыми. В то же время, даже если данные в двух сущностях полностью одинаковы (за исключением Id поля), они не являются одной и той же сущностью.
Вы можете думать об этом в том же ключе, в котором вы думаете о двух людях, носящих одинаковое имя. Мы не считаем их одним и тем же человеком из-за этого. Они оба обладают внутренней (неотъемлимой) идентичностью. В то же время, если у нас есть 1 рубль, нам все равно та же ли это монета, что была у нас вчера. То тех пор пока эта монета является монетой ценностью в 1 рубль, мы не против заменить ее другой, точно такой же. Концепция денег в таком случае является объектом-значением.
Жизненный цикл
Еще одно отличие между двумя понятиями состоит в жизненном цикле их экземпляров. Сущности живут в континууме. Они обладают историей (даже если мы не храним эту историю) того, что с ними случилось и как они менялись в течение жизни.
Объекты-значения, с другой стороны, обладают нулевым жизненным циклом. Мы создаем и уничтожаем их с легкостью. Это следствие, логично вытекающее из того, что они взаимозаменяемы. Если рублевая монета — точно такая же, что и другая рублевая монета, то какая разница? Мы можем просто заменить имеющийся объект другим экземпляром и забыть о нем после этого.
Гайдлан, который следует из этого отличия, заключается в том, что объекты-значения не могут существовать сами по себе, они всегда должны принадлежать одной или нескольким сущностям. Данные, которые представляет из себя объект-значение, имеют значение только в контексте какой-либо сущности. В примере с монетами, приведенном выше, вопрос «Сколько денег?» не имеет смысла, т.к. он не несет в себе достаточного контекста. С другой стороны, вопрос «Сколько денег у Пети?» или «Сколько денег у всех юзеров нашей системы?» полностью валидны.
Другое следствие здесь в том, что мы не храним объекты-значения отдельно. Вместо этого, мы должны инлайнить (присоединять) их к сущностям при сохранении в БД (об этом ниже).
Неизменяемость
Следующее отличие — неизменямость. Объекты-значения должны быть неизменяемы в том смысле, что если нам необходимо изменить такой объект, мы создаем новый экземпляр на основе имеющегося вместо того чтобы изменять существующий. В противовес этому, сущности почти всегда изменяемы.
Обязательная неизменяемость объектов-значений принимается не всеми программистами. Некоторые считают, что этот гайдлайн не такой строгий, как предыдущие и объекты-значения могут быть изменяемыми в некоторых случаях. Я тоже придерживался этого мнения некоторое время назад.
В настоящее время я считаю, что связь между неизменяемостью и возможностью заменить один объект-значение на другой лежит глубже чем я думал. Изменяя экземпляр объекта-значения, мы подразумеваем, что он имеет неравный нулю жизненный цикл. А это предположение, в свою очередь, ведет к заключению о том, что объекты-значения имеют внутреннюю идентичность, что противоречит определию этого понятия.
Это несложное мысленное упражнение делает неизменяемость неотъемлемой частью объектов-значений. Если мы принимаем, что они имеют нулевой жизненный цикл, в том смысле что они являются всего лишь слепком какого-либо состояния и ничем более, то мы должны также принять, что они могут представлять только один вариант этого состояния.
Это приводит нас к следующиему правилу: если вы не можете сделать объект-значение неизменяемым, значит этот класс не является объектом-значением.
Как распознать объект-значение в доменной модели?
Не всегда ясно является ли концепция в доменной модели сущностью или объектом-значением. И к сожалению, не существует объективных атрибутов, по которым мы могли мы судить об этом. Является или нет класс объектом-значением полностью зависит от доменной области, в которой мы работаем: один и тот же предмет можно смоделировать в виде сущности в одном домене и в виде объекта-значения в другом.
В примере выше мы рассматриваем деньги как нечно взаимозаменяемое. Таким образом, это понятие является объектом-значением. В то же самое время, если мы создаем систему для отслеживания всех купюр в стране, нам необходимо рассмативать каждую банкноту отдельно для сбора статистики по ней. В этом случае понятие денег будет являться сущностью.
Не смотря на отсутствие объективных показателей, мы все же можем использовать некоторые приемы для того, чтобы отнести концепт к сущностям или объектам-значениям. Мы уже обсуждали три вида эквивалентности: если мы можем заменить один экземпляр класса другим с теми же свойствами, то это хороший знак того, что перед нами объект-значение.
Более простая версия того же приема заключается в том, чтобы мысленно сравнить класс с целочисленным значением (integer). Вам как разработчику безразлично является ли цифра 5 той же цифрой, которую вы использовали в предыдущем методе. Все пятерки в вашем приложении одинаковы, не зависимо от того, как они были созданы. Это делает тип integer по сути объектом-значением. Теперь, задайте себе вопрос: выглядит ли этот класс как integer? Если ответ да, то это объект-значение.
Как хранить объекты-значения в базе данных?
Предположим, что мы имеем два класса в доменной модели: сущность Person и объект-значение Address:
Как будет выглядить структура БД в этом случае? Решение, которое приходит в голову в такой ситуации — создать отдельные таблицы для обоих классов:
Такой дизайн, не смотря на полную валидность с точки зрения БД, имеет два недостатка. Во-первых, таблица Address содержит идентификатор. Это означает, что нам будет необходимо ввести отдельное поле Id в класс Address чтобы работать с такой таблицей корректно. Это, в свою очередь, означает, что мы добавляем классу некоторую идентичность. А это уже нарушает определение объекта-значения.
Второй недостаток здесь в том, что мы потенциально можем отделить объект-значение от родителькой сущности. Address может жить собственной жизнью, т.к. мы можем удалить Person из БД без удаления соответствующей строки Address. Это будет нарушением другого правила, говорящего о том, что время жизни объектов-значений должно полностью зависеть от времени жизни их родительских сущностей.
Наилучшим решением в данном случае будет «заинлайнить» поля из таблицы Address в таблицу Person:
Это решит обе проблемы: Address не будет иметь собственного идентификатора и его время жизни будет полностью зависеть от времени жизни сущности Person.
Этот дизайн также имеет смысл если вы мысленно замените все поля, относящиеся к Address, единственным integer, как я предложил ранее. Создаете ли вы отдельную таблицу для каждого целочисленного значения в вашей доменной модели? Конечно нет, вы просто включаете его в родительскую таблицу. Те же правила применимы к объектам-значениям. Не создавайте отдельную таблицу для объектов-значений, просто включите их поля в таблицу сущности, к которой они принадлежат.
Предпочитайте объекты-значения сущностям
В вопросе объектов-значений и сущностей важное значение имеет следующее правило: всегда предпочитайте объекты-значения сущностям. Объекты-значения неизменяемы и из-за этого с ними крайне просто работать. В идеале, вы всегда должны стремиться включить большинство бизнес-логики в объекты-значения. Сущности в таких ситуациях будут служить обертками над ними и представлять более высокоуровневую функциональность.
Также, может случиться так, что концепт, который вы изначально видели как сущность, на самом деле является объектом-значением. К примеру, вы могли изначально представить класс Address в вашем коде как сущность. Он может иметь собственный Id и отдельную таблицу в БД. После некоторого размышления вы замечаете, что в вашей предметной области адреса на самом деле не имеют собственной идентичности и могут использоваться взаимозаменяемо. В этом случае, не стесняйтесь рефакторить вашу доменную модель, конвертируйте сущность в объект-значение.
Domain Driven Design: Value Objects и Entity Framework Core на практике
Под катом много кода.
Немного теории
Ядром архитектуры Domain Driven Design является Домен — предметная область, к которой применяется разрабатываемое программное обеспечение. Здесь находится вся бизнес-логика приложения, которая обычно взаимодействует с различными данными. Данные могут быть двух типов:
Entity может содержать другие Entity и VO. В состав VO могут быть включены другие VO, но не Entity.
Таким образом, логика домена должна работать исключительно с Entity и VO — этим гарантируется его консистентность. Базовые типы данных, такие как string, int и т.д. зачастую не могут выступать в качестве VO, потому что могут элементарно нарушить состояние домена — что в рамках DDD является почти катастрофой.
Пример. Набивший всем оскомину в различных руководствах класс Person часто показывают вот так:
Просто и наглядно — идентификатор, имя и возраст, где же тут можно ошибиться?
А ошибок тут может быть несколько — например, с точки зрения бизнес-логики, имя обязательно, не может быть нулевой длины или более 100 символов и не должно содержать спецсимволы, пунктуацию и т.д. А возраст не может быть меньше 10 или больше 120 лет.
С точки зрения языка программирования, 5 — вполне нормальное целое число, аналогично и пустая строка. А вот домен уже находится в некорректном состоянии.
Переходим к практике
К этому моменту мы знаем, что VO должен быть иммутабельным и содержать значение, допустимое для бизнес-логики.
Иммутабельность достигается инициализацией readonly свойства при создании объекта.
Проверка допустимости значения происходит в конструкторе (Guard clause). Саму проверку желательно сделать доступной публично — для того, чтобы, другие слои могли провалидировать данные поступившие от клиента (тот же браузер).
Давайте создадим VO для Name и Age. Дополнительно немного усложним задачу — добавим PersonalName, объединяющий в себе FirstName и LastName, и применим это к Person.
Таким образом, мы не можем создать Person без полного имени или возраста. Также мы не можем создать “неправильное” имя или “неправильный” возраст. А хороший программист обязательно проверит в контроллере поступившие данные с помощью методов Name.IsValid(“John”) и Age.IsValid(35) и в случае некорректных данных — сообщит об этом клиенту.
Если мы возьмем за правило везде в модели использовать только Entity и VO, то убережем себя от большого количества ошибок — неправильные данные просто не попадут в модель.
Persistence
Теперь нам нужно сохранить наши данные в хранилище данных и получить их по запросу. В качестве ORM будем использовать Entity Framework Core, хранилище данных — MS SQL Server.
DDD четко определяет: Persistence — это подвид инфраструктурного слоя, поскольку скрывает в себе конкретную реализацию доступа к данным.
Домен ничего не должен знать про Persistence, по этому определяет только интерфейсы репозиториев.
А Persistence содержит в себе конкретные реализации, конфигурации маппинга, а также объект UnitOfWork.
Существует два мнения, стоит ли создавать репозитории и Unit of Work.
Но домен в DDD не зависит от хранения данных и используемого ORM — это всё тонкости имплементации, которые инкапсулированы в Persistence и никого больше не интересуют. Если мы предоставляем DbContext в другие слои, то тут же раскрываем детали имплементации, намертво завязываемся на выбранную ORM и получаем DAL — как основу всей бизнес-логики, а такого быть не должно. Грубо говоря, домен не должен заметить изменение ORM и даже потерю Persistence как слоя.
Итак, интерфейс репозитория Persons, в домене:
и его реализация в Persistence:
Казалось бы, ничего сложного, но есть проблема. Entity Framework Core “из коробки” работает только с базовыми типами (string, int, DateTime и т.д.) и ничего не знает про PersonalName и Age. Давайте научим EF Core понимать наши Value Objects.
Configuration
Для конфигурирования Entity в DDD больше всего подходит Fluent API. Атрибуты не подходят, так как домен не должен ничего знать про нюансы маппинга.
Создадим в Persistence класс с базовой конфигурацией PersonConfiguration:
и подключим его в DbContext:
Mapping
Тот раздел, ради которого и написан этот материал.
В данный момент есть два более-менее удобных способа маппинга нестандартных классов к базовым типам — Value Conversions и Owned Types.
Value Conversions
Эта фича появилась в Entity Framework Core 2.1 и позволяет определять конвертацию между двумя типами данных.
Напишем конвертер для Age (в этом разделе весь код — в PersonConfiguration):
Простой и лаконичный синтаксис, но не обошлось без недостатков:
Здесь есть условие по возрасту, но EF Core не сможет его преобразовать в SQL запрос и, дойдя до Where(), загрузит всю таблицу в память приложения и, только потом, с помощью LINQ, выполнит условие p.Age.Value > age.Value.
В общем, Value Conversions — простой и быстрый вариант маппинга, но нужно помнить о такой особенности работы EF Core, иначе, в какой то момент, при запросе в большие таблицы, память может закончиться.
Owned Types
Owned Types появились в Entity Framework Core 2.0 и пришли на замену Complex Types из обычного Entity Framework.
Давайте сделаем Age как Owned Type:
Неплохо. А еще Owned Types не имеют некоторых недостатков Value Conversions, а именно пунктов 2 и 3.
2. Возможно конвертировать одно свойство в несколько колонок в таблице и наоборот
То, что нужно для PersonalName, хотя синтаксис уже немного перегружен:
3. EF Core умеет преобразовывать LINQ выражение с этим свойством в SQL запрос.
Добавим сортировку по LastName и FirstName при загрузке списка:
Такое выражение будет корректно преобразовано в SQL запрос и сортировка выполняется на стороне SQL сервера, а не в приложении.
Конечно, есть и недостатки.
С другой стороны — это такие детали реализации, которые можно опустить, но, опять же, забывать не стоит. Трекинг изменений влияет на производительность. Если с выборками единичных Entity (например, по Id) или небольших списков это не заметно, то с выборкой больших списков “тяжелых” Entity (много VO-свойств) — просадка в производительности будет весьма заметной именно из-за трекинга.
Presentation
Мы разобрались как реализовать Value Objects в домене и репозитории. Пришло время все это использовать. Создадим две простейшие странички — со списком Person и формой добавления Person.
Код контроллера без Action методов выглядит так:
Добавим Action для получения списка Person:
Ничего сложного — загрузили список, создали Data-Transfer Object (PersonModel) на каждый
Person и отправили в соответствующую View.
Гораздо интереснее добавление Person:
Здесь присутствует обязательная валидация входящих данных:
Если этого не делать, то при создании VO с некорректным значением будет выкинуто ArgumentException (помним про Guard Clause в конструкторах VO). С проверкой же гораздо легче отправить пользователю сообщение, что какое то из значений неверное.
Здесь нужно сделать небольшое отступление — в Asp Net Core есть штатный способ валидации данных — с помощью атрибутов. Но в DDD такой способ валидации не является корректным по нескольким причинам:
Заключение
Я привел примеры реализации Value Objects в общем и нюансы маппинга в Entity Framework Core. Надеюсь, что материал пригодится в понимании того, как применять элементы Domain Driven Design на практике.
Полный исходный код проекта PersonsDemo — GitHub
В материале не раскрыта проблема взаимодействия с опциональными (nullable) Value Objects — если бы PersonalName или Age были не обязательными свойствами Person. Я хотел это описать в данной статье, но она и так вышла несколько перегруженной. Если есть интерес к этой проблематике — пишите в комментариях, продолжение будет.
Фанатам “красивых архитектур” в общем и Domain Driven Design в частности очень рекомендую ресурс Enterprise Craftsmanship.
Также использовалась официальная документация по Owned Types и Value Conversions.