Domain-Driven Design (DDD, предметно-орієнтоване проєктування) — це підхід до розробки програмного забезпечення, який зосереджується на моделюванні бізнес-логіки на основі реального домену (предметної області). Його запропонував Ерік Еванс у своїй книзі "Domain-Driven Design: Tackling Complexity in the Heart of Software".
Основні принципи DDD
- Фокус на домені – головна увага приділяється предметній області, а не технічним деталям.
- Єдина мова (Ubiquitous Language) – розробники, бізнес-аналітики та інші учасники проєкту використовують спільну термінологію, щоб уникнути непорозумінь.
- Бізнес-логіка відокремлена від технічної реалізації – код моделюється так, щоб він чітко відображав реальний бізнес-процес.
Основні концепції DDD
- Entity (Сутність) – об’єкт з унікальним ідентифікатором, що зберігається в системі (наприклад, Користувач, Замовлення).
- Value Object (Об’єкт-значення) – об’єкт, який не має унікального ідентифікатора та є незмінним (наприклад, Адреса або Гроші).
- Aggregate (Агрегат) – група сутностей та об'єктів-значень, що складають логічне ціле з єдиною точкою доступу (кореневою сутністю).
- Repository (Репозиторій) – клас, що відповідає за отримання та збереження агрегатів у базі даних.
- Service (Сервіс домену) – містить бізнес-логіку, яка не може бути розміщена всередині окремої сутності або агрегату.
- Factory (Фабрика) – використовується для створення складних об'єктів, щоб уникнути складної логіки створення в самому коді бізнес-логіки.
- Domain Event (Доменна подія) – повідомлення про те, що в домені сталася певна важлива подія.
Агрегати в DDD
Основні характеристики агрегату
- Кореневий об'єкт (Aggregate Root) – головна сутність, через яку взаємодіють із агрегатом.
- Інкапсуляція – агрегат приховує внутрішню логіку та правила інваріантів.
- Консистентність – операції над агрегатом гарантують узгоджений стан його складових.
- Обмеження доступу – зовнішній код може працювати лише з коренем агрегату, а не з його підлеглими об'єктами напряму.
Приклад реалізації в Java (Spring Boot, JPA)
Припустимо, у нас є система замовлень. Логічно об'єднати такі об'єкти в один агрегат:
- Order (Замовлення) – кореневий об'єкт
- OrderItem (Позиція замовлення) – частина агрегату
- Payment (Платіж) – теж може бути частиною
Всі ці об'єкти разом формують єдину транзакційну одиницю.
Створюємо кореневий об'єкт Order
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String status;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "order", orphanRemoval = true)
private List<OrderItem> items = new ArrayList<>();
protected Order() {}
public Order(String status) {
this.status = status;
}
public void addItem(String productName, int quantity, double price) {
items.add(new OrderItem(this, productName, quantity, price));
}
public void removeItem(OrderItem item) {
items.remove(item);
}
public void completeOrder() {
if (items.isEmpty()) {
throw new IllegalStateException("Замовлення не може бути завершене без товарів");
}
this.status = "COMPLETED";
}
// Гетери та сетери
}
Створюємо підлеглий об'єкт OrderItem
import jakarta.persistence.*;
@Entity
@Table(name = "order_items")
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productName;
private int quantity;
private double price;
@ManyToOne
@JoinColumn(name = "order_id", nullable = false)
private Order order;
protected OrderItem() {}
public OrderItem(Order order, String productName, int quantity, double price) {
this.order = order;
this.productName = productName;
this.quantity = quantity;
this.price = price;
}
// Гетери та сетери
}
Репозиторій для збереження агрегату:
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderRepository extends JpaRepository<Order, Long> {
}
Сервіс для роботи з агрегатом
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional
public void createOrderWithItems() {
Order order = new Order("NEW");
order.addItem("Ноутбук", 1, 1500.00);
order.addItem("Мишка", 1, 50.00);
orderRepository.save(order);
}
@Transactional
public void completeOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("Order not found"));
order.completeOrder();
orderRepository.save(order);
}
}
Коментарі
Дописати коментар