Files
sa-lessons/modules/module1/sections/03-domain-driven-design.md
T
Alexander Abakumov 15e744da49 Initial commit: Complete Software Architecture course with 11 modules
- Added comprehensive course structure with 11 modules
- Included theoretical content, practical examples, and tasks
- Multi-language code examples (Java, Rust, Node.js, Python)
- Detailed analysis of architectural approaches and trade-offs
- Complete caching strategies documentation
- Architecture examples from real-world systems (Twitter, Netflix, Uber)
- Structured navigation and learning path
- Ready for educational use
2025-09-09 22:08:07 +03:00

684 lines
21 KiB
Markdown

# Доменная модель и 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<OrderItem> 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<OrderItem> 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<OrderItem> items;
private List<DomainEvent> domainEvents;
// Конструктор и методы...
// Методы для работы с доменными событиями
public List<DomainEvent> 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<Order> findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId);
List<Order> 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<Order> findById(OrderId id) {
Order order = entityManager.find(Order.class, id);
return Optional.ofNullable(order);
}
@Override
public List<Order> findByCustomerId(CustomerId customerId) {
return entityManager.createQuery(
"SELECT o FROM Order o WHERE o.customerId = :customerId", Order.class)
.setParameter("customerId", customerId)
.getResultList();
}
@Override
public List<Order> 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<OrderResponse> 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<Order> 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<OrderItem> 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 особенно эффективен для сложных бизнес-доменов, где важно точное понимание предметной области и создание архитектуры, которая может эволюционировать вместе с бизнесом.