Вступ
Проектування на основі доменів (DDD) — це архітектурний підхід, який фокусується на моделюванні бізнес-домену, його логіки та правил у програмних системах. В середовищі мікросервісів DDD допомагає розділити складну програму на менші, самостійні сервіси, які точно відображають реальні бізнес-домени. Замість того, щоб організовувати сервіси на основі технічних компонентів (як “контролери” або “репозиторії”), DDD заохочує організовувати їх за бізнес-можливостями.
Цей підхід гарантує, що мікросервіси добре узгоджені з бізнес-контекстом, що робить систему легшою для розуміння, підтримки та масштабування.
1. Ключові концепції DDD
1.1 Обмежений контекст
- Обмежений контекст (Bounded Context) визначає межі, в яких застосовується певна модель. Це гарантує, що модель залишатиметься послідовною і чіткою в межах свого контексту.
- У мікросервісній архітектурі кожен мікросервіс часто розглядається як Обмежений контекст.
Приклад:
- Сервіс “Order” обробляє операції, пов'язані з замовленнями.
- Сервіс “Payment” займається обробкою платежів.
Ці сервіси можуть мати спільні поняття (наприклад, “Order ID”), але їх значення і деталі реалізації можуть відрізнятися в різних контекстах.
1.2 Модель домену
- Модель домену представляє основні сутності, об'єкти значень та бізнес-правила домену.
- Сутності мають унікальні ідентифікатори (наприклад, Order, Customer), тоді як об'єкти значень представляють незмінні дані без ідентифікації (наприклад, Address, Money).
1.3 Універсальна мова
- Універсальна мова (Ubiquitous Language) — це спільна мова, яку розробники, експерти з домену та зацікавлені сторони використовують для опису системи. Вона з'єднує бізнес та технічні команди.
Приклад: Замість того, щоб використовувати загальні терміни як “record” або “object”, використовуйте бізнес-терміни як “Order”, “Customer”, “Payment”.
1.4 Агрегати
- Агрегат (Aggregate) — це група взаємопов'язаних сутностей та об'єктів значень, які розглядаються як єдина одиниця послідовності.
- Корінь агрегату (Aggregate Root) — це основна сутність, яка контролює доступ до агрегату.
Приклад:
В агрегаті Order сутність Order може агрегувати OrderItem, PaymentDetails та ShippingDetails.
1.5 Репозиторії
- Репозиторій (Repository) — це шар, який надає методи для доступу та управління доменними об'єктами.
- Він абстрагує операції з базою даних, надаючи методи типу save(), findById(), delete().
1.6 Доменні події
- Доменні події (Domain Events) представляють собою значні зміни або дії в домені.
- У мікросервісах доменні події часто публікуються в брокер подій (Event Broker) (як Kafka або RabbitMQ), щоб повідомити інші сервіси про зміни стану.
Приклад:
Коли замовлення розміщується, подія OrderPlacedEvent публікується для наступних сервісів (наприклад, для інвентаризації або платежів), щоб вони могли відреагувати.
2. Принципи DDD у мікросервісах
Проектування сервісів з обмеженими контекстами
Кожен мікросервіс відповідає за Обмежений контекст і інкапсулює конкретну частину домену. Це дає:
- Незалежні сервіси з чіткими обов'язками.
- Уникання спільних баз даних та моделей домену між сервісами.
- Зниження зв'язуваності та збільшення автономії.
3. Приклад DDD у середовищі мікросервісів
Використання: Додаток для електронної комерції
Система включає:
- Order Service: Керує замовленнями.
- Payment Service: Обробляє платіжні транзакції.
- Inventory Service: Резервує та відслідковує рівні запасів.
Кожен сервіс:
- Має свій обмежений контекст.
- Використовує моделі домену, що відображають його частину бізнес-домену.
- Комунікує через доменні події, щоб забезпечити кінцеву консистентність.
4. Приклад реалізації коду
1.
Сервіс замовлень
Сутності та агрегати
Order.java
package com.atk.order.domain;
import java.util.List;
public class Order {
private String orderId;
private List items;
private OrderStatus status;
public Order(String orderId, List items) {
this.orderId = orderId;
this.items = items;
this.status = OrderStatus.CREATED;
}
public void markAsPaid() {
if (this.status == OrderStatus.CREATED) {
this.status = OrderStatus.PAID;
}
}
public String getOrderId() {
return orderId;
}
}
OrderItem.java
package com.atk.order.domain;
public class OrderItem {
private String productId;
private int quantity;
// Конструктор, Геттери
}
Доменна подія
OrderPlacedEvent.java
package com.example.order.events;
public class OrderPlacedEvent {
private String orderId;
public OrderPlacedEvent(String orderId) {
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
Репозиторій
OrderRepository.java
package com.atk.order.repository;
import com.atk.order.domain.Order;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderRepository extends JpaRepository {
}
Сервіс
OrderService.java
package com.atk.order.service;
import com.atk.order.domain.Order;
import com.atk.order.events.OrderPlacedEvent;
import com.atk.order.repository.OrderRepository;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final KafkaTemplate kafkaTemplate;
public OrderService(OrderRepository orderRepository, KafkaTemplate kafkaTemplate) {
this.orderRepository = orderRepository;
this.kafkaTemplate = kafkaTemplate;
}
public void placeOrder(Order order) {
orderRepository.save(order);
kafkaTemplate.send("order-events", new OrderPlacedEvent(order.getOrderId()));
}
}
2. Сервіс платежів
Прослуховувач подій (Event Listener) для обробки платежів
PaymentListener.java
package com.atk.payment.listeners;
import com.atk.order.events.OrderPlacedEvent;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class PaymentListener {
@KafkaListener(topics = "order-events", groupId = "payment-group")
public void handleOrderPlaced(OrderPlacedEvent event) {
System.out.println("Обробка платежу для замовлення: " + event.getOrderId());
// Логіка обробки платежу тут
}
}
3. Сервіс інвентаризації
InventoryListener.java
package com.atk.inventory.listeners;
import com.atk.order.events.OrderPlacedEvent;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
public class InventoryListener {
@KafkaListener(topics = "order-events", groupId = "inventory-group")
public void handleOrderPlaced(OrderPlacedEvent event) {
System.out.println("Резервування інвентарю для замовлення: " + event.getOrderId());
// Логіка резервування інвентарю тут
}
}
5. Переваги DDD у мікросервісах
- Відповідність бізнес-доменам: Мікросервіси розробляються навколо реальних бізнес-можливостей.
- Чітко визначені межі сервісів: Обмежені контексти допомагають визначити чіткі обов'язки сервісів.
- Підтримуваність: DDD допомагає розбивати складні системи на зрозумілі компоненти.
- Масштабованість: Мікросервіси з окремими моделями доменів можуть масштабуватись незалежно.
- Стійкість: Події, засновані на подіях, з доменними подіями (Domain Events), забезпечують, що сервіси можуть відновлюватися після збоїв.
6. Виклики DDD у мікросервісах
1.
Збільшена складність
DDD вимагає ретельного проектування та глибокого розуміння бізнес-домену.
2. Навантаження на комунікацію: Мікросервіси потребують надійних механізмів для обробки асинхронної комунікації.
3. Кінцева узгодженість: Обробка кінцевої узгодженості вимагає ретельного тестування та моніторингу.
7. Коли використовувати DDD
DDD є корисним, коли:
- Бізнес-домен складний та включає в себе численні процеси та правила.
- Є часті зміни в бізнес-вимогах.
- Команди працюють тісно з експертами домену для впровадження бізнес-логіки.
Висновок
У середовищі мікросервісів Domain-Driven Design (DDD) надає структурований підхід до проектування сервісів, які відповідають бізнес-домену. Орієнтуючись на обмежені контексти, агрегати, доменні події та всезагальну мову (ubiquitous language), ви можете створити більш підтримувану, масштабовану та стійку систему. Хоча DDD вводить додаткову складність, він значно покращує здатність системи еволюціонувати разом із змінами бізнес-вимог.
Перекладено з: Domain-Driven Design (DDD) in Microservices Environment