# Доменная модель и Domain-Driven Design ## Навигация - [🏠 Главная страница курса](../../../README.md) - [📚 Модуль 1](../README.md) --- ## Что такое домен? **Домен (Domain)** — это предметная область, в которой работает приложение. Это сфера знаний, правил и процессов, которые определяют, как работает бизнес. ### Ключевые характеристики домена: 1. **Бизнес-логика** — правила и процессы, специфичные для предметной области 2. **Терминология** — язык, используемый экспертами в данной области 3. **Ограничения** — правила и ограничения, которые должны соблюдаться 4. **Процессы** — последовательности действий, выполняемых в домене ## Domain-Driven Design (DDD) **Domain-Driven Design** — подход к разработке программного обеспечения, который фокусируется на моделировании предметной области. ### Основные принципы DDD: 1. **Фокус на домене** — архитектура отражает структуру домена 2. **Единый язык** — общий язык между разработчиками и экспертами 3. **Модель-код соответствие** — код отражает доменную модель 4. **Итеративное уточнение** — постоянное улучшение понимания домена ## Строительные блоки DDD ### 1. Entity (Сущность) **Entity** — объект, который имеет уникальную идентичность и жизненный цикл. ```java // Пример Entity - Заказ @Entity public class Order { @Id private OrderId id; private CustomerId customerId; private OrderStatus status; private Money totalAmount; private List items; // Конструктор public Order(OrderId id, CustomerId customerId) { this.id = id; this.customerId = customerId; this.status = OrderStatus.DRAFT; this.items = new ArrayList<>(); } // Бизнес-методы public void addItem(ProductId productId, Quantity quantity, Money unitPrice) { if (status != OrderStatus.DRAFT) { throw new IllegalStateException("Cannot modify confirmed order"); } OrderItem item = new OrderItem(productId, quantity, unitPrice); items.add(item); recalculateTotal(); } public void confirm() { if (items.isEmpty()) { throw new IllegalStateException("Cannot confirm empty order"); } this.status = OrderStatus.CONFIRMED; // Доменное событие DomainEvents.raise(new OrderConfirmed(this.id)); } private void recalculateTotal() { this.totalAmount = items.stream() .map(OrderItem::getSubtotal) .reduce(Money.ZERO, Money::add); } // Геттеры public OrderId getId() { return id; } public CustomerId getCustomerId() { return customerId; } public OrderStatus getStatus() { return status; } public Money getTotalAmount() { return totalAmount; } public List getItems() { return Collections.unmodifiableList(items); } } ``` ### 2. Value Object (Объект-значение) **Value Object** — объект, который определяется только своими атрибутами, без идентичности. ```java // Пример Value Object - Деньги public class Money { private final BigDecimal amount; private final Currency currency; public Money(BigDecimal amount, Currency currency) { if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("Amount must be non-negative"); } if (currency == null) { throw new IllegalArgumentException("Currency cannot be null"); } this.amount = amount.setScale(2, RoundingMode.HALF_UP); this.currency = currency; } public Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException("Cannot add different currencies"); } return new Money(this.amount.add(other.amount), this.currency); } public Money multiply(BigDecimal factor) { return new Money(this.amount.multiply(factor), this.currency); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Money money = (Money) obj; return Objects.equals(amount, money.amount) && Objects.equals(currency, money.currency); } @Override public int hashCode() { return Objects.hash(amount, currency); } // Геттеры public BigDecimal getAmount() { return amount; } public Currency getCurrency() { return currency; } public static final Money ZERO = new Money(BigDecimal.ZERO, Currency.getInstance("USD")); } ``` ### 3. Aggregate (Агрегат) **Aggregate** — кластер связанных объектов, которые рассматриваются как единое целое. ```java // Пример Aggregate - Заказ public class Order { private OrderId id; private CustomerId customerId; private OrderStatus status; private Money totalAmount; private List items; private List domainEvents; // Конструктор и методы... // Методы для работы с доменными событиями public List getUncommittedEvents() { return new ArrayList<>(domainEvents); } public void markEventsAsCommitted() { domainEvents.clear(); } private void raiseEvent(DomainEvent event) { domainEvents.add(event); } } // Корневой объект агрегата public class OrderAggregate { private Order order; public OrderAggregate(OrderId id, CustomerId customerId) { this.order = new Order(id, customerId); } public void addItem(ProductId productId, Quantity quantity, Money unitPrice) { order.addItem(productId, quantity, unitPrice); } public void confirm() { order.confirm(); } public Order getOrder() { return order; } } ``` ### 4. Domain Service (Доменный сервис) **Domain Service** — сервис, который содержит бизнес-логику, не принадлежащую ни одной сущности. ```java // Пример Domain Service - Расчет скидки @Service public class DiscountCalculationService { public Money calculateDiscount(Order order, Customer customer) { Money discount = Money.ZERO; // Скидка за количество товаров int totalItems = order.getItems().stream() .mapToInt(item -> item.getQuantity().getValue()) .sum(); if (totalItems >= 10) { discount = discount.add(order.getTotalAmount().multiply(new BigDecimal("0.1"))); } // Скидка для VIP клиентов if (customer.isVip()) { discount = discount.add(order.getTotalAmount().multiply(new BigDecimal("0.05"))); } // Скидка за праздники if (isHolidayPeriod()) { discount = discount.add(order.getTotalAmount().multiply(new BigDecimal("0.15"))); } return discount; } private boolean isHolidayPeriod() { LocalDate now = LocalDate.now(); return now.getMonth() == Month.DECEMBER && now.getDayOfMonth() >= 20; } } ``` ### 5. Repository (Репозиторий) **Repository** — интерфейс для доступа к агрегатам. ```java // Интерфейс репозитория public interface OrderRepository { void save(Order order); Optional findById(OrderId id); List findByCustomerId(CustomerId customerId); List findByStatus(OrderStatus status); void delete(OrderId id); } // Реализация репозитория @Repository public class JpaOrderRepository implements OrderRepository { @PersistenceContext private EntityManager entityManager; @Override public void save(Order order) { entityManager.persist(order); } @Override public Optional findById(OrderId id) { Order order = entityManager.find(Order.class, id); return Optional.ofNullable(order); } @Override public List findByCustomerId(CustomerId customerId) { return entityManager.createQuery( "SELECT o FROM Order o WHERE o.customerId = :customerId", Order.class) .setParameter("customerId", customerId) .getResultList(); } @Override public List findByStatus(OrderStatus status) { return entityManager.createQuery( "SELECT o FROM Order o WHERE o.status = :status", Order.class) .setParameter("status", status) .getResultList(); } @Override public void delete(OrderId id) { Order order = entityManager.find(Order.class, id); if (order != null) { entityManager.remove(order); } } } ``` ## Bounded Context (Ограниченный контекст) **Bounded Context** — граница, в которой определенная доменная модель является валидной. ### Примеры Bounded Context: 1. **Order Management** — управление заказами 2. **Customer Management** — управление клиентами 3. **Inventory Management** — управление складом 4. **Payment Processing** — обработка платежей ```java // Bounded Context: Order Management @BoundedContext("OrderManagement") public class OrderContext { // Агрегаты public class Order { /* ... */ } public class OrderItem { /* ... */ } // Сервисы public class OrderService { /* ... */ } public class OrderValidationService { /* ... */ } // Репозитории public interface OrderRepository { /* ... */ } } // Bounded Context: Customer Management @BoundedContext("CustomerManagement") public class CustomerContext { // Агрегаты public class Customer { /* ... */ } public class CustomerProfile { /* ... */ } // Сервисы public class CustomerService { /* ... */ } public class CustomerRegistrationService { /* ... */ } // Репозитории public interface CustomerRepository { /* ... */ } } ``` ## Ubiquitous Language (Единый язык) **Ubiquitous Language** — общий язык между разработчиками и экспертами предметной области. ### Пример единого языка для e-commerce: ```java // Термины домена public enum OrderStatus { DRAFT("Черновик"), CONFIRMED("Подтвержден"), SHIPPED("Отправлен"), DELIVERED("Доставлен"), CANCELLED("Отменен"); private final String description; OrderStatus(String description) { this.description = description; } public String getDescription() { return description; } } // Доменные события public class OrderConfirmed implements DomainEvent { private final OrderId orderId; private final LocalDateTime occurredOn; public OrderConfirmed(OrderId orderId) { this.orderId = orderId; this.occurredOn = LocalDateTime.now(); } public OrderId getOrderId() { return orderId; } public LocalDateTime getOccurredOn() { return occurredOn; } } ``` ## Event Storming **Event Storming** — методология для изучения домена через события. ### Процесс Event Storming: 1. **Доменные события** — что происходит в системе 2. **Команды** — что вызывает события 3. **Агрегаты** — что обрабатывает команды 4. **Политики** — бизнес-правила 5. **Внешние системы** — интеграции ```markdown # Event Storming для E-commerce ## Доменные события - OrderCreated (Заказ создан) - OrderConfirmed (Заказ подтвержден) - PaymentProcessed (Платеж обработан) - OrderShipped (Заказ отправлен) - OrderDelivered (Заказ доставлен) ## Команды - CreateOrder (Создать заказ) - ConfirmOrder (Подтвердить заказ) - ProcessPayment (Обработать платеж) - ShipOrder (Отправить заказ) ## Агрегаты - Order (Заказ) - Customer (Клиент) - Product (Товар) - Payment (Платеж) ## Политики - Скидка за количество товаров - VIP статус клиента - Праздничные скидки ``` ## Антипаттерны DDD ### 1. Anemic Domain Model (Анемичная доменная модель) **Проблема:** Доменные объекты содержат только данные без поведения. ```java // Плохо: Анемичная модель public class Order { private Long id; private String customerId; private String status; private BigDecimal totalAmount; // Только геттеры и сеттеры public Long getId() { return id; } public void setId(Long id) { this.id = id; } // ... } // Хорошо: Богатая доменная модель public class Order { private OrderId id; private CustomerId customerId; private OrderStatus status; private Money totalAmount; // Бизнес-методы public void addItem(ProductId productId, Quantity quantity, Money unitPrice) { // Бизнес-логика } public void confirm() { // Бизнес-логика } } ``` ### 2. God Object (Божественный объект) **Проблема:** Один объект содержит слишком много ответственности. ```java // Плохо: God Object public class Order { // Слишком много ответственности public void addItem() { /* ... */ } public void calculateTax() { /* ... */ } public void sendEmail() { /* ... */ } public void updateInventory() { /* ... */ } public void processPayment() { /* ... */ } } // Хорошо: Разделение ответственности public class Order { public void addItem() { /* ... */ } } public class TaxCalculationService { public Money calculateTax(Order order) { /* ... */ } } public class EmailService { public void sendOrderConfirmation(Order order) { /* ... */ } } ``` ## Интеграция DDD с архитектурой ### 1. Layered Architecture + DDD ```java // Presentation Layer @RestController public class OrderController { @Autowired private OrderApplicationService orderService; @PostMapping("/orders") public ResponseEntity createOrder(@RequestBody CreateOrderRequest request) { OrderId orderId = orderService.createOrder(request); return ResponseEntity.ok(new OrderResponse(orderId)); } } // Application Layer @Service public class OrderApplicationService { @Autowired private OrderRepository orderRepository; @Autowired private OrderDomainService orderDomainService; public OrderId createOrder(CreateOrderRequest request) { Order order = new Order( new OrderId(UUID.randomUUID()), new CustomerId(request.getCustomerId()) ); for (OrderItemRequest itemRequest : request.getItems()) { order.addItem( new ProductId(itemRequest.getProductId()), new Quantity(itemRequest.getQuantity()), new Money(itemRequest.getUnitPrice(), Currency.USD) ); } orderRepository.save(order); return order.getId(); } } // Domain Layer public class Order { // Доменная логика } // Infrastructure Layer @Repository public class JpaOrderRepository implements OrderRepository { // Реализация репозитория } ``` ### 2. Hexagonal Architecture + DDD ```java // Domain Layer (внутренний круг) public class Order { // Доменная логика } public interface OrderRepository { void save(Order order); Optional findById(OrderId id); } // Application Layer @Service public class OrderService { private final OrderRepository orderRepository; public OrderService(OrderRepository orderRepository) { this.orderRepository = orderRepository; } public void createOrder(Order order) { // Применение доменных правил orderRepository.save(order); } } // Infrastructure Layer (внешний круг) @Repository public class JpaOrderRepository implements OrderRepository { // Реализация через JPA } // Adapters @RestController public class OrderController { // REST API } @Component public class OrderEventHandler { // Обработка событий } ``` ## Практические рекомендации ### 1. Начните с доменных событий ```java // Сначала определите события public class OrderCreated implements DomainEvent { private final OrderId orderId; private final CustomerId customerId; private final LocalDateTime occurredOn; // Конструктор, геттеры } // Затем создайте агрегаты public class Order { public void confirm() { // Бизнес-логика DomainEvents.raise(new OrderConfirmed(this.id)); } } ``` ### 2. Используйте Value Objects для примитивов ```java // Вместо String public class CustomerId { private final String value; public CustomerId(String value) { if (value == null || value.trim().isEmpty()) { throw new IllegalArgumentException("Customer ID cannot be empty"); } this.value = value; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; CustomerId that = (CustomerId) obj; return Objects.equals(value, that.value); } @Override public int hashCode() { return Objects.hash(value); } } ``` ### 3. Следите за границами агрегатов ```java // Правильно: Агрегат управляет только своими данными public class Order { private OrderId id; private List items; public void addItem(ProductId productId, Quantity quantity, Money unitPrice) { // Логика добавления товара в заказ } } // Неправильно: Агрегат управляет чужими данными public class Order { public void updateProductInventory(ProductId productId, Quantity quantity) { // Это не должно быть в Order агрегате } } ``` ## Заключение Domain-Driven Design помогает создавать архитектуру, которая: 1. **Отражает бизнес-логику** — код соответствует домену 2. **Улучшает коммуникацию** — единый язык между командами 3. **Повышает качество** — четкие границы и ответственности 4. **Упрощает тестирование** — изолированная доменная логика 5. **Обеспечивает эволюцию** — гибкая архитектура для изменений DDD особенно эффективен для сложных бизнес-доменов, где важно точное понимание предметной области и создание архитектуры, которая может эволюционировать вместе с бизнесом.