- 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
684 lines
21 KiB
Markdown
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 особенно эффективен для сложных бизнес-доменов, где важно точное понимание предметной области и создание архитектуры, которая может эволюционировать вместе с бизнесом.
|