DDD: Идентичност на обекта преди да бъде запазена

При дизайна, управляван от домейн, една от определящите характеристики на обекта е, че той има идентичност.

Проблем:

Не мога да предоставя уникална самоличност на обекти при създаване на екземпляр. Тази самоличност се предоставя от хранилището само след като обектът се запази (тази стойност се предоставя от основната база данни).

Не мога да започна да използвам Guid стойности на този етап. Съществуващите данни се съхраняват с int стойности на първичен ключ и не мога да генерирам уникален int при инстанциране.

Моето решение:

  • Всяка единица има стойност на идентичност
  • Идентичността се настройва на истинска самоличност само след като се запази (осигурена от базата данни)
  • Самоличността е зададена по подразбиране, когато се инстанцира преди постоянството
  • Ако самоличността е по подразбиране, обектите са сравними чрез препратка
  • Ако самоличността не е по подразбиране, обектите са сравними чрез стойности на идентичност

Код (абстрактният базов клас за всички обекти):

public abstract class Entity<IdType>
{
    private readonly IdType uniqueId;

    public IdType Id
    {
        get 
        { 
            return uniqueId; 
        }
    }

    public Entity()
    {
        uniqueId = default(IdType);
    }

    public Entity(IdType id)
    {
        if (object.Equals(id, default(IdType)))
        {
            throw new ArgumentException("The Id of a Domain Model cannot be the default value");
        }

        uniqueId = id;
    }

    public override bool Equals(object obj)
    {
        if (uniqueId.Equals(default(IdType)))
        { 
            var entity = obj as Entity<IdType>;

            if (entity != null)
            {
                return uniqueId.Equals(entity.Id);
            }
        }

        return base.Equals(obj);
    }

    public override int GetHashCode()
    {
        return uniqueId.GetHashCode();
    }
}

Въпрос:

  • Смятате ли, че това е добра алтернатива на генерирането на Guid стойности при създаване на екземпляр?
  • Има ли по-добри решения за този проблем?

person Dave New    schedule 21.01.2014    source източник
comment
Каква база данни използвате? Ако използвате RavenDB или NHibernate, може да сте в състояние да се възползвате от модела HiLo, който ви позволява да получите id предварително, преди да запазите обекта в базата данни.   -  person Adrian Thompson Phillips    schedule 21.01.2014
comment
Entity Framework 6 върху Azure SQL база данни. Не вярвам, че е възможно да се получи идентификатор, преди да се вмъкне нещо.   -  person Dave New    schedule 21.01.2014
comment
Защо се нуждаете от идентификатор, преди обектът да бъде запазен и защо трябва да разчитате на предоставени от DB стойности? Моля, дайте ни повече подробности за вашия домейн, защото е възможно вашите предположения да са погрешни.   -  person Bartłomiej Szypelow    schedule 21.01.2014
comment
@BartłomiejSzypelow: Тъй като обектите трябва да имат идентичност, понякога дори от гледна точка на инстанциране. Бих могъл просто да разчитам на препратката, но работя върху разпределена система, така че самоличността на обекта трябва да бъде квалифицирана чрез идентификатор (тъй като ще се извърши сериализация). Опитвам се да постигна ранно генериране на самоличност (вижте книгата на Vaughn Vernon: Implementing Domain-Driven Design).   -  person Dave New    schedule 22.01.2014
comment
Споменахте разпределени. Още една причина за GUID. Както споменахте IDDD, има и глава за хакване с използване на таблица, имитираща SEQUENCE на Oracle за ранно генериране на самоличност. Не ми харесва този подход, но какво ще кажете за вас?   -  person Bartłomiej Szypelow    schedule 22.01.2014
comment
Много съм любопитен: Как успяхте да оставите вашето репо да зададе частното поле uniqueId след вмъкване? И как вашето репо има достъп до това поле при актуализиране, за да знае кой ред да актуализира?   -  person Timo    schedule 28.06.2018


Отговори (5)


Не мога да предоставя уникална самоличност на обекти при създаване на екземпляр. Тази самоличност се предоставя от хранилището само след като обектът се запази (тази стойност се предоставя от основната база данни).

Колко места имате, където създавате списък с обекти от един и същи тип и имате повече от един обект с идентификатор по подразбиране?

Смятате ли, че това е добра алтернатива на генерирането на Guid стойности при създаване на екземпляр?

Ако не използвате ORM, вашият подход е достатъчно добър. Особено при внедряване на идентификационна карта и работна единица е ваша отговорност. Но сте поправили само Equals(object obj). GetHashCode() методът не проверява дали uniqueId.Equals(default(IdType)).

Предлагам ви да разгледате всеки „Инфраструктурен шаблон“ с отворен код като Sharp-Architecture и проверете тяхната имплементация на базовия клас за всички обекти на домейн.

Свикнал съм да пиша персонализирани реализации на Equals() за обекти на домейн, но може да е излишно, когато става въпрос за използване на ORM. Ако използвате който и да е ORM, той предоставя реализации на идентификационна карта и единица за работа моделите са готови и можете да разчитате на тях.

person Ilya Palkin    schedule 21.01.2014

Можете да използвате генератор на последователности, за да генерирате уникални int/long идентификатори, когато създавате обект на обект.

Интерфейсът изглежда така:

interface SequenceGenerator {
    long getNextSequence();
}

Типична реализация на генератор на последователности използва таблица с последователности в базата данни. Таблицата с последователности съдържа две колони: sequenceName и allocatedSequence.

Когато getNextSequence се извика за първи път, той записва голяма стойност (да кажем 100) в колоната allocatedSequence и връща 1. Следващото повикване ще върне 2 без да има нужда от достъп до базата данни. Когато последователностите 100 свършат, той чете и увеличава allocatedSequence с 100 отново.

Разгледайте SequenceHiLoGenerator в изходния код на Hibernate. По принцип прави това, което описах по-горе.

person nwang0    schedule 21.01.2014

Вярвам, че решението на това всъщност е доста лесно:

  • Както споменахте, субектите трябва да имат идентичност,

  • Съгласно вашите (напълно валидни) изисквания, самоличността на вашите обекти се присвоява централно от СУБД,

  • Следователно всеки обект, на който все още не е присвоена идентичност, не е обект.

Това, с което си имате работа тук, е тип обект за прехвърляне на данни, който няма идентичност. Трябва да мислите за това като за прехвърляне на данни от каквато и да е входна система, която използвате, към модела на домейна чрез хранилището (което ви е необходимо като интерфейс тук за присвояване на самоличност). Предлагам ви да създадете друг тип за тези обекти (който няма ключ) и да го предадете на метода Add/Create/Insert/New на вашето хранилище.

Когато данните не се нуждаят от много предварителна обработка (т.е. не е необходимо да се предават много), някои хора дори пропускат DTO и предават различните части от данни директно чрез аргументите на метода. Това наистина е начинът, по който трябва да гледате на такива DTO: като на удобни аргументни обекти. Отново забележете липсата на аргумент "ключ" или "идентификатор".

Ако трябва да манипулирате обекта като обект, преди да го вмъкнете в базата данни, тогава DBMS последователностите са единствената ви опция. Обърнете внимание, че това обикновено е сравнително рядко, единствената причина, поради която може да се наложи да направите това, е ако резултатите от тези манипулации в крайна сметка променят състоянието на обекта, така че ще трябва да направите втора заявка за актуализирането му в базата данни, което вие със сигурност бих предпочел да избягвам.

Много често функциите за „създаване“ и „модифициране“ в приложенията са достатъчно различни, така че винаги ще добавяте първо записите за обектите в базата данни, преди да ги извлечете отново по-късно, за да ги модифицирате.

Несъмнено ще се притеснявате от повторното използване на кода. В зависимост от начина, по който конструирате вашите обекти, вероятно ще искате да отделите някаква логика за валидиране, така че хранилището да може да валидира данните, преди да ги вмъкнете в базата данни. Обърнете внимание, че това обикновено не е необходимо, ако използвате последователности на СУБД, и може да е причина някои хора да ги използват систематично, дори ако нямат стриктна нужда от тях. В зависимост от вашите изисквания за производителност, вземете коментарите по-горе под внимание, тъй като последователността ще генерира допълнително двупосочно пътуване, което често ще можете да избегнете.

  • Пример: Създайте валидатор обект, който използвате както в обекта, така и в хранилището.

Отказ от отговорност: Нямам задълбочени познания за каноничния DDD, не знам дали това наистина е препоръчителният подход, но има смисъл за мен.

Също така ще добавя, че по мое мнение промяната на поведението на Equals (и други методи) въз основа на това дали обектът представлява обект или прост обект с данни просто не е идеален. С техниката, която използвате, вие също трябва да се уверите, че стойността по подразбиране, която използвате за ключа, е правилно изключена от стойностния домейн във цялата логика на домейна.

Ако все пак искате да използвате тази техника, предлагам да използвате специален тип за ключ. Този тип ще постави в кутия/увие ключа с допълнително състояние, показващо дали ключът съществува или не. Имайте предвид, че тази дефиниция прилича на Nullable<T> толкова много, че бих помислил да я използвам (можете да използвате синтаксиса type? в C#). С този дизайн е по-ясно, че позволявате на обекта да няма идентичност (нулев ключ). Също така трябва да е по-очевидно защо дизайнът не е идеален (отново според мен): Вие използвате един и същ тип, за да представите както обекти, така и обекти за прехвърляне на данни без самоличност.

person tne    schedule 18.08.2014

Не мога да започна да използвам Guid стойности на този етап.

Да, можете и това би било алтернатива. Ръководствата няма да бъдат първичните ключове на вашата база данни, а по-скоро ще се използват на ниво модел на домейн. При този подход можете дори да имате два отделни модела - модел на постоянство с int като първични ключове и guid като атрибути и друг модел, моделът на домейн, където guids играят ролята на идентификатори.

По този начин вашите обекти на домейн могат да получат своите идентичности, след като бъдат създадени, а постоянството е само едно от незначителните бизнес проблеми.

Другият вариант, който ми е известен, е този, който описахте.

person Wiktor Zychla    schedule 21.01.2014
comment
Но как стойността на Guid ще бъде последователна през физическите граници? Ако обектът бъде извлечен в две различни системи или по различно време, техните ръководства ще бъдат различни. Те трябва да са еднакви. Имам отделен постоянен (EF) модел под реализациите на хранилището. Благодаря за отговора! - person Dave New; 21.01.2014
comment
Преди самоличността да бъде запазена, тя не може да бъде извлечена от същия магазин. От друга страна, няма нищо лошо преходно образувание да пресече физическа граница, без дори да бъде поддържано. Някъде в даден момент някой може да го запази и водачът ще бъде неговата самоличност, същият пътеводител, който сте му дали при раждането. Обърнете внимание, че той дори може да бъде поддържан многократно и да получава различни int идентификатори в различни системи и по този начин guid все още действа като DDD идентичност, където int идентификаторите са само идентификатори на постоянство. - person Wiktor Zychla; 21.01.2014
comment
Искате да кажете, че същият обект, зареден днес, може да има различна идентичност, когато бъде зареден утре? - person Dave New; 21.01.2014
comment
Не, самоличността (guid) не се променя. Идентичността на базата данни се променя и тя е локална за системата, която запазва данните. Така или иначе няма да избягате от водачи. - person Wiktor Zychla; 21.01.2014

Предложеното от вас решение е напълно валидно според моя опит. Доста съм използвал този подход.

Имайте предвид, че споделянето на идентификационни номера с автоматично нарастване външно изтича информация за вашите томове. Това понякога може да изисква допълнително свойство GUID - не е нещо красиво.

Пренаписване в един ред за вашето внедряване

Обичам да прилагам спретнато Equals() и GetHashCode() на обект, както следва. (Включвам ToString(), тъй като винаги отменям и това за по-лесно отстраняване на грешки и регистриране.)

public override string ToString() => $"{{{this.GetType().Name} Id={this.Id}}}"; // E.g. {MyEntity Id=1} (extra brackets help when nesting)
public override bool Equals(object obj) => (this.Id == default) ? ReferenceEquals(this, obj) : this.Id == (obj as MyEntity)?.Id;
public override int GetHashCode() => this.Id.GetHashCode();

ReferenceEquals() срещу base.Equals() е интересна дискусия. :)

Алтернативно решение

Ако искате нещо още по-добро, ето още едно предложение. Какво ще стане, ако можете да имате стойност, която е (за нашите намерения и цели) толкова добра, колкото GUID, но се вписва в long? Ами ако можеше да се поднови и без необходимост от хранилище?

Осъзнавам, че в момента вашата маса може да пасне само на int като PRIMARY KEY. Но ако можете да промените това на long или за вашите бъдещи таблици, моето предложение може да ви заинтересува.

В Предложение: локално уникална алтернатива на GUID обяснявам как да изградя локално уникална, нова, строго възходяща 64-битова стойност. Той замества комбинацията ID + GUID с автоматично нарастване.

Винаги съм не харесвал идеята да имам както цифров идентификатор, така и GUID. Това е все едно да кажете: „Това е уникалният идентификатор на обекта. И... това е неговият друг уникален идентификатор.“ Разбира се, можете да запазите един извън домейна и езика, но това ви оставя с техническия проблем с едновременното управление и скриване на допълнителния цифров идентификатор. Ако предпочитате да имате един идентификатор, който е едновременно удобен за домейн (с възможност за нов без хранилище и с име ID вместо GUID) и удобен за база данни (малък, бърз и възходящ), опитайте моето предложение.

Предупреждавам ви, че внедряването може да бъде трудно да се направи правилно, особено по отношение на сблъсъците и безопасността на нишките. Все още не съм успял да публикувам код за него.

person Timo    schedule 29.10.2017