- 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
21 KiB
Доменная модель и Domain-Driven Design
Навигация
Что такое домен?
Домен (Domain) — это предметная область, в которой работает приложение. Это сфера знаний, правил и процессов, которые определяют, как работает бизнес.
Ключевые характеристики домена:
- Бизнес-логика — правила и процессы, специфичные для предметной области
- Терминология — язык, используемый экспертами в данной области
- Ограничения — правила и ограничения, которые должны соблюдаться
- Процессы — последовательности действий, выполняемых в домене
Domain-Driven Design (DDD)
Domain-Driven Design — подход к разработке программного обеспечения, который фокусируется на моделировании предметной области.
Основные принципы DDD:
- Фокус на домене — архитектура отражает структуру домена
- Единый язык — общий язык между разработчиками и экспертами
- Модель-код соответствие — код отражает доменную модель
- Итеративное уточнение — постоянное улучшение понимания домена
Строительные блоки DDD
1. Entity (Сущность)
Entity — объект, который имеет уникальную идентичность и жизненный цикл.
// Пример 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 — объект, который определяется только своими атрибутами, без идентичности.
// Пример 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 — кластер связанных объектов, которые рассматриваются как единое целое.
// Пример 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 — сервис, который содержит бизнес-логику, не принадлежащую ни одной сущности.
// Пример 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 — интерфейс для доступа к агрегатам.
// Интерфейс репозитория
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:
- Order Management — управление заказами
- Customer Management — управление клиентами
- Inventory Management — управление складом
- Payment Processing — обработка платежей
// 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:
// Термины домена
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:
- Доменные события — что происходит в системе
- Команды — что вызывает события
- Агрегаты — что обрабатывает команды
- Политики — бизнес-правила
- Внешние системы — интеграции
# Event Storming для E-commerce
## Доменные события
- OrderCreated (Заказ создан)
- OrderConfirmed (Заказ подтвержден)
- PaymentProcessed (Платеж обработан)
- OrderShipped (Заказ отправлен)
- OrderDelivered (Заказ доставлен)
## Команды
- CreateOrder (Создать заказ)
- ConfirmOrder (Подтвердить заказ)
- ProcessPayment (Обработать платеж)
- ShipOrder (Отправить заказ)
## Агрегаты
- Order (Заказ)
- Customer (Клиент)
- Product (Товар)
- Payment (Платеж)
## Политики
- Скидка за количество товаров
- VIP статус клиента
- Праздничные скидки
Антипаттерны DDD
1. Anemic Domain Model (Анемичная доменная модель)
Проблема: Доменные объекты содержат только данные без поведения.
// Плохо: Анемичная модель
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 (Божественный объект)
Проблема: Один объект содержит слишком много ответственности.
// Плохо: 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
// 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
// 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. Начните с доменных событий
// Сначала определите события
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 для примитивов
// Вместо 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. Следите за границами агрегатов
// Правильно: Агрегат управляет только своими данными
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 помогает создавать архитектуру, которая:
- Отражает бизнес-логику — код соответствует домену
- Улучшает коммуникацию — единый язык между командами
- Повышает качество — четкие границы и ответственности
- Упрощает тестирование — изолированная доменная логика
- Обеспечивает эволюцию — гибкая архитектура для изменений
DDD особенно эффективен для сложных бизнес-доменов, где важно точное понимание предметной области и создание архитектуры, которая может эволюционировать вместе с бизнесом.