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

10

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

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

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

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

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

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

Дизайн, который я тогда адаптировал, выглядит так:

class ProductRepository
{
    // throws Repository.RecordNotFoundException
    public Product GetBySKU(string sku);
}

class ProductCrudService
{
    private ProductRepository pr;

    public ProductCrudService(ProductRepository repository)
    {
        pr = repository;
    }

    public void SaveProduct(Domain.Product product)
    {
        try {
            pr.GetBySKU(product.SKU);

            throw Service.ProductWithSKUAlreadyExistsException("msg");
        } catch (Repository.RecordNotFoundException e) {
            // suppress/log exception
        }

        pr.MarkFresh(product);
        pr.ProcessChanges();
    }
}

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

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

Очевидное решение состоит в том, чтобы создать Domain.ProductCollectionкласс с Add(Domain.Product)методом броска ProductWithSKUAlreadyExistsException, но ему не хватает производительности, потому что вам нужно было бы получить все Продукты из хранилища данных, чтобы узнать в коде, имеет ли Продукт тот же самый SKU как продукт, который вы пытаетесь добавить.

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

Энди
источник
Зачем тянуть весь список имен, когда вы можете просто спросить его и основывать логику на том, был ли он найден? Вы говорите постоянному слою, что делать, а не как это делать, пока есть соглашение с интерфейсом.
JeffO
1
При использовании термина Сервис мы должны стремиться к большей ясности, такой как разница между доменными сервисами на уровне домена и сервисами приложений на уровне приложений. См. Gorodinski.com/blog/2012/04/14/…
Эрик Эйдт
Отличная статья, @ErikEidt, спасибо. Я полагаю, что мой дизайн не так уж плох, если я могу доверять г-ну Городинскому, он в значительной степени говорит то же самое: лучшее решение состоит в том, чтобы служба приложений извлекала информацию, требуемую объектом, эффективно настраивала среду выполнения и обеспечивала это к сущности, и имеет то же самое возражение против внедрения репозитория в модель предметной области непосредственно, как я, главным образом через нарушение SRP.
Энди
Цитата из ссылки "говори, не спрашивай": "Но лично я не использую" говори, не спрашивай "".
радаробоб

Ответы:

7

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

Я бы не согласился с этой частью. Особенно последнее предложение.

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

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

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

Euphoric
источник
Спасибо за вклад. Может ли репозиторий фактически представлять то, что Domain.ProductCollectionя имел в виду, учитывая, что они отвечают за извлечение объектов из слоя домена?
Энди
@DavidPacker Я не очень понимаю, что вы имеете в виду. Потому что не нужно хранить все элементы в памяти. Реализация метода DoesNameExist должна быть (наиболее вероятно) запросом SQL на стороне хранилища данных.
Эйфорическая
Что означает, вместо того, чтобы хранить данные в коллекции в памяти, когда я хочу знать весь Продукт, от которого мне не требуется получать их, Domain.ProductCollectionа вместо этого спрашиваю хранилище, то же самое, спрашивая, Domain.ProductCollectionсодержит ли Продукт Продукт с передал SKU, на этот раз снова, вместо этого запрашивая хранилище (это на самом деле пример), которое вместо итерации по предварительно загруженным продуктам запрашивает базовую базу данных. Я не имею в виду хранить весь продукт в памяти, если мне не нужно, это было бы полной чушью.
Энди
Что приводит к другому вопросу: должен ли репозиторий знать, должен ли атрибут быть уникальным? Я всегда реализовывал репозитории как довольно глупые компоненты, сохраняя то, что вы им передаете, чтобы сохранить, и пытаясь извлечь данные на основе пройденных условий и поместить решение в сервисы.
Энди
@DavidPacker Ну, «знание предмета» в интерфейсе. Интерфейс говорит: «Домен нуждается в этой функциональности», и хранилище данных затем предоставляет эту функциональность. Если у вас есть IProductRepositoryинтерфейс с DoesNameExist, очевидно, что метод должен делать. Но обеспечение того, чтобы Product не мог иметь то же имя, что и существующий продукт, должно быть где-то в домене.
Эйфорическая
4

Вам нужно прочитать Грега Янга о проверке набора .

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

Проблема с требованиями «уникальности» заключается в том, что очень часто существует более глубокая причина, по которой люди хотят их получить, - Ив Рейнхаут

Более длинный ответ: я видел меню возможностей, но у них всех есть компромиссы.

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

class Product {
    void register(SKU sku, DuplicationService skuLookup) {
        if (skuLookup.isKnownSku(sku) {
            throw ProductWithSKUAlreadyExistsException(...)
        }
        ...
    }
}

Разумеется, для реализации DeduplicationService необходимо будет кое-что узнать о том, как искать существующее skus. Таким образом, несмотря на то, что часть работы возвращается в домен, вы все еще сталкиваетесь с теми же основными проблемами (необходим ответ для проверки набора, проблемы с условиями гонки).

Вы можете выполнить проверку в вашем слое персистентности. Реляционные базы данных действительно хороши при проверке набора. Поместите ограничение уникальности в столбец sku вашего продукта, и все готово. Приложение просто сохраняет продукт в репозитории, и вы получаете сообщение о нарушении ограничения, которое копируется в случае возникновения проблемы. Таким образом, код приложения выглядит хорошо, и ваше состояние гонки устранено, но у вас есть «доменные» правила.

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

Одним из них является что-то вроде ProductCatalog; продукты существуют где-то еще, но связь между продуктами и skus поддерживается каталогом, который гарантирует уникальность sku. Это не значит, что у продуктов нет скуса; Skus присваивается ProductCatalog (если вы хотите, чтобы Skus был уникальным, вы достигнете этого, имея только один агрегат ProductCatalog). Просмотрите вездесущий язык с экспертами в своей области - если такая вещь существует, это вполне может быть правильным подходом.

Альтернатива - это что-то вроде сервиса бронирования номеров. Основной механизм один и тот же: агрегат знает обо всех skus, поэтому может предотвратить введение дубликатов. Но механизм немного другой: вы приобретаете арендную плату перед тем, как присвоить ее продукту; при создании продукта вы передаете его в аренду. В игре по-прежнему присутствует состояние гонки (разные агрегаты, следовательно, разные транзакции), но у него другой вкус. Настоящим недостатком здесь является то, что вы проектируете в доменную модель лизинговую услугу, не имея на самом деле обоснования в доменном языке.

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

Мне не нравится необходимость вытаскивать все SKU продукта из базы данных для выполнения операции в памяти.

Может быть, вам не нужно. Если вы протестируете свой sku с фильтром Bloom , вы можете обнаружить множество уникальных skus, не загружая набор вообще.

Если ваш вариант использования позволяет вам быть произвольным в отношении того, какой класс отвергнуть, вы можете удалить все ложные срабатывания (не так уж сложно, если вы разрешите клиентам тестировать предлагаемый план перед отправкой команды). Это позволит вам избежать загрузки набора в память.

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

VoiceOfUnreason
источник
Мне не нравится необходимость вытаскивать все SKU продукта из базы данных для выполнения операции в памяти. Это очевидное решение, которое я предложил в первоначальном вопросе и поставил под сомнение его эффективность. Кроме того, IMO, полагаясь на ограничение в вашей БД, чтобы нести ответственность за уникальность, плохо. Если вам нужно было переключиться на новый механизм базы данных, и каким-то образом уникальное ограничение было потеряно во время преобразования, вы нарушили код, потому что ранее отсутствовала информация базы данных, которая была там раньше. В любом случае спасибо за ссылку, интересно читаю.
Энди
Возможно, фильтр Блума (см. Редактирование).
VoiceOfUnreason