مقدمه: دام اشتباه رایج
در دنیای توسعه نرمافزار، یک باور غلط به شدت شایع شده است: “هر Entity باید Repository خودش را داشته باشد.” این تفکر که از معماری سنتی CRUD نشأت میگیرد، وقتی وارد حوزه Domain-Driven Design میشود، به یک فاجعه تبدیل میگردد.
مشکل اساسی چیست؟
تقلیل مدل غنی دامنه به لایه CRUD
وقتی برای هر Entity یک Repository میسازید، در واقع:
· منطق کسبوکار را به عملیات دیتابیسی تقلیل میدهید
· روابط معنادار بین موجودیتها را نادیده میگیرید
· یکپارچگی دادهها را به خطر میاندازید
// ❌ طراحی اشتباه - Repository برای هر Entity
interface IOrderRepository { /* ... */ }
interface IOrderItemRepository { /* ... */ }
interface ICustomerRepository { /* ... */ }
interface IAddressRepository { /* ... */ }
// ✅ طراحی صحیح - Repository فقط برای Aggregate Root
interface IOrderRepository { /* ... */ }
interface ICustomerRepository { /* ... */ }راه حل: قاعده طلایی DDD
هر Aggregate Root یک Repository
Aggregate Root مدلی است که:
· مرزهای یکپارچگی را تعریف میکند
· نقطه ورود به مجموعهای از موجودیتهای مرتبط است
· میتواند مستقیماً و تنها با ID خود لود شود
تست 30 ثانیهای تشخیص Aggregate Root
سؤال کلیدی:
“آیا این شیء میتواند مستقیماً و تنها با ID خود از دیتابیس لود شود، یا برای معنا پیدا کردن نیاز به یک Parent دارد؟”
سناریوهای عملی:
مورد ۱: سفارش (Order) و آیتمهای سفارش (OrderItems)
// ✅ Order یک Aggregate Root است // میتواند مستقیم با OrderId لود شود const order = await orderRepository.getById(orderId); // ❌ OrderItem یک Aggregate Root نیست // بدون Order معنا ندارد - نیاز به Parent دارد // const orderItem = await orderItemRepository.getById(itemId); // اشتباه!
مورد ۲: مشتری (Customer) و آدرسها (Addresses)
// ✅ Customer یک Aggregate Root است const customer = await customerRepository.getById(customerId); // آدرسها از طریق Customer دسترسی مییابند const addresses = customer.getAddresses();
مزایای رعایت این اصل
۱. حفظ یکپارچگی کسبوکار
// Domain Models
class OrderItem {
constructor(
public readonly productId: string,
public readonly price: number,
public readonly quantity: number
) {
if (quantity <= 0) {
throw new Error("Quantity must be positive");
}
}
}
class Order implements AggregateRoot {
private readonly items: OrderItem[] = [];
private status: OrderStatus = OrderStatus.PENDING;
constructor(public readonly id: string) {}
addItem(productId: string, price: number, quantity: number): void {
// منطق کسبوکار در یک مکان متمرکز شده
const item = new OrderItem(productId, price, quantity);
this.items.push(item);
}
getItems(): ReadonlyArray<OrderItem> {
return [...this.items];
}
markAsPaid(): void {
this.status = OrderStatus.PAID;
}
}۲. سادهسازی لایه دیتا اکسس
// Repository Interface
interface AggregateRoot {
id: string;
}
interface Repository<T extends AggregateRoot> {
getById(id: string): Promise<T | null>;
save(aggregate: T): Promise<void>;
delete(id: string): Promise<void>;
}
// Order Repository Implementation
class OrderRepository implements Repository<Order> {
async getById(orderId: string): Promise<Order | null> {
// لود کردن کامل Aggregate در یک کوئری
const orderData = await db.orders
.findOne({
_id: orderId
})
.populate('items');
if (!orderData) return null;
return this.mapToDomain(orderData);
}
async save(order: Order): Promise<void> {
await db.orders.findOneAndUpdate(
{ _id: order.id },
this.mapToPersistence(order),
{ upsert: true }
);
}
private mapToDomain(data: any): Order {
const order = new Order(data._id);
// بازسازی وضعیت دامنه از دادههای persistence
data.items.forEach((item: any) => {
order.addItem(item.productId, item.price, item.quantity);
});
return order;
}
}۳. بهبود Performance
· کاهش تعداد کوئریهای دیتابیس
· جلوگیری از N+1 Query Problem
· بارگذاری کامل Aggregate در یک درخواست
پیامدهای نقض این اصل
۱. آنمی دامنه (Domain Anemia)
// ❌ مدلهای ضعیف بدون منطق کسبوکار
interface Order {
id: string;
items: OrderItem[];
}
interface OrderItem {
id: string;
productId: string;
quantity: number;
price: number;
}
// منطق کسبوکار پراکنده در سرویسها
class OrderService {
constructor(
private orderRepository: IOrderRepository,
private orderItemRepository: IOrderItemRepository, // ❌ اشتباه!
private productRepository: IProductRepository
) {}
async addItemToOrder(orderId: string, productId: string, quantity: number): Promise<void> {
const order = await this.orderRepository.getById(orderId);
const product = await this.productRepository.getById(productId);
// منطق در سرویس، نه در دامنه
if (quantity <= 0) throw new Error("Quantity must be positive");
const item: OrderItem = {
id: generateId(),
productId,
quantity,
price: product.price
};
await this.orderItemRepository.save(item); // ❌ نقض یکپارچگی
}
}۲. مشکلات یکپارچگی دادهها
// ❌ احتمال ناسازگاری دادهها const order = await orderRepository.getById(orderId); const items = await orderItemRepository.getByOrderId(orderId); // اگر items جداگانه لود شوند، ممکن است با order همگام نباشند
۳. پیچیدگی غیرضروری
· Repository های متعدد برای مدیریت
· تراکنشهای پیچیده برای حفظ یکپارچگی
· رابطهای کاربری پیچیدهتر
الگوهای پیادهسازی صحیح
۱. طراحی بر اساس محدودههای کسبوکار
// Base Interfaces
interface AggregateRoot {
id: string;
}
interface Repository<T extends AggregateRoot> {
getById(id: string): Promise<T | null>;
save(aggregate: T): Promise<void>;
delete(id: string): Promise<void>;
}
// Order Aggregate
class Order implements AggregateRoot {
constructor(
public readonly id: string,
private customerId: string,
private items: OrderItem[] = [],
private status: OrderStatus = OrderStatus.PENDING
) {}
addItem(productId: string, price: number, quantity: number): void {
const existingItem = this.items.find(item => item.productId === productId);
if (existingItem) {
existingItem.increaseQuantity(quantity);
} else {
this.items.push(new OrderItem(productId, price, quantity));
}
}
getTotal(): number {
return this.items.reduce((total, item) => total + item.getTotal(), 0);
}
// سایر متدهای دامنه...
}
// Order Repository
interface IOrderRepository extends Repository<Order> {
findByCustomerId(customerId: string): Promise<Order[]>;
findPendingOrders(): Promise<Order[]>;
}
class MongoDBOrderRepository implements IOrderRepository {
async getById(id: string): Promise<Order | null> {
const data = await db.collection('orders').findOne({ _id: id });
return data ? this.mapToDomain(data) : null;
}
async save(order: Order): Promise<void> {
const data = this.mapToPersistence(order);
await db.collection('orders').updateOne(
{ _id: order.id },
{ $set: data },
{ upsert: true }
);
}
private mapToDomain(data: any): Order {
const order = new Order(data._id, data.customerId);
// بازسازی وضعیت دامنه
data.items.forEach((item: any) => {
order.addItem(item.productId, item.price, item.quantity);
});
return order;
}
}۲. استفاده از Specification Pattern
// Specification Pattern
interface Specification<T> {
isSatisfiedBy(candidate: T): boolean;
and(other: Specification<T>): Specification<T>;
or(other: Specification<T>): Specification<T>;
}
class OrderStatusSpecification implements Specification<Order> {
constructor(private readonly status: OrderStatus) {}
isSatisfiedBy(order: Order): boolean {
return order.getStatus() === this.status;
}
and(other: Specification<Order>): Specification<Order> {
return new AndSpecification(this, other);
}
or(other: Specification<Order>): Specification<Order> {
return new OrSpecification(this, other);
}
}
// Repository با قابلیت Specification
interface ISpecificationRepository<T extends AggregateRoot> extends Repository<T> {
findBySpecification(spec: Specification<T>): Promise<T[]>;
}مطالعه موردی: سیستم فروشگاه آنلاین
طراحی صحیح Aggregate Roots:
// ✅ Aggregate Roots
class Product implements AggregateRoot {
constructor(
public readonly id: string,
private name: string,
private price: number,
private stock: number
) {}
decreaseStock(quantity: number): void {
if (this.stock < quantity) {
throw new Error('Insufficient stock');
}
this.stock -= quantity;
}
}
class Customer implements AggregateRoot {
constructor(
public readonly id: string,
private email: string,
private addresses: CustomerAddress[] = []
) {}
addAddress(address: CustomerAddress): void {
if (this.addresses.length >= 5) {
throw new Error('Cannot add more than 5 addresses');
}
this.addresses.push(address);
}
}
class Order implements AggregateRoot {
// implementation as above
}
// ❌ اینها Aggregate Root نیستند
class OrderItem {
// بخشی از Order
}
class CustomerAddress {
// بخشی از Customer
}
class CartItem {
// بخشی از ShoppingCart
}سرویسهای دامنه با طراحی صحیح:
class OrderApplicationService {
constructor(
private orderRepository: IOrderRepository,
private productRepository: IProductRepository,
private customerRepository: ICustomerRepository
) {}
async createOrder(customerId: string, items: Array<{productId: string, quantity: number}>): Promise<string> {
const customer = await this.customerRepository.getById(customerId);
if (!customer) {
throw new Error('Customer not found');
}
const order = new Order(generateId(), customerId);
for (const item of items) {
const product = await this.productRepository.getById(item.productId);
if (!product) {
throw new Error(`Product ${item.productId} not found`);
}
order.addItem(product.id, product.getPrice(), item.quantity);
product.decreaseStock(item.quantity);
await this.productRepository.save(product);
}
await this.orderRepository.save(order);
return order.id;
}
}جمعبندی
قوانین طلایی:
- هر Aggregate Root فقط یک Repository
- موجودیتهای child نباید Repository مستقل داشته باشند
- همیشه از طریق Aggregate Root به موجودیتهای داخلی دسترسی پیدا کنید
- تغییرات را در سطح Aggregate commit کنید
پرسشهای باز برای تیمتان:
· چه Repository هایی در پروژه فعلی دارید که برای non-Aggregate Root ساخته شدهاند؟
· این طراحی چه مشکلاتی ایجاد کرده است؟
· چگونه میتوانید طراحی را refactor کنید؟
// نمونه refactoring
// قبل:
interface IOrderItemRepository { // ❌
getById(id: string): Promise<OrderItem>;
save(item: OrderItem): Promise<void>;
}
// بعد:
interface IOrderRepository { // ✅
getById(id: string): Promise<Order>;
save(order: Order): Promise<void>;
// OrderItem از طریق Order مدیریت میشود
}این اصل ساده اما قدرتمند میتواند تفاوت بین یک معماری قابل نگهداری و یک کدبیس پیچیده و شکننده را ایجاد کند. با تمرین و درک عمیق مفاهیم DDD، میتوانید از این اشتباه رایج اجتناب کنید.