Files
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

21 KiB

Доменная модель и Domain-Driven Design

Навигация


Что такое домен?

Домен (Domain) — это предметная область, в которой работает приложение. Это сфера знаний, правил и процессов, которые определяют, как работает бизнес.

Ключевые характеристики домена:

  1. Бизнес-логика — правила и процессы, специфичные для предметной области
  2. Терминология — язык, используемый экспертами в данной области
  3. Ограничения — правила и ограничения, которые должны соблюдаться
  4. Процессы — последовательности действий, выполняемых в домене

Domain-Driven Design (DDD)

Domain-Driven Design — подход к разработке программного обеспечения, который фокусируется на моделировании предметной области.

Основные принципы DDD:

  1. Фокус на домене — архитектура отражает структуру домена
  2. Единый язык — общий язык между разработчиками и экспертами
  3. Модель-код соответствие — код отражает доменную модель
  4. Итеративное уточнение — постоянное улучшение понимания домена

Строительные блоки 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:

  1. Order Management — управление заказами
  2. Customer Management — управление клиентами
  3. Inventory Management — управление складом
  4. 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:

  1. Доменные события — что происходит в системе
  2. Команды — что вызывает события
  3. Агрегаты — что обрабатывает команды
  4. Политики — бизнес-правила
  5. Внешние системы — интеграции
# 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 помогает создавать архитектуру, которая:

  1. Отражает бизнес-логику — код соответствует домену
  2. Улучшает коммуникацию — единый язык между командами
  3. Повышает качество — четкие границы и ответственности
  4. Упрощает тестирование — изолированная доменная логика
  5. Обеспечивает эволюцию — гибкая архитектура для изменений

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