יישום Domain-Driven Design (DDD) לאפליקציות ווב
עבדנו פעם על פרויקט שבו הלוגיקה העסקית הייתה מפוזרת בין קונטרולרים ושירותים. כל שינוי דרש שעות של חיפוש אחר מי שמעדכן את סטטוס ההזמנה ואיפה. DDD פתר זאת בכך שהפך את הקוד להשתקפות של תהליכים עסקיים. להלן הניסיון שלנו ביישום DDD לאפליקציות ווב עם דוגמאות ב-TypeScript.
Domain-Driven Design היא מתודולוגיית עיצוב תוכנה שבה הארכיטקטורה של המערכת משקפת את התחום העסקי. הקוד מדבר באותה שפה כמו מומחי התחום. לא 'עדכן את הרשומה בטבלת המשתמשים', אלא 'חסום את החשבון עקב הפרת מדיניות'. DDD מוצדק לתחומים מורכבים—חנויות אונליין עם חוקי תמחור לא טריוויאליים, מערכות פיננסיות, SaaS עם תעריפים גמישים. יישמנו DDD בפרויקטים הדורשים שלמות גבוהה ושינויים תכופים. למהנדסים שלנו יש ניסיון של 7+ שנים בפרויקטי DDD והם ביצעו למעלה מ-40 יישומים עבור לקוחות בתעשיות שונות. Domain-Driven Design אינו פתרון קסם, אלא כלי חזק למערכות מורכבות.
מתי DDD הכרחי
אם הפרויקט שלך מעבד אלפי הזמנות עם חוקי הנחה מותאמים אישית, מסים ואילוצים—DDD עוזר לבנות סדר בכאוס. לדוגמה, בפרויקט אחד צמצמנו את הזמן לשינוי לוגיקת תמחור פי 3, ומספר השגיאות במהלך שחרורים ירד ב-70%.
ניהול מורכבות עם DDD
DDD מציג גבולות ברורים—Bounded Context. בתוך כל הקשר, למונחים יש משמעות מדויקת. לדוגמה, 'מוצר' בקטלוג הוא תיאור ותמונה, בעוד שבהזמנות הוא SKU ומחיר. הקשרים מתקשרים דרך שכבת אנטי-שחיתות. זה מבודד שינויים ומאיץ פיתוח. Eric Evans, Domain-Driven Design מדגיש ש-Bounded Context הוא המפתח לניהול מורכבות.
DDD לעומת CRUD
| היבט | CRUD מסורתי | DDD |
|---|---|---|
| שלמות נתונים | מפוזרת בין שירותים | Aggregate מבטיח אינווריאנטים |
| מהירות שינויים | חיפוש ידני של כל המקומות | שינוי רק ב-aggregate אחד |
| מורכבות בדיקות | הרבה mocks | בדיקות תחום מבודדות |
| זמן לתכונה חדשה | יום עד שבוע | שעתיים עד יומיים (פי 2–3 מהר יותר) |
תהליך יישום DDD
- זהה את ה-Bounded Context המורכב ביותר (למשל, ניהול הזמנות).
- הגדר את השפה האוניברסלית עם מומחי עסקים—תעד את כל המונחים.
- תכנן aggregates ו-value objects, וקבע אינווריאנטים.
- כתוב בדיקות יחידה ללוגיקת התחום (כיסוי של 90%+).
- שלב בין הקשרים דרך אירועי תחום ושכבת אנטי-שחיתות.
מקרה בוחן: חנות אלקטרוניקה אונליין
בפרויקט מכירת אלקטרוניקה, בודדנו את הקשר 'ניהול הזמנות'. המורכבות הייתה בחוקי הנחות ומבצעים גמישים. תכננו את ה-Order aggregate עם אינווריאנטים (אין הוספת פריט לאחר משלוח). בדיקות יחידה כיסו 95% מהתרחישים. זמן הפיתוח למבצעים חדשים ירד משבוע ליום אחד.למה להכניס DDD בהדרגה?
DDD דורש משמעת. אל תכסה את כל המערכת בבת אחת. התחל עם ההקשר המורכב ביותר—הוא מניב תוצאות מהירות והפחתה של 60% בשגיאות באזור זה. בהדרגה, מספר הבאגים יורד ב-40–50% בהשוואה ל-CRUD. אנו ממליצים לזהות 2–3 aggregates מרכזיים ולבנות את שאר הארכיטקטורה סביבם. הלקוחות שלנו חוסכים עד 40% מתקציב התחזוקה לאחר אימוץ DDD.
אילו בעיות DDD פותר?
הבעיה המרכזית היא לוגיקה עסקית מפוזרת שבה שינויים נוגעים בשירותים רבים. DDD מציג גבולות הקשר ברורים ומכיל חוקים בתוך aggregates. זה מפחית את עלות השינוי ב-30–50% ומאיץ את אספקת התכונות החדשות.
אבני בניין
Entity
אובייקט עם זהות שנשמרת גם כשהתכונות משתנות:
class Order { private readonly _id: OrderId; private _status: OrderStatus; private _items: OrderItem[] = []; private _domainEvents: DomainEvent[] = []; constructor(id: OrderId, customerId: CustomerId) { this._id = id; this._status = OrderStatus.Draft; this.raise(new OrderCreatedEvent(id, customerId)); } addItem(product: Product, quantity: Quantity): void { if (this._status !== OrderStatus.Draft) { throw new OrderNotEditableError(this._id); } if (quantity.isZero()) { throw new InvalidQuantityError(); } const existing = this._items.find(i => i.productId.equals(product.id)); if (existing) { existing.increaseQuantity(quantity); } else { this._items.push(new OrderItem(product.id, product.price, quantity)); } } submit(): void { this.ensureCanTransitionTo(OrderStatus.Submitted); if (this._items.length === 0) throw new EmptyOrderError(); this._status = OrderStatus.Submitted; this.raise(new OrderSubmittedEvent(this._id, this.calculateTotal())); } } Value Object
אובייקט ללא זהות, המוגדר על ידי הערכים שלו. בלתי ניתן לשינוי:
class Money { private constructor( private readonly _amount: number, private readonly _currency: Currency ) { if (_amount < 0) throw new NegativeAmountError(); } static of(amount: number, currency: Currency): Money { return new Money(amount, currency); } add(other: Money): Money { if (!this._currency.equals(other._currency)) { throw new CurrencyMismatchError(); } return new Money(this._amount + other._amount, this._currency); } } Domain Service
פעולה שאינה שייכת לאף entity:
class OrderPricingService { constructor( private discountRepo: DiscountRepository, private taxService: TaxCalculationService ) {} async calculateTotal(order: Order, customer: Customer): Promise<PricingResult> { const discounts = await this.discountRepo.findApplicable( customer.segment, order.items ); let subtotal = order.items.reduce( (sum, item) => sum.add(item.price.multiply(item.quantity.value)), Money.zero(Currency.USD) ); const discountAmount = this.applyDiscounts(subtotal, discounts, customer); const taxAmount = await this.taxService.calculate(subtotal, customer.address); return new PricingResult(subtotal, discountAmount, taxAmount); } } Repository
אבסטרקציה לאחסון aggregates:
interface OrderRepository { findById(id: OrderId): Promise<Order | null>; findByCustomer(customerId: CustomerId, options?: FindOptions): Promise<Order[]>; save(order: Order): Promise<void>; } class PostgresOrderRepository implements OrderRepository { async findById(id: OrderId): Promise<Order | null> { const row = await this.db.queryOne( 'SELECT * FROM orders WHERE id = $1', [id.value] ); return row ? this.toDomain(row) : null; } } שכבת היישום
שכבה דקה שמתזמנת אובייקטי תחום:
class PlaceOrderUseCase { async execute(dto: PlaceOrderDto): Promise<PlaceOrderResult> { const customer = await this.customerRepo.findById( CustomerId.from(dto.customerId) ); if (!customer) throw new CustomerNotFoundError(dto.customerId); const order = new Order(OrderId.generate(), customer.id); for (const item of dto.items) { const product = await this.productRepo.findById(ProductId.from(item.productId)); order.addItem(product, Quantity.of(item.quantity)); } const pricing = await this.pricingService.calculateTotal(order, customer); order.applyPricing(pricing); order.submit(); await this.orderRepo.save(order); await this.eventBus.publishAll(order.pullDomainEvents()); return { orderId: order.id.value, total: order.total }; } } לוחות זמנים ועלות
| שלב | משך |
|---|---|
| ניתוח תחום ומפת הקשרים | 1–2 שבועות |
| עיצוב aggregates | 1–2 שבועות |
| יישום (3–5 aggregates) | 3–5 שבועות |
| אינטגרציה ובדיקות | 2–4 שבועות |
| סה"כ | 2 עד 4 חודשים |
העלות מחושבת באופן אישי ותלויה במורכבות התחום. בממוצע, אימוץ DDD מחזיר את עצמו תוך 6–12 חודשים באמצעות הפחתת עלויות תחזוקה ופיתוח מהיר יותר של תכונות חדשות.
מה מקבלים כתוצאה
| תוצאה | תיאור |
|---|---|
| תיעוד תחום | תיאור שפה אוניברסלית, מפת bounded contexts |
| Aggregates מיושמים | קוד מוכן עם חוקים עסקיים |
| בדיקות יחידה | כיסוי של 90%+ |
| הכשרת צוות | סדנה על DDD ועבודה עם הקוד |
| תמיכה לאחר שחרור | שבועיים לאחר היישום |
טעויות נפוצות באימוץ DDD
- ניסיון לכסות את כל המערכת בבת אחת—התחל עם ההקשר המורכב ביותר.
- הזנחת השפה האוניברסלית—ללא שפה משותפת, הצוות יתבלבל במונחים.
- התעלמות מבדיקות—ללא בדיקות יחידה, הנקודה של בידוד הלוגיקה אובדת.
הזמינו ייעוץ להערכת הפרויקט שלכם—נבחר את היקף היישום האופטימלי ונתכנן מפת דרכים. צרו קשר לבדיקה חינמית של הארכיטקטורה הנוכחית שלכם. המהנדסים שלנו מבטיחים איכות ועמידה בלוחות זמנים.







