Entity vs Value Object: полный список отличий

    Тема отличий таких понятий как Entity (Сущность) и Value Object (Объект-Значение) из Domain-Driven Design не нова. Тем не менее, я не смог найти статью с полным списком их отличий, так что решил написать свою.

    Типы эквивалентности


    Чтобы обозначить разницу между entities и value objects, нам необходимо определить три типа эквивалентности (equality), которые вступают в силу как только мы пытаемся сравнить два объекта друг с другом.

    Reference equality (ссылочная эквивалентность) означает, что два объекта равны в случае если они ссылаются на один и тот же объект в куче:

    image

    Вот как мы можем проверить ссылочную эквивалентность в C#:

    object object1 = new object();
    object object2 = object1;
    bool areEqual = object.ReferenceEquals(object1, object2); // возвращает true
    

    Identifier equality (эквивалентность идентификаторов) подразумевает, что у класса присутствует Id поле. Два объекта такого класса будут равны если они имеют одинаковый идентификатор:

    image

    И, наконец, струкрурная эквивалентность означает полную эквивалентность всех полей двух объектов:

    image

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

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

    Вы можете думать об этом в том же ключе, в котором вы думаете о двух людях, носящих одинаковое имя. Мы не считаем их одним и тем же человеком из-за этого. Они оба обладают внутренней (неотъемлимой) идентичностью. В то же время, если у нас есть 1 рубль, нам все равно та же ли это монета, что была у нас вчера. То тех пор пока эта монета является монетой ценностью в 1 рубль, мы не против заменить ее другой, точно такой же. Концепция денег в таком случае является объектом-значением.

    Жизненный цикл


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

    Объекты-значения, с другой стороны, обладают нулевым жизненным циклом. Мы создаем и уничтожаем их с легкостью. Это следствие, логично вытекающее из того, что они взаимозаменяемы. Если рублевая монета — точно такая же, что и другая рублевая монета, то какая разница? Мы можем просто заменить имеющийся объект другим экземпляром и забыть о нем после этого.

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

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

    Неизменяемость


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

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

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

    Это несложное мысленное упражнение делает неизменяемость неотъемлемой частью объектов-значений. Если мы принимаем, что они имеют нулевой жизненный цикл, в том смысле что они являются всего лишь слепком какого-либо состояния и ничем более, то мы должны также принять, что они могут представлять только один вариант этого состояния.

    Это приводит нас к следующиему правилу: если вы не можете сделать объект-значение неизменяемым, значит этот класс не является объектом-значением.

    Как распознать объект-значение в доменной модели?


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

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

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

    Более простая версия того же приема заключается в том, чтобы мысленно сравнить класс с целочисленным значением (integer). Вам как разработчику безразлично является ли цифра 5 той же цифрой, которую вы использовали в предыдущем методе. Все пятерки в вашем приложении одинаковы, не зависимо от того, как они были созданы. Это делает тип integer по сути объектом-значением. Теперь, задайте себе вопрос: выглядит ли этот класс как integer? Если ответ да, то это объект-значение.

    Как хранить объекты-значения в базе данных?


    Предположим, что мы имеем два класса в доменной модели: сущность Person и объект-значение Address:

    // Entity
    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Address Address { get; set; }
    }
     
    // Value Object
    public class Address
    {
        public string City { get; set; }
        public string ZipCode { get; set; }
    }
    

    Как будет выглядить структура БД в этом случае? Решение, которое приходит в голову в такой ситуации — создать отдельные таблицы для обоих классов:

    image

    Такой дизайн, не смотря на полную валидность с точки зрения БД, имеет два недостатка. Во-первых, таблица Address содержит идентификатор. Это означает, что нам будет необходимо ввести отдельное поле Id в класс Address чтобы работать с такой таблицей корректно. Это, в свою очередь, означает, что мы добавляем классу некоторую идентичность. А это уже нарушает определение объекта-значения.

    Второй недостаток здесь в том, что мы потенциально можем отделить объект-значение от родителькой сущности. Address может жить собственной жизнью, т.к. мы можем удалить Person из БД без удаления соответствующей строки Address. Это будет нарушением другого правила, говорящего о том, что время жизни объектов-значений должно полностью зависеть от времени жизни их родительских сущностей.

    Наилучшим решением в данном случае будет «заинлайнить» поля из таблицы Address в таблицу Person:

    image

    Это решит обе проблемы: Address не будет иметь собственного идентификатора и его время жизни будет полностью зависеть от времени жизни сущности Person.

    Этот дизайн также имеет смысл если вы мысленно замените все поля, относящиеся к Address, единственным integer, как я предложил ранее. Создаете ли вы отдельную таблицу для каждого целочисленного значения в вашей доменной модели? Конечно нет, вы просто включаете его в родительскую таблицу. Те же правила применимы к объектам-значениям. Не создавайте отдельную таблицу для объектов-значений, просто включите их поля в таблицу сущности, к которой они принадлежат.

    Предпочитайте объекты-значения сущностям


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

    Также, может случиться так, что концепт, который вы изначально видели как сущность, на самом деле является объектом-значением. К примеру, вы могли изначально представить класс Address в вашем коде как сущность. Он может иметь собственный Id и отдельную таблицу в БД. После некоторого размышления вы замечаете, что в вашей предметной области адреса на самом деле не имеют собственной идентичности и могут использоваться взаимозаменяемо. В этом случае, не стесняйтесь рефакторить вашу доменную модель, конвертируйте сущность в объект-значение.

    Заключение


    • Сущности имеют свою собственную, внутренне присущую им идентичность. Объекты-значения — нет.
    • Понятие эквивалентности идентификаторов относится к сущностям; понятие структурной эквивалентности — к объектам-значениям; ссылочной эквивалентности — к обоим.
    • Сущности имеют историю; у объектов-значений нулевой жизненный цикл.
    • Объект-значение всегда должен принадлежать одной или нескольким сущностям, он не может жить собственной жизнью.
    • Объекты-значения должны быть неизменяемыми; сущности почти всегда изменяемы.
    • Чтобы распознать объект-значение, мысленно замените его на integer.
    • Объекты-значения не должны иметь собственной таблицы в БД.
    • Предпочитайте объекты-значения сущностям при моделивании домена.

    Английская версия статьи: Entity vs Value Object (DDD)
    Share post

    Similar posts

    Comments 20

      +6
      Второй недостаток здесь в том, что мы потенциально можем отделить объект-значение от родителькой сущности. Address может жить собственной жизнью, т.к. мы можем удалить Person из БД без удаления соответствующей строки Address

      Для этого придумали каскадное удаление в частности и правила удаления по внешнему ключу в целом.

      А что если в объекте-значении Address 100 полей? Заинлайнить все?
        +3
        В DDD Aggregation Root для этого есть. Но, автор, видимо, о нём не слышал…
        +7
        В вопросе объектов-значений и сущностей важное значение имеет следующее правило: всегда предпочитайте объекты-значения сущностям.
        Это противоречит самому духу DDD. DDD призывает строить дизайн приложения исходя из предметной области. Т.е. выбор Entity vs Value Object делается исходя из уникальности\неуникальности.
        У вас прям сборник вредных советов какой-то получился.
          +2
          С адресом плохой пример. Дом снесли — адрес исчез, ну или «стал неактивен». А например цифру 2 отменить нельзя. Кроме того, у человека или организации может быть несколько адресов.
            +2
            Имхо, проблема не в примере, а в правиле, которое этот пример иллюстрирует:
            Объекты-значения не должны иметь собственной таблицы в БД.
              +4
              В классической книге по DDD Эрика Эванса «Предметно-ориентированное программирование» приводится тот же самый пример про человека и адрес, а потом идёт раздел про оптимизацию, и там говорится, что объекты-значения легко подвергнуть денормализации и вставить внутрь таблицы объекта-сущности. Ни слова про то, что поступать так нужно всегда, там нет. Такой совет противоречит DDD. Имхо, вольный пересказ книги у автора статьи не удался.
                +3
                Мне кажется, считать адрес объектом-значением можно, если он нужен только для информации и выводится один раз где-нибудь в профиле пользователя, а реальность адреса не имеет значения. Но фактически это самостоятельная сущность, никак не связанная с пользователем, и она может изменяться. Например, переименование улицы. Местоположение на карте осталось то же самое, этажность и номер дома те же, люди и организации из этого дома никуда не переехали, а текстовое представление адреса изменилось. Это аналогично изменению фамилии у человека.
                  +4
                  Совершенно верно. В одних программах в рамках DDD адрес будет объектом-сущностью (когда нас интересует уникальность жилища; например, когда у каждого жилища есть какой-то рейтинг или история жильцов), а в других — объектом-значением.
                    0
                    ФИО — в подавляющем большинстве задач тоже объект-значение :)
              +4
              Инлайнить значения в общую таблицу или создавать отдельную — вопрос тактический, вопрос локальной архитектуры. Тут много нюансов. Но если в отдельную таблицу, то пример идеологически неправильный (хотя технически может быть обоснован). Не в таблице Person должно быть полe AddressId, а в таблице Address поле PersonId, а поля Id быть не должно.
                +2
                Заинлайнить хорошо когда связь один к одному, и когда не надо с этими полями работать, тоесть делать выборку всех людей с таким то зипкодом.
                  0
                  deleted
                    –1
                    На хабре всерьёз разбирают отличия Value Object и Entity — приплыли…
                      0
                      Ничего плохого не вижу в том, чтобы у объекта-значения была отдельная таблица в СУБД и, следовательно, идентификатор для доступа к записи. Семантически при этом с записью можно работать как с объектом-значением. Допустим, у персоны есть адреса. Чтобы присвоить второй персоне такой же адрес, как и у первой, мы просто создаём новую запись в таблице и копируем значимые поля. В доменной модели это будет работать именно так, тип «Адрес» будет иметь семантику объекта-значения, а наличие идентификатора служит лишь для доступа к записи и «наружу» за пределы доменной модели не виден. Что плохого-то? Зачем инлайнить? Глупости какие.
                        +1
                        Это у вас выходит уже не просто «Адрес», а отдельная сущность — «Адрес персоны», в которой можно выделить идентификатор (его нельзя прятать: он вам понадобится когда вы будете один из адресов удалять), связь с персоной и собственно адрес (объект-значение).
                          0

                          Аналогичный вопрос рассматривается в известной книге, а именно в пятой главе. Если коротко, то никаким образом наличие или отсутствие поля id не делает что-то сущностью или обьектом-значением. Речь там идет о том, чтобы не добавлять лишних идентификаторов и не усложнять работу с обьектами-значениями без необходимости.

                            0
                            Конкретно поле с именем id, конечно же, ни на что не влияет.

                            А вот наличие или отсутствие поля с семантикой главного идентификатора (=первичного ключа) как раз и разделяет сущности и объекты-значения.
                              0

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

                        0
                        Семантически неверно создавать две записи, отличающиеся только значением суррогатного идентификатора. Вы же не создаёте одинаковые записи для, например, датывремени? И не сравниваете датувремя по идентификатору? Не, бывают задачи когда нужно группировать записи по части значения датывремени типа "отчёт за год", но логически они ничем не отличаются от задач "отчёт по улице". Что для вас (ваших задач) значит "отредактировать адрес с id 12345678"? зачем этому адресу идентификатор?
                          –2
                          Смешали ограничения накладываем на разработку с отображением объектов на БД в одну кучу. Ужас!

                          Only users with full accounts can post comments. Log in, please.