Моделирование продукта, категории, атрибутов в DDD

#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, поскольку мы извлекаем входные данные из разных мест.

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

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

Большинство данных из реального мира — это данные извне (платежный адрес Боба может изменяться без вашего разрешения — в вашей базе данных есть кэшированная копия платежного адреса Боба на какой-то момент в прошлом).