طراحی مبتنی بر دامنه (Domain-Driven Design – DDD) یک فناوری یا فریمورک نیست. این یک چگونه اندیشیدن (Mindset) و مجموعهای از اولویتها برای پروژههای نرمافزاری پیچیده است. ایده اصلی آن همتراز کردن ساختار کد شما با «دامنه کسبوکار» (Business Domain) است تا یک زبان همهجاگیر (Ubiquitous Language) ایجاد کند که هم توسعهدهندگان و هم متخصصان دامنه کسبوکار بتوانند آن را درک کنند.
۱. تصویر کلان: محدودههای مستقل و الگوی راهبردی (Bounded Contexts & Strategic Pattern)
مهمترین مفهوم در DDD، «محدوده مستقل» (Bounded Context) است. یک سیستم بزرگ (مثلاً یک پلتفرم تجارت الکترونیک) یک مدل یکپارچه و عظیم نیست. بلکه مجموعهای از مدلهای کوچکتر و دقیق است که هر کدام مرزهای مشخصی دارند.
نقشه مفهومی:
شرح تصویر:
- محدوده مستقل (فروش): این محدوده به ایجاد سفارش، محاسبه کل مبلغ و پردازش پرداخت مربوط میشود. در اینجا، یک
Customer(مشتری) درباره آدرس صورتحساب و تاریخچه سفارشاتش است. - محدوده مستقل (حمل و نقل): این محدوده به تحویل بستهها مربوط میشود. در اینجا، یک
Customer(مشتری) درباره آدرس تحویل و زمانهای ترجیحی حملش است. - یک کلمه، معانی مختلف: کلمه “Customer” در محدودههای مختلف معانی مختلفی دارد. DDD این را میپذیرد! ما یک کلاس منفرد، عظیم و مبهم
Customerبرای کل شرکت ایجاد نمیکنیم. ما یکSales::Customerو یکShipping::Customerداریم. - کلهای تجمعی (Aggregates): هر محدوده شامل کلهای تجمعی (خوشههایی از اشیاء که به عنوان یک واحد扱ه میشوند) است.
Orderریشه یک کل تجمعی است وShipmentریشه کل تجمعی دیگری است.
۲. بلوکهای سازنده تاکتیکی: مثالهای جاوا
بیایید محدوده مستقل فروش (Sales) را از diagram بالا با کدهای جاوا جزئیتر کنیم.
الف. شیء ارزش (Value Object – VO): Address
اشیاء ارزش تغییرناپذیر هستند، ویژگیها را توصیف میکنند و هویت مفهومی ندارند. دو شیء ارزش در صورتی برابر هستند که فیلدهایشان یکسان باشد.
// Value Object (شیء ارزش): تغییرناپذیر و هویت ندارد، فقط ارزش هایش مهم هستند.
public final class Address {
private final String street;
private final String city;
private final String postalCode;
public Address(String street, String city, String postalCode) {
this.street = street;
this.city = city;
this.postalCode = postalCode;
}
// اشیاء ارزش باید تغییرناپذیر باشند. فقط متدهای گیرنده (getter) ارائه دهید.
public String getStreet() { return street; }
public String getCity() { return city; }
public String getPostalCode() { return postalCode; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(street, address.street) &&
Objects.equals(city, address.city) &&
Objects.equals(postalCode, address.postalCode);
}
@Override
public int hashCode() {
return Objects.hash(street, city, postalCode);
}
}ب. موجودیت (Entity): Customer
موجودیتها، اشیاء تغییرپذیری هستند که با هویتشان (مثلاً یک ID) تعریف میشوند، نه با ویژگیهایشان. ویژگیهای آنها میتواند تغییر کند، اما هویتشان ثابت میماند.
// Entity (موجودیت): تغییرپذیر و با هویت (id) خود تعریف میشود.
public class Customer {
// هویت برای یک موجودیت کلیدی است
private final CustomerId id; // یک شیء ارزش دیگر برای typing قوی
private String name;
private Address address; // از شیء ارزش استفاده میکند
private String email;
public Customer(CustomerId id, String name, Address address, String email) {
this.id = id;
this.name = name;
this.address = address;
this.email = email;
}
// رفتارهای درون موجودیت، قواعد کسبوکار را反映 میکنند
public void updateAddress(Address newAddress) {
// میتوان منطق دامنه را اینجا اضافه کرد، مثلاً اعتبارسنجی قالب آدرس
this.address = newAddress;
}
public void changeEmail(String newEmail) {
// اعتبارسنجی ساده
if (newEmail == null || !newEmail.contains("@")) {
throw new IllegalArgumentException("آدرس ایمیل نامعتبر است");
}
this.email = newEmail;
}
// متدهای گیرنده (Getters)
public CustomerId getId() { return id; }
public String getName() { return name; }
public Address getAddress() { return address; }
public String getEmail() { return email; }
// برابری فقط بر اساس هویت تعیین میشود
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Customer customer = (Customer) o;
return Objects.equals(id, customer.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}ج. کل تجمعی و ریشه تجمع (Aggregate & Aggregate Root): Order
یک کل تجمعی (Aggregate) خوشهای از اشیاء مرتبط (موجودیتها و اشیاء ارزش) است که برای تغییرات داده به عنوان یک واحد واحد در نظر گرفته میشود. ریشه تجمع (Aggregate Root) موجودیت واحدی است که از یکپارچگی کل کل تجمعی محافظت میکند.
// Aggregate Root (ریشه تجمع): نقطه ورود برای تمام عملیات روی کل تجمعی.
public class Order {
// هویت برای موجودیت ریشه
private final OrderId id;
private final CustomerId customerId; // ارجاع به کل تجمعی مشتری با ID، نه شیء!
private OrderStatus status;
private Money total; // شیء ارزش برای پول
private final List<OrderLineItem> lineItems; // لیستی از موجودیت های فرزند درون کل تجمعی
public Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.CREATED;
this.total = new Money(0.0, "USD"); // برای سادگی USD فرض شده
this.lineItems = new ArrayList<>();
}
// منطق اصلی دامنه: افزودن یک آیتم به سفارش
public void addItem(Product product, int quantity) {
// 1. بررسی invariant کسبوکار: نمیتوان به سفارش تکمیل شده آیتم اضافه کرد
if (this.status != OrderStatus.CREATED) {
throw new IllegalStateException("نمیتوان یک سفارش تکمیل شده را تغییر داد.");
}
// 2. بررسی میکند که محصول از قبل در سفارش موجود است یا خیر
for (OrderLineItem item : lineItems) {
if (item.getProductId().equals(product.getId())) {
item.increaseQuantity(quantity);
recalculateTotal();
return;
}
}
// 3. اگر نه، یک آیتم جدید ایجاد میکند
OrderLineItem newItem = new OrderLineItem(product.getId(), product.getPrice(), quantity);
lineItems.add(newItem);
recalculateTotal();
}
// رفتار: علامت گذاری سفارش به عنوان پرداخت شده
public void confirmPayment() {
if (this.status != OrderStatus.CREATED) {
throw new IllegalStateException("سفارش از قبل پردازش شده است.");
}
this.status = OrderStatus.PAID;
// رویداد دامنه میتواند在这里 انتشار یابد: OrderPaidEvent(this.id)
}
// یک متد خصوصی برای اعمال consistency درون کل تجمعی
private void recalculateTotal() {
Money newTotal = new Money(0.0, "USD");
for (OrderLineItem item : lineItems) {
newTotal = newTotal.add(item.getPrice().multiply(item.getQuantity()));
}
this.total = newTotal;
}
// متدهای گیرنده (Getters)
public OrderId getId() { return id; }
public CustomerId getCustomerId() { return customerId; }
public OrderStatus getStatus() { return status; }
public Money getTotal() { return total; }
public List<OrderLineItem> getLineItems() { return Collections.unmodifiableList(lineItems); }
}
// موجودیت فرزند درون کل تجمعی سفارش
class OrderLineItem {
private final ProductId productId;
private final Money price;
private int quantity;
public OrderLineItem(ProductId productId, Money price, int quantity) {
this.productId = productId;
this.price = price;
this.quantity = quantity;
}
public void increaseQuantity(int amount) {
this.quantity += amount;
}
// متدهای گیرنده (Getters)
public ProductId getProductId() { return productId; }
public Money getPrice() { return price; }
public int getQuantity() { return quantity; }
}د. مخزن (Repository): OrderRepository
مخازن (Repositories) مانند مجموعههای در حافظه برای کلهای تجمعی هستند. آنها جزئیات ذخیرهسازی داده را پنهان میکنند. شما ریشه یک کل تجمعی را با شناسه آن درخواست میکنید و کل ریشه تجمع را دوباره ذخیره میکنید.
// رابط مخزن (Repository Interface): در لایه دامنه تعریف شده است.
public interface OrderRepository {
// فقط با ریشه های تجمعی (Order) کار می کند، نه با اشیاء درونی (OrderLineItem)
Optional<Order> findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId);
void save(Order order); // کل کل تجمعی را ذخیره یا به روز می کند
void delete(OrderId id);
}
// پیاده سازی در لایه زیرساخت (Infrastructure Layer) (مثلاً با استفاده از JPA)
// این کلاس در یک پکیج جداگانه 'infrastructure' قرار می گیرد و رابط دامنه را پیاده سازی می کند.
@Service
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Optional<Order> findById(OrderId id) {
// کل تجمعی Order را از پایگاه داده مپ کرده و برمی گرداند
Order order = entityManager.find(Order.class, id);
return Optional.ofNullable(order);
}
@Override
public void save(Order order) {
// کل کل تجمعی Order و فرزندانش را ذخیره (persist) یا ادغام (merge) می کند
entityManager.merge(order);
}
// ... سایر متدها
}ه. سرویس دامنه (Domain Service): OrderPlacementService
برخی از مفاهیم دامنه به طور طبیعی به عنوان مسئولیت یک موجودیت یا شیء ارزش نمیگنجند. یک سرویس دامنه (Domain Service) یک کلاس بدون حالت (stateless) است که این منطق دامنه را کپسوله میکند.
// Domain Service (سرویس دامنه): کلاس بدون حالتی که منطق دامنه پیچیده را مدیریت می کند.
@Service
public class OrderPlacementService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService; // یک سرویس دامنه دیگر
public OrderPlacementService(OrderRepository orderRepository, InventoryService inventoryService) {
this.orderRepository = orderRepository;
this.inventoryService = inventoryService;
}
// این عملیات به خود کل تجمعی Order تعلق ندارد
// زیرا شامل چندین کل تجمعی و قواعد خارجی می شود.
public OrderId placeOrder(CustomerId customerId, List<OrderRequest> requests) {
// 1. موجودی انبار را برای تمامی آیتم ها بررسی می کند (منطق پیچیده خارج از یک کل تجمعی منفرد)
for (OrderRequest request : requests) {
if (!inventoryService.isItemInStock(request.getProductId(), request.getQuantity())) {
throw new IllegalArgumentException("آیتم " + request.getProductId() + " موجود نیست.");
}
}
// 2. کل تجمعی سفارش را ایجاد و پر می کند
OrderId newOrderId = orderRepository.nextId();
Order order = new Order(newOrderId, customerId);
for (OrderRequest request : requests) {
// در یک برنامه واقعی، کل تجمعی Product را با استفاده از مخزن آن fetch می کنید
Product product = inventoryService.getProduct(request.getProductId());
order.addItem(product, request.getQuantity());
}
// 3. کل تجمعی را ذخیره می کند (persist)
orderRepository.save(order);
// 4. احتمالاً یک رویداد دامنه (Domain Event) منتشر می کند تا فرآیندهای دیگر را راه اندازی کند (مثلاً به روزرسانی موجودی، ارسال تاییدیه)
// domainEventPublisher.publish(new OrderCreatedEvent(newOrderId));
return newOrderId;
}
}خلاصه: چرا DDD؟
| مفهوم (Concept) | هدف (Purpose) | مثال جاوا (Java Example) |
|---|---|---|
| شیء ارزش (Value Object – VO) | نمایش مفاهیم توصیفی و تغییرناپذیر. | Address, Money |
| موجودیت (Entity) | نمایش اشیاء دارای چرخه زندگی و هویت. | Customer, Order (Root) |
| کل تجمعی (Aggregate) | خوشهای از اشیاء که به عنوان یک واحد扱ه میشوند. مرزهای consistency را تعریف میکند. | Order (ریشه) + OrderLineItem (فرزند) |
| ریشه تجمع (Aggregate Root) | نقطه ورود واحد به یک کل تجمعی. | Order |
| مخزن (Repository) | بازیابی و ذخیره کلهای تجمعی، abstracting persistence. | OrderRepository |
| سرویس دامنه (Domain Service) | قرار دادن منطقی که به یک موجودیت/VO تعلق ندارد. | OrderPlacementService |
| محدوده مستقل (Bounded Context) | یک مرز explicit که در آن یک مدل دامنه اعمال می شود. | Sales Context, Shipping Context |
با استفاده از این الگوها، شما یک پایگاه کد (codebase) ایجاد میکنید که:
- بیانگر (Expressive) است: کد مانند دامنه کسبوکار خوانده میشود.
- قابل نگهداری (Maintainable) است: تغییرات به محدودههای مستقل و کلهای تجمعی خاص محدود میشوند.
- قابل آزمون (Testable) است: منطق دامنه در اشیاء خالص (pure) جدا شده و به راحتی unit test میشوند.
- انعطافپذیر (Flexible) است: هسته دامنه از زیرساخت جدا شده و تطبیق آن آسانتر است.