Я уже 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 как продукт, который вы пытаетесь добавить.
Как вы, ребята, решаете эту конкретную проблему? На самом деле это не проблема, у меня есть уровень обслуживания, представляющий определенные правила домена в течение многих лет. Сервисный уровень обычно также обслуживает более сложные доменные операции, мне просто интересно, наткнулись ли вы на лучшее, более централизованное решение в течение своей карьеры.
Ответы:
Я бы не согласился с этой частью. Особенно последнее предложение.
Хотя это правда, что домен должен игнорировать постоянство, он знает, что существует «Коллекция сущностей домена». И что существуют доменные правила, которые касаются этой коллекции в целом. Уникальность является одним из них. А поскольку реализация реальной логики в значительной степени зависит от конкретного режима персистентности, в области должна быть какая-то абстракция, определяющая потребность в этой логике.
Так что это так же просто, как создать интерфейс, который может запрашивать, если имя уже существует, который затем реализуется в вашем хранилище данных и вызывается тем, кто должен знать, является ли имя уникальным.
И я хотел бы подчеркнуть, что репозитории являются сервисами DOMAIN. Это абстракции вокруг постоянства. Это реализация репозитория, которая должна быть отделена от домена. Нет абсолютно ничего плохого в том, что сущность домена вызывает службу домена. Нет ничего плохого в том, что одна сущность может использовать хранилище для извлечения другой сущности или извлечения некоторой конкретной информации, которую нельзя легко сохранить в памяти. Это причина, почему репозиторий является ключевым понятием в книге Эванса .
источник
Domain.ProductCollection
я имел в виду, учитывая, что они отвечают за извлечение объектов из слоя домена?Domain.ProductCollection
а вместо этого спрашиваю хранилище, то же самое, спрашивая,Domain.ProductCollection
содержит ли Продукт Продукт с передал SKU, на этот раз снова, вместо этого запрашивая хранилище (это на самом деле пример), которое вместо итерации по предварительно загруженным продуктам запрашивает базовую базу данных. Я не имею в виду хранить весь продукт в памяти, если мне не нужно, это было бы полной чушью.IProductRepository
интерфейс сDoesNameExist
, очевидно, что метод должен делать. Но обеспечение того, чтобы Product не мог иметь то же имя, что и существующий продукт, должно быть где-то в домене.Вам нужно прочитать Грега Янга о проверке набора .
Короткий ответ: прежде чем заходить слишком далеко в гнездо крыс, необходимо убедиться, что вы понимаете ценность требования с точки зрения бизнеса. На самом деле, насколько дорогостоящим является обнаружение и уменьшение дублирования, а не его предотвращение?
Более длинный ответ: я видел меню возможностей, но у них всех есть компромиссы.
Вы можете проверить наличие дубликатов перед отправкой команды в домен. Это может быть сделано в клиенте или в сервисе (ваш пример показывает технику). Если вас не устраивает утечка логики из доменного уровня, вы можете добиться такого же результата с помощью
DomainService
.Разумеется, для реализации DeduplicationService необходимо будет кое-что узнать о том, как искать существующее skus. Таким образом, несмотря на то, что часть работы возвращается в домен, вы все еще сталкиваетесь с теми же основными проблемами (необходим ответ для проверки набора, проблемы с условиями гонки).
Вы можете выполнить проверку в вашем слое персистентности. Реляционные базы данных действительно хороши при проверке набора. Поместите ограничение уникальности в столбец sku вашего продукта, и все готово. Приложение просто сохраняет продукт в репозитории, и вы получаете сообщение о нарушении ограничения, которое копируется в случае возникновения проблемы. Таким образом, код приложения выглядит хорошо, и ваше состояние гонки устранено, но у вас есть «доменные» правила.
Вы можете создать отдельный агрегат в своем домене, который представляет набор известных skus. Я могу думать о двух вариантах здесь.
Одним из них является что-то вроде ProductCatalog; продукты существуют где-то еще, но связь между продуктами и skus поддерживается каталогом, который гарантирует уникальность sku. Это не значит, что у продуктов нет скуса; Skus присваивается ProductCatalog (если вы хотите, чтобы Skus был уникальным, вы достигнете этого, имея только один агрегат ProductCatalog). Просмотрите вездесущий язык с экспертами в своей области - если такая вещь существует, это вполне может быть правильным подходом.
Альтернатива - это что-то вроде сервиса бронирования номеров. Основной механизм один и тот же: агрегат знает обо всех skus, поэтому может предотвратить введение дубликатов. Но механизм немного другой: вы приобретаете арендную плату перед тем, как присвоить ее продукту; при создании продукта вы передаете его в аренду. В игре по-прежнему присутствует состояние гонки (разные агрегаты, следовательно, разные транзакции), но у него другой вкус. Настоящим недостатком здесь является то, что вы проектируете в доменную модель лизинговую услугу, не имея на самом деле обоснования в доменном языке.
Вы можете объединить все объекты продуктов в один агрегат, т. Е. Каталог продуктов, описанный выше. Вы абсолютно получите уникальность артикулов, когда вы делаете это, но стоимость является дополнительным утверждение, изменение какой-либо продукт на самом деле означает изменение всего каталога.
Может быть, вам не нужно. Если вы протестируете свой sku с фильтром Bloom , вы можете обнаружить множество уникальных skus, не загружая набор вообще.
Если ваш вариант использования позволяет вам быть произвольным в отношении того, какой класс отвергнуть, вы можете удалить все ложные срабатывания (не так уж сложно, если вы разрешите клиентам тестировать предлагаемый план перед отправкой команды). Это позволит вам избежать загрузки набора в память.
(Если вы хотите быть более восприимчивым, вы можете рассмотреть ленивую загрузку скуса в случае совпадения в фильтре Блума; вы все равно иногда рискуете загрузить все скусы в память, но это не должно быть распространенным случаем, если вы разрешите код клиента для проверки команды на наличие ошибок перед отправкой).
источник