#domain-driven-design #e-commerce #aggregateroot
#дизайн, управляемый доменом #электронная коммерция #aggregateroot
Вопрос:
Я пытаюсь смоделировать каталог интернет-магазина, используя дизайн, управляемый доменом.
На данный момент у меня есть три основные концепции: продукт, категория, атрибут.
Атрибут — это характеристика продукта. Например, такие вещи, как цвет, вес, количество ядер процессора и т.д. Есть атрибуты, возможные значения которых фиксированы, например «условие» — может быть новым или использованным. Некоторые из них находятся в пределах некоторого диапазона значений, например «количество ядер процессора». Некоторые из них создаются свободно, например, «color».
Категория имеет обязательные атрибуты, которые должен иметь каждый продукт в этой категории, и необязательные. Категории могут иметь родительские категории.
Продукт принадлежит к одной категории, которая должна быть конечной категорией (без дочерних категорий).
Теперь у меня проблема в том, чтобы смоделировать эти три концепции как агрегированные.
Один из вариантов — иметь три разных агрегата: продукт, атрибут, категория.
Продукт будет иметь свои значения атрибутов (каждое с родительским идентификатором для атрибута AR). Атрибуты будут разных типов (фиксированные, свободно выбираемые, диапазон). Категория будет содержать список идентификаторов атрибутов, которые требуются, и список идентификаторов
Проблема здесь в том, что всякий раз, когда мне нужно создать новый продукт, мне нужно будет проверить, имеет ли он все требуемые атрибуты, проверить значения, а затем сохранить продукт. Эта проверка будет охватывать три агрегированных показателя. Куда это должно пойти? Это должен быть доменный сервис?
Другой вариант — иметь 2 AR. Категория с ее продуктами и атрибутами. Проблема здесь снова заключается в проверке правильности значений для одного атрибута, добавленного к продукту. Другая огромная проблема, которую я вижу здесь, заключается в том, что я должен извлекать всю совокупность из репозитория. Учитывая, что в этой категории могут быть сотни продуктов, я не думаю, что это хорошая идея. Однако это имеет смысл как концептуальное целое, поскольку, если я хотел бы удалить категорию, все ее продукты также должны быть удалены.
Чего мне здесь не хватает?
Ответ №1:
В «Реализации дизайна, управляемого доменом» Во Вернон использует «шаблон спецификации» для обработки проверки сущности / агрегата. Не цитируя всю главу, у вас есть разные возможности: (В моем примере используется Java, я надеюсь, вы уловили общую идею)
Проверка атрибутов / свойств
Если это простой процесс проверки по полю, то проверяйте каждый атрибут отдельно внутри метода setter.
class Product {
String name;
public Product(String name) {
setName(name);
}
public void setName(String name) {
if(name == null) {
throw new IllegalArgumentException("name cannot be null");
}
if(name.length() == 0) {
throw new IllegalArgumentException("name cannot be empty");
}
this.name = name;
}
}
Проверка всего объекта
Если вам нужно проверить весь объект, вы можете использовать своего рода спецификацию, которая поможет вам. Чтобы избежать того, чтобы у объекта было слишком много обязанностей (иметь дело с состоянием и проверять его), вы можете использовать средство проверки.
a. Create a generic Validator class, and implement it for your Product Validator. Use a NotificationHandler to deal with your validation error (exception, event, accumulating errors and then sending them ? up to you) :
public abstract class Validator {
private ValidationNotificationHandler notificationHandler;
public Validator(ValidationNotificationHandler aHandler) {
super();
this.setNotificationHandler(aHandler);
}
public abstract void validate();
protected ValidationNotificationHandler notificationHandler() {
return this.notificationHandler;
}
private void setNotificationHandler(ValidationNotificationHandler aHandler) {
this.notificationHandler = aHandler;
}
}
NotificationHandler
это интерфейс, который вы могли бы реализовать с учетом ваших требований с точки зрения обработки ошибок проверки. Вот интерфейс, предложенный Во Верноном :
public interface ValidationNotificationHandler {
public void handleError(String aNotificationMessage);
public void handleError(String aNotification, Object anObject);
public void handleInfo(String aNotificationMessage);
public void handleInfo(String aNotification, Object anObject);
public void handleWarning(String aNotificationMessage);
public void handleWarning(String aNotification, Object anObject);
}
b. Реализует этот класс с помощью определенного средства проверки ProductValidator
:
public class ProductValidator extends Validator {
private Product product;
public ProductValidator(Product product, ValidationNotificationHandler aHandler) {
super(aHandler);
this.setProduct(product);
}
private void setProduct(Product product) {
this.product = product;
}
@Override
public void validate() {
this.checkForCompletness();
}
private void checkForCompletness() {
if(product.getName().equals("bad name") amp;amp; anotherCondition()) {
notificationHandler().handleError("This specific validation failed");
}
...
}
}
И затем вы можете обновить свою сущность с помощью метода validate, который вызовет этот валидатор для проверки всего объекта:
public class Product {
private String name;
public Product(String name) {
setName(name);
}
private void setName(String name) {
if (name == null) {
throw new IllegalArgumentException("Name cannot be null");
}
if (name.length() == 0) {
throw new IllegalArgumentException("Name cannot be empty");
}
this.name = name;
}
// Here is the new method to validate your object
public void validate(ValidationNotificationHandler aHandler) {
(new ProductValidator(this, aHandler)).validate();
}
}
Проверка нескольких агрегатов
И, наконец, что является вашей непосредственной заботой, если вы хотите проверить несколько агрегатов, чтобы получить что-то согласованное, рекомендуется создать доменную службу и конкретный валидатор. Службы домена могут либо вводить репозитории для поиска различных агрегатов, либо все создается прикладными уровнями, а затем вводить различные агрегаты в качестве параметра метода:
public class ProductCategoryValidator extends Validator {
private Product product;
private Category category;
public ProductCategoryValidator(Product product, Category category, ValidationNotificationHandler aHandler) {
super(aHandler);
this.setProduct(product);
this.setCategory(category);
}
private void setCategory(Category category) {
this.category = category;
}
private void setProduct(Product product) {
this.product = product;
}
@Override
public void validate() {
this.checkForCompletness();
}
private void checkForCompletness() {
// Count number of attributes, check for correctness...
}
}
И служба домена, которая будет вызывать средство проверки
public class ProductService {
// Use this is you can pass the parameters from the client
public void validateProductWithCategory(Product product, Category category, ValidationNotificationHandler handler) {
(new ProductCategoryValidator(product, category, handler)).validate();
}
// Use This is you need to retrieve data from persistent layer
private ProductRepository productRepository;
private CategoryReposiory categoryReposiory;
public ProductService(ProductRepository productRepository, CategoryReposiory categoryReposiory) {
this.productRepository = productRepository;
this.categoryReposiory = categoryReposiory;
}
public void validate(String productId, ValidationNotificationHandler handler) {
Product product = productRepository.findById(productId);
Category category = categoryReposiory.categoryOfProductId(productId);
(new ProductCategoryValidator(product, category, handler)).validate();
}
}
Как я уже сказал, я думаю, вас может заинтересовать решение 3. Как вы уже догадались, вы можете использовать доменную службу. Но добавьте специальный валидатор, чтобы гарантировать, что «обязанности» не смешаны.
Комментарии:
1. Спасибо за такой подробный ответ — я думаю, мне нужно прочесть эту книгу 🙂
Ответ №2:
Проблема здесь в том, что всякий раз, когда мне нужно создать новый продукт, мне нужно будет проверить, имеет ли он все требуемые атрибуты, проверить значения, а затем сохранить продукт. Эта проверка будет охватывать три агрегированных показателя. Куда это должно пойти? Это должен быть доменный сервис?
Обычный ответ заключается в том, что извлечение информации (иначе называемый ввод-вывод) выполняется в службе приложения. Копии этой информации затем передаются, как и другие входные данные, в модель домена.
Одна «транзакция» может включать в себя несколько вызовов методов aggregate, поскольку мы извлекаем входные данные из разных мест.
Эти копии информации обычно обрабатываются как данные извне — здесь у нас есть разблокированная копия данных; пока мы используем эту копию, авторитетная копия может меняться.
Если вы поймаете себя на мысли, что «авторитетную копию данных вон там нельзя изменять, пока я использую ее здесь» — это большой красный флаг, свидетельствующий о том, что либо (а) вы на самом деле не понимаете свои реальные ограничения данных, либо (б) вы неправильно нарисовали свои совокупные границы.
Большинство данных из реального мира — это данные извне (платежный адрес Боба может изменяться без вашего разрешения — в вашей базе данных есть кэшированная копия платежного адреса Боба на какой-то момент в прошлом).