Skip to content
~/sph.sh

Domain-Driven Design: Giriş ve Temeller

Domain-Driven Design'a kapsamlı giriş - temel kavramlar, yapı taşları, stratejik desenler ve DDD'yi yazılım geliştirmede ne zaman ve nasıl uygulayacağınıza dair pratik rehber

Özet

Domain-Driven Design (DDD), kod yapısını business domain mantığıyla hizalayarak karmaşık yazılım sistemleri oluşturmanın stratejik bir yaklaşımıdır. Bu rehber, DDD'nin temel kavramlarını, yapı taşlarını ve stratejik desenlerini, bu prensipleri ne zaman ve nasıl etkili bir şekilde uygulayacağınızı gösteren pratik TypeScript örnekleriyle birlikte inceliyor.

Domain-Driven Design Nedir?

Domain-Driven Design, Eric Evans tarafından 2003'te tanıtılan, teknik uzmanlar ve domain uzmanları arasındaki işbirliğini vurgulayan bir yazılım geliştirme yaklaşımıdır. Ana fikir: kodunuz, hizmet ettiği business domain'i yansıtmalı ve domain uzmanlarının kullandığı dil ve kavramları kullanmalıdır.

DDD ile çalışmak bana bunun sadece teknik pattern'lerle ilgili olmadığını öğretti—geliştiriciler ve business paydaşları arasında ortak bir anlayış oluşturmakla ilgili. Ubiquitous Language bu anlayışın taşıyıcısıdır; kod ve domain aynı terimleri kullandığında yanlış anlaşılmalar azalır. Kodunuz business'ınızla aynı dili konuştuğunda, iletişim gelişir ve yazılım daha sürdürülebilir hale gelir.

DDD'nin odaklandığı noktalar:

  • Ubiquitous Language: Geliştiriciler ve domain uzmanları arasında paylaşılan ortak bir kelime dağarcığı
  • Model-Driven Design: Business domain'i yansıtan kod yapısı
  • Bounded Context'ler: Sistemin farklı bölümleri arasındaki net sınırlar
  • Strategic Design: Büyük sistemleri organize etmek için üst düzey pattern'ler
  • Tactical Design: Domain mantığını uygulamak için somut yapı taşları

DDD'yi Ne Zaman Kullanmalı (Ne Zaman Kullanmamalı)

DDD güçlü bir yaklaşım ama evrensel bir çözüm değil. Ne zaman mantıklı olduğuna dair öğrendiklerim.

DDD Kullanın:

Karmaşık Business Logic: Uygulamanız sık değişen karmaşık business kurallarına sahipse, DDD bu karmaşıklığı yönetmeye yardımcı olur. Business mantığının controller'lar ve servisler arasında dağıldığı codebas'ler gördüm - DDD bu kaosa yapı getirir.

Uzun Vadeli Projeler: Yıllarca sürdüreceğiniz sistemler için, DDD modellemesine yapılan ön yatırım karşılığını verir. Açık domain modeli yeni geliştiricilerin adapte olmasını hızlandırır ve değişiklikler sırasında business kurallarını bozma riskini azaltır.

İşbirlikçi Ortamlar: Domain uzmanları mevcut ve işbirliği yapmaya istekliyse, DDD parlıyor. Paylaşılan dil ve model, geleneksel geliştirme yaklaşımlarının başarmakta zorlandığı bir uyum yaratır.

Birden Fazla Bounded Context: Farklı alt alanlara sahip sistemler (örneğin, envanter, ödeme, sevkiyat içeren e-ticaret) DDD'nin sınırları ve ilişkileri yönetmek için stratejik pattern'lerinden faydalanır.

DDD'yi Atlayın:

Basit CRUD Uygulamaları: Minimal business mantığı olan basit bir veri giriş sistemi oluşturuyorsanız, DDD gereksiz karmaşıklık ekler. Temel bir MVC veya katmanlı mimari yeterlidir.

Prototip ve MVP'ler: Hızlı doğrulama projeleri için, DDD'nin modelleme yükü sizi yavaşlatır. Önce geri bildirim alın, proje büyürse DDD'yi düşünün.

Veri Odaklı Sistemler: ETL pipeline'ları, raporlama araçları ve analitik sistemleri genellikle domain modellemesi yerine veri odaklı yaklaşımlarla daha iyi hizmet verir.

Domain Uzmanlığı Olmayan Küçük Ekipler: Domain uzmanlarına erişemiyorsanız veya ekibiniz modelleme yatırımını haklı çıkaramayacak kadar küçükse, daha basit yaklaşımlar daha iyi çalışır.

Temel Yapı Taşları

DDD'nin yapı taşlarını oluşturan tactical pattern'leri inceleyelim. Bunlar kodunuzda kullanacağınız somut implementasyonlar.

Entity'ler

Entity'ler, zaman içinde devam eden benzersiz bir kimliğe sahip nesnelerdir. Aynı veriye sahip ancak farklı ID'lere sahip iki entity farklı nesnelerdir.

typescript
// Entity: Benzersiz kimliğe sahip Userclass User {  private constructor(    private readonly id: string,    private email: string,    private name: string,    private registeredAt: Date  ) {}
  static create(email: string, name: string): User {    // Validation mantığı    if (!email.includes('@')) {      throw new Error('Invalid email format');    }
    return new User(      crypto.randomUUID(),      email,      name,      new Date()    );  }
  static reconstitute(    id: string,    email: string,    name: string,    registeredAt: Date  ): User {    return new User(id, email, name, registeredAt);  }
  changeEmail(newEmail: string): void {    if (!newEmail.includes('@')) {      throw new Error('Invalid email format');    }    this.email = newEmail;  }
  getId(): string {    return this.id;  }
  getEmail(): string {    return this.email;  }
  equals(other: User): boolean {    return this.id === other.id;  }}

Temel özellikler:

  • Kimlik: id alanı kullanıcıyı benzersiz şekilde tanımlar
  • Yaşam Döngüsü: Entity'ler oluşturulur, değiştirilir ve sonunda silinebilir
  • Factory method'lar: Yeni entity'ler için create(), storage'dan yükleme için reconstitute()
  • Business mantığı: changeEmail() gibi method'lar domain kurallarını uygular

Value Object'ler

Value Object'ler kimliği olmayan kavramları temsil eder. Aynı veriye sahip iki value object eşit kabul edilir.

typescript
// Value Object: Email adresiclass Email {  private constructor(private readonly value: string) {}
  static create(email: string): Email {    if (!email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {      throw new Error('Invalid email format');    }    return new Email(email.toLowerCase());  }
  getValue(): string {    return this.value;  }
  equals(other: Email): boolean {    return this.value === other.value;  }
  getDomain(): string {    return this.value.split('@')[1];  }}
// Value Object: Para birimi ile Moneyclass Money {  private constructor(    private readonly amount: number,    private readonly currency: string  ) {}
  static create(amount: number, currency: string): Money {    if (amount < 0) {      throw new Error('Amount cannot be negative');    }    return new Money(amount, currency.toUpperCase());  }
  add(other: Money): Money {    if (this.currency !== other.currency) {      throw new Error('Cannot add money with different currencies');    }    return new Money(this.amount + other.amount, this.currency);  }
  multiply(factor: number): Money {    return new Money(this.amount * factor, this.currency);  }
  equals(other: Money): boolean {    return this.amount === other.amount && this.currency === other.currency;  }
  getAmount(): number {    return this.amount;  }
  getCurrency(): string {    return this.currency;  }}

Value object'ler:

  • Immutable: Setter yok, operasyonlar yeni instance'lar döner
  • Kendi kendini doğrulayan: Veri geçersizse oluşturma başarısız olur
  • Değiştirilebilir: Onları değiştirmezsiniz, yeni instance'larla değiştirirsiniz
  • Değere göre karşılaştırılabilir: Eşitlik kimliğe değil veriye dayalıdır

Aggregate'ler

Aggregate'ler, net bir sınır ve tek bir root entity'ye sahip entity ve value object kümeleridir. Aggregate root tutarlılık kurallarını uygular.

typescript
// Aggregate: OrderItem'ları olan Orderclass OrderItem {  constructor(    private readonly productId: string,    private readonly productName: string,    private readonly price: Money,    private quantity: number  ) {    if (quantity <= 0) {      throw new Error('Quantity must be positive');    }  }
  getTotal(): Money {    return this.price.multiply(this.quantity);  }
  changeQuantity(newQuantity: number): void {    if (newQuantity <= 0) {      throw new Error('Quantity must be positive');    }    this.quantity = newQuantity;  }
  getProductId(): string {    return this.productId;  }
  getQuantity(): number {    return this.quantity;  }}
// Aggregate Rootclass Order {  private items: OrderItem[] = [];  private status: 'draft' | 'confirmed' | 'shipped' | 'cancelled' = 'draft';
  private constructor(    private readonly id: string,    private readonly customerId: string,    private readonly createdAt: Date  ) {}
  static create(customerId: string): Order {    return new Order(crypto.randomUUID(), customerId, new Date());  }
  addItem(productId: string, productName: string, price: Money, quantity: number): void {    if (this.status !== 'draft') {      throw new Error('Cannot modify confirmed order');    }
    // Item zaten var mı kontrol et    const existingItem = this.items.find(item => item.getProductId() === productId);    if (existingItem) {      existingItem.changeQuantity(existingItem.getQuantity() + quantity);    } else {      this.items.push(new OrderItem(productId, productName, price, quantity));    }  }
  removeItem(productId: string): void {    if (this.status !== 'draft') {      throw new Error('Cannot modify confirmed order');    }    this.items = this.items.filter(item => item.getProductId() !== productId);  }
  confirm(): void {    if (this.items.length === 0) {      throw new Error('Cannot confirm empty order');    }    if (this.status !== 'draft') {      throw new Error('Order already confirmed');    }    this.status = 'confirmed';  }
  cancel(): void {    if (this.status === 'shipped') {      throw new Error('Cannot cancel shipped order');    }    this.status = 'cancelled';  }
  getTotal(): Money {    if (this.items.length === 0) {      return Money.create(0, 'USD');    }    return this.items.reduce(      (total, item) => total.add(item.getTotal()),      Money.create(0, 'USD')    );  }
  getId(): string {    return this.id;  }
  getItems(): readonly OrderItem[] {    return [...this.items];  }
  getStatus(): string {    return this.status;  }}

Aggregate prensipleri:

  • Tek Root: Dışarıdan sadece Order referans edilir; OrderItem içseldir
  • Tutarlılık Sınırı: Tüm business kuralları aggregate root tarafından uygulanır
  • Transaction'al: Bir aggregate'e yapılan değişiklikler atomik olarak commit edilmelidir
  • ID ile Referans: Diğer aggregate'ler buna ID ile referans verir, doğrudan nesne referansı ile değil

Repository'ler

Repository'ler, aggregate'lere erişim için bir soyutlama sağlar ve persistence detaylarını gizler.

typescript
// Repository interface'iinterface OrderRepository {  save(order: Order): Promise<void>;  findById(orderId: string): Promise<Order | null>;  findByCustomer(customerId: string): Promise<Order[]>;  delete(orderId: string): Promise<void>;}
// Test için in-memory implementasyonclass InMemoryOrderRepository implements OrderRepository {  private orders = new Map<string, Order>();
  async save(order: Order): Promise<void> {    this.orders.set(order.getId(), order);  }
  async findById(orderId: string): Promise<Order | null> {    return this.orders.get(orderId) || null;  }
  async findByCustomer(customerId: string): Promise<Order[]> {    return Array.from(this.orders.values()).filter(      order => order['customerId'] === customerId    );  }
  async delete(orderId: string): Promise<void> {    this.orders.delete(orderId);  }}
// PostgreSQL implementasyonuclass PostgresOrderRepository implements OrderRepository {  constructor(private db: any) {} // Veritabanı client'ınız
  async save(order: Order): Promise<void> {    await this.db.transaction(async (trx: any) => {      // Order'ı kaydet      await trx('orders').insert({        id: order.getId(),        customer_id: order['customerId'],        status: order.getStatus(),        created_at: order['createdAt']      }).onConflict('id').merge();
      // Order item'ları kaydet      await trx('order_items').where('order_id', order.getId()).delete();
      const items = order.getItems().map(item => ({        order_id: order.getId(),        product_id: item.getProductId(),        quantity: item.getQuantity(),        // ... diğer alanlar      }));
      if (items.length > 0) {        await trx('order_items').insert(items);      }    });  }
  async findById(orderId: string): Promise<Order | null> {    const orderData = await this.db('orders')      .where('id', orderId)      .first();
    if (!orderData) return null;
    const itemsData = await this.db('order_items')      .where('order_id', orderId);
    // Aggregate'i veriden yeniden oluştur    return this.reconstitute(orderData, itemsData);  }
  async findByCustomer(customerId: string): Promise<Order[]> {    const ordersData = await this.db('orders')      .where('customer_id', customerId);
    return Promise.all(      ordersData.map((data: any) => this.findById(data.id))    );  }
  async delete(orderId: string): Promise<void> {    await this.db.transaction(async (trx: any) => {      await trx('order_items').where('order_id', orderId).delete();      await trx('orders').where('id', orderId).delete();    });  }
  private reconstitute(orderData: any, itemsData: any[]): Order {    // Order aggregate'ini veritabanı verisinden yeniden oluştur    // Bu, Order üzerinde statik bir reconstitute method'u kullanır    // Implementation detayları kısalık için atlandı    throw new Error('Not implemented');  }}

Repository pattern'leri:

  • Collection benzeri interface: Bunu in-memory bir collection olarak düşünün
  • Persistence ignorance: Domain katmanı veritabanı detaylarını bilmez
  • Aggregate odaklı: Her aggregate root için bir repository
  • Test edilebilirlik: Test için implementasyonları kolayca değiştirebilirsiniz

Domain Service'ler

Domain service'ler, bir entity veya value object'e doğal olarak uymayan business mantığını içerir. Domain nesneleri üzerinde stateless operasyonlardır.

typescript
// Domain Service: Fiyatlandırma hesaplamasıclass PricingService {  calculateDiscount(order: Order, customer: Customer): Money {    const total = order.getTotal();
    // VIP müşteriler %10 indirim alır    if (customer.isVIP()) {      return total.multiply(0.1);    }
    // 500$'ın üzerindeki siparişler %5 indirim alır    if (total.getAmount() >= 500) {      return total.multiply(0.05);    }
    return Money.create(0, total.getCurrency());  }
  applySeasonalPricing(    basePrice: Money,    season: 'peak' | 'regular' | 'off-peak'  ): Money {    switch (season) {      case 'peak':        return basePrice.multiply(1.3);      case 'off-peak':        return basePrice.multiply(0.7);      default:        return basePrice;    }  }}
// Domain Service: Order fulfillment koordinasyonuclass OrderFulfillmentService {  constructor(    private inventoryService: InventoryService,    private shippingService: ShippingService  ) {}
  async fulfillOrder(order: Order): Promise<void> {    // Envanter doğrulaması    for (const item of order.getItems()) {      const available = await this.inventoryService.checkAvailability(        item.getProductId(),        item.getQuantity()      );
      if (!available) {        throw new Error(`Product ${item.getProductId()} not available`);      }    }
    // Envanter rezervasyonu    for (const item of order.getItems()) {      await this.inventoryService.reserve(        item.getProductId(),        item.getQuantity(),        order.getId()      );    }
    // Sevkiyat ayarla    await this.shippingService.createShipment(order);  }}

Domain service'leri ne zaman kullanmalı:

  • Cross-aggregate operasyonlar: Birden fazla aggregate içeren mantık
  • External sistem koordinasyonu: Birden fazla domain operasyonunu orkestre etme
  • Karmaşık hesaplamalar: Birden fazla entity kullanan ama birine ait olmayan business mantığı
  • Stateless operasyonlar: İç state yok, sadece dönüşümler

Stratejik Design Pattern'leri

Stratejik DDD pattern'leri büyük sistemleri organize etmeye ve karmaşıklığı daha üst düzeyde yönetmeye yardımcı olur.

Ubiquitous Language

Ubiquitous Language, geliştiriciler ve domain uzmanları arasındaki paylaşılan kelime dağarcığıdır. Bu dil kod, konuşmalar, dokümantasyon ve testlerde görünür.

İşte öğrendiğim: kodunuz business paydaşlarınızdan farklı terimler kullandığında, çeviri hataları sızar. Business buna "rezervasyon" derken kodunuz "booking" diyorsa, birisi gereksinimleri yanlış anlayacaktır.

Kötü Örnek (Genel teknik terimler):

typescript
class DataManager {  processRequest(data: any): any {    // "process" business terimleriyle ne anlama geliyor?  }}

İyi Örnek (Ubiquitous Language):

typescript
class ReservationService {  confirmReservation(reservation: Reservation): void {    // Net business operasyonu  }
  cancelReservation(reservationId: string): void {    // Business paydaşları bunu anlar  }}

Pratikte, ubiquitous language oluşturmak şu anlama gelir:

  • Domain uzmanlarıyla işbirlikçi modelleme oturumları
  • Business ve teknoloji arasında paylaşılan terim sözlüğü
  • Kod, doküman ve konuşmalarda tutarlı isimlendirme
  • Anlayış derinleştikçe zaman içinde iyileştirme

Bounded Context'ler

Bounded Context, bir domain modelinin tanımlandığı ve uygulandığı açık bir sınırdır. Farklı context'ler aynı kavram için farklı modellere sahip olabilir.

Bir e-ticaret sisteminde "Customer"ı düşünün:

Kod'da bunlar farklı görünebilir:

typescript
// Sales Context - Satın almaya odaklı Customernamespace SalesContext {  export class Customer {    constructor(      private readonly id: string,      private readonly email: string,      private shippingAddresses: Address[],      private orderHistory: Order[]    ) {}
    placeOrder(order: Order): void {      this.orderHistory.push(order);    }
    getPreferredShippingAddress(): Address {      // Satış için business mantığı      return this.shippingAddresses[0];    }  }}
// Support Context - Servis sorunlarına odaklı Customernamespace SupportContext {  export class Customer {    constructor(      private readonly id: string,      private readonly email: string,      private tickets: SupportTicket[],      private preferredContactMethod: 'email' | 'phone'    ) {}
    createTicket(issue: string): SupportTicket {      const ticket = new SupportTicket(issue, this.id);      this.tickets.push(ticket);      return ticket;    }
    getOpenTickets(): SupportTicket[] {      return this.tickets.filter(t => t.isOpen());    }  }}

Bounded context'lerin faydaları:

  • Odaklanmış modeller: Her context sadece ihtiyacı olanı içerir
  • Bağımsız evrim: Context'ler diğerlerini etkilemeden değişebilir
  • Net sahiplik: Ekipler belirli context'lere sahiptir
  • Azaltılmış coupling: Bağımlılıklar context sınırlarında açıktır

Context Mapping

Context Mapping, bounded context'ler arasındaki ilişkileri tanımlar. Yaygın pattern'ler:

Customer/Supplier: Downstream context upstream'e bağlıdır. Ekipler değişiklikleri müzakere eder.

typescript
// Sales Context (Upstream)interface OrderPlaced {  orderId: string;  items: { productId: string; quantity: number }[];}
// Inventory Context (Downstream)class InventoryService {  handleOrderPlaced(event: OrderPlaced): void {    // Siparişe dayalı envanter rezerve et    event.items.forEach(item => {      this.reserveStock(item.productId, item.quantity);    });  }}

Anti-Corruption Layer: Modelinizi harici sistem kavramlarından korur.

typescript
// External legacy sistem farklı modele sahipinterface LegacyCustomerDTO {  cust_id: number;  cust_name: string;  cust_email: string;  // İhtiyacımız olmayan birçok alan}
// Anti-Corruption Layerclass LegacyCustomerAdapter {  toDomainModel(dto: LegacyCustomerDTO): Customer {    return new Customer(      dto.cust_id.toString(),      Email.create(dto.cust_email),      dto.cust_name    );  }
  toDTO(customer: Customer): LegacyCustomerDTO {    return {      cust_id: parseInt(customer.getId()),      cust_name: customer.getName(),      cust_email: customer.getEmail().getValue()    };  }}

Shared Kernel: İki context domain modelinin bir alt kümesini paylaşır. Değişiklikler koordinasyon gerektirir.

typescript
// Sales ve Pricing arasında shared kernelnamespace SharedKernel {  export class Money {    // Paylaşılan implementasyon  }
  export class ProductId {    // Paylaşılan value object  }}

Yaygın Tuzaklar ve Nasıl Kaçınılır

DDD ile çalışırken, bu sorunlarla tekrar tekrar karşılaştım:

Anemic Domain Model'ler

Sorun: Entity'ler getter/setter'lara sahip veri containerları haline gelir, tüm mantık service'lerde.

typescript
// Anemic - Bunu yapmayınclass Order {  public id: string;  public items: OrderItem[];  public status: string;
  // Sadece getter ve setter'lar, davranış yok}
class OrderService {  placeOrder(order: Order): void {    // Order içinde değil, burada tüm business mantığı    if (order.items.length === 0) {      throw new Error('Empty order');    }    order.status = 'placed';  }}

Çözüm: Davranışı domain modeline taşıyın.

typescript
// Zengin domain modelclass Order {  private status: OrderStatus;  private items: OrderItem[];
  place(): void {    if (this.items.length === 0) {      throw new Error('Cannot place empty order');    }    this.status = OrderStatus.Placed;  }}

Basit Domain'leri Aşırı Mühendislik

Sorun: Basit CRUD operasyonlarına tam DDD uygulamak.

Basit bir adres defteri oluşturuyorsanız, aggregate'lere, repository'lere ve domain service'lere ihtiyacınız yok. Validation ile basit bir veri modeli yeterlidir. DDD'yi karmaşık business mantığı için saklayın.

Context Sınırlarını Görmezden Gelmek

Sorun: Tüm sistemi tek bir büyük model olarak ele almak.

Bu, her kullanım durumuna hizmet etmeye çalışan 50 özelliğe sahip Customer gibi god object'lere yol açar. Bunun yerine, "customer"ın sales, support ve billing context'lerinde farklı anlamlara geldiğini kabul edin.

Repository'yi Database Gateway Olarak Kullanmak

Sorun: Her kullanım durumu için sorgu methodları sunan repository'ler.

typescript
// Çok fazla sorgu methoduinterface OrderRepository {  findById(id: string): Promise<Order>;  findByCustomerId(customerId: string): Promise<Order[]>;  findByStatus(status: string): Promise<Order[]>;  findByDateRange(start: Date, end: Date): Promise<Order[]>;  findByCustomerAndStatus(customerId: string, status: string): Promise<Order[]>;  // ... 20 method daha}

Çözüm: Repository'leri aggregate root'lara odaklı tutun. Sorgular için ayrı read model'ler kullanın.

typescript
// Basit repositoryinterface OrderRepository {  save(order: Order): Promise<void>;  findById(id: string): Promise<Order | null>;  delete(id: string): Promise<void>;}
// Okumalar için ayrı query serviceinterface OrderQueryService {  searchOrders(criteria: OrderSearchCriteria): Promise<OrderDTO[]>;}

Büyük Aggregate'ler

Sorun: Çok büyüyen ve performans sorunlarına neden olan aggregate'ler.

Order aggregate'iniz customer detaylarını, sevkiyat bilgilerini, ödeme geçmişini ve ürün kataloglarını içeriyorsa, basit operasyonlar için çok fazla veri yüklersiniz.

Çözüm: Aggregate'leri küçük ve odaklı tutun. Diğer aggregate'lere ID ile referans verin.

typescript
class Order {  constructor(    private readonly id: string,    private readonly customerId: string, // ID ile referans    private items: OrderItem[]  ) {}}

Ubiquitous Language'ı Atlamak

Sorun: Geliştiriciler business dilini kullanmak yerine kendi teknik terimlerini yaratır.

Bu, hataların gizlendiği bir çeviri katmanı oluşturur. Kod "transaction processing" derken business "payment confirmation" dediğinde, yanlış anlamalar oluşur.

Çözüm: İşbirlikçi modelleme oturumlarına zaman yatırın. Kodunuz domain uzmanlarınızın kullandığı kesin terimleri kullanmalıdır.

Pratik Uygulama İpuçları

Pratikte işe yarayanlar:

Küçük Başlayın: Her şeyi birden ele almayın. Bir karmaşık subdomain seçin ve DDD'yi orada uygulayın. Genişletmeden önce ekibiniz için neyin işe yaradığını öğrenin.

Event Storming: Geliştiricilerin ve domain uzmanlarının yapışkan notlarla business süreçlerini haritaladığı işbirlikçi oturumlar düzenleyin. Bu, ubiquitous language'ı ve bounded context'leri doğal olarak ortaya çıkarır.

Test-Driven Development: Testleri business dilinde yazın. Bu, domain modelini güçlendirir ve kodun business niyetinden ayrıldığını yakalar.

typescript
describe('Order', () => {  it('boş siparişlerin onaylanmasını engellemeli', () => {    const order = Order.create('customer-123');
    expect(() => order.confirm()).toThrow('Cannot confirm empty order');  });
  it('birden fazla item ile doğru toplamı hesaplamalı', () => {    const order = Order.create('customer-123');    order.addItem('product-1', 'Widget', Money.create(10, 'USD'), 2);    order.addItem('product-2', 'Gadget', Money.create(15, 'USD'), 1);
    expect(order.getTotal().getAmount()).toBe(35);  });});

Aşamalı Refactoring: Her şeyi yeniden yazmanıza gerek yok. Domain mantığını service'lerden entity'lere kademeli olarak çıkarın. Her iyileştirme bir sonrakini kolaylaştırır.

Kod Olarak Dokümantasyon: Domain kavramlarını belgelemek için type'lar ve interface'ler kullanın. Kendi kendini belgeleyen kod, eskiyen harici doküman ihtiyacını azaltır.

Kaynaklar ve İleri Okuma

DDD anlayışınızı derinleştirmek için temel kaynaklar:

Kitaplar:

  • "Domain-Driven Design" by Eric Evans - Orijinal mavi kitap. Yoğun ama kapsamlı. Yapı taşlarıyla ilgili Bölüm II ile başlayın.
  • "Implementing Domain-Driven Design" by Vaughn Vernon - Daha pratik ve modern. Implementasyon rehberliği için mükemmel.
  • "Domain-Driven Design Distilled" by Vaughn Vernon - Yoğunlaştırılmış giriş, hızlı başlamak için iyi.

Online Kaynaklar:

  • Martin Fowler'ın makaleleri martinfowler.com'da - Temel pattern'lerin net açıklamaları
  • DDD Community ddd-community.org'da - Kaynaklar ve etkinliklerle aktif topluluk
  • Alberto Brandolini'nin EventStorming websitesi - İşbirlikçi modelleme tekniği

Pratik Örnekler:

Sonuç

Domain-Driven Design, yazılım sistemlerindeki karmaşıklığı yönetmek için güçlü araçlar sağlar. Tactical pattern'ler - entity'ler, value object'ler, aggregate'ler, repository'ler ve domain service'ler - size temiz domain modelleri için yapı taşları verir. Stratejik pattern'ler - ubiquitous language, bounded context'ler ve context mapping - büyük sistemleri organize etmeye yardımcı olur.

İşte en çok önemsediğim şeyleri öğrendim: DDD körü körüne pattern'leri uygulamakla ilgili değil. Business ve teknoloji arasında ortak bir anlayış oluşturmak, sonra bu anlayışı kodda ifade etmekle ilgili. Kodunuz business dilini konuştuğunda, modelleriniz domain uzmanlarının problemler hakkında düşünme şeklini yansıttığında, yazılımın anlaşılması, sürdürülmesi ve evrimleşmesi daha kolay hale gelir.

Bir karmaşık subdomain ile başlayın. Domain uzmanlarıyla işbirliği yapın. Birlikte ubiquitous language oluşturun. Tactical pattern'leri değer kattıkları yerde uygulayın. Sisteminiz büyüdükçe bounded context'leri tanıyın. DDD bir yolculuktur, bir hedef değil - bu prensipleri gerçek projelerde uygularken anlayışınız derinleşecektir.

İlgili Yazılar