Domain-Driven Design: Giriş ve Temeller
Domain-Driven Design'a kapsamlı giriş: temel kavramlar, yapı taşları, stratejik desenler ve DDD'yi ne zaman ve nasıl uygulayacağına dair rehber.
Domain-Driven Design (DDD), kodun yapısını hizmet ettiği business domain’iyle hizalar. Karmaşık ve uzun ömürlü sistemlerde bu hizalama karşılığını verir: sınıflar ve method’lar domain uzmanlarının zaten kullandığı isimleri taşıdığında, business kuralları controller’lara ve servislere dağılmak yerine tek bir yerde toplanır. Bedeli, işi bilen kişilerle birlikte harcanan modelleme zamanıdır.
Karar da tam olarak bu bedelin üzerine kuruludur. Sık değişen kurallar, yıllarca sahipleneceğiniz bir sistem ve sizinle oturmaya istekli domain uzmanları: bu üç koşul varsa yatırım kendini amorti eder. Yoksa sade bir katmanlı mimari daha iyi hizmet eder.
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.
Bu işbirliği, DDD’nin ubiquitous language dediği ortak kelime dağarcığını doğurur: kodda, konuşmalarda ve dokümantasyonda aynı şekilde karşınıza çıkar. Kod ve business aynı terimleri konuştuğunda yanlış anlaşılmalar azalır, yazılım daha sürdürülebilir kalır.
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.
DDD’nin Karşılığını Verdiği Durumlar#
En güçlü sinyal sık değişen business kurallarıdır: mantığın controller’lar ve servisler arasında dağıldığı codebase’ler yaygındır, DDD bu kaosa yapı getirir. Yıllarca sürdüreceğiniz sistemlerde de karşılığını verir, çünkü ön modelleme yatırımı yeni geliştiricilerin adaptasyonunu hızlandırır ve değişiklikler sırasında kuralları bozma riskini azaltır.
İşbirliği de en az bunlar kadar önemlidir. Erişilebilir ve işbirliğine istekli domain uzmanları, sıradan bir geliştirme sürecinin tek başına ulaşamayacağı ortak bir dil ve model yaratır. Aynı mantık farklı alt alanlardan oluşan sistemler için de geçerlidir: envanter, ödeme ve sevkiyatı barındıran bir e-ticaret sistemi, DDD’nin stratejik pattern’lerinin context’ler arasındaki sınırları yönettiği tipik bir örnektir.
DDD’yi Atlamanız Gereken Durumlar#
Minimal business mantığı olan basit bir veri giriş sistemi DDD’ye ihtiyaç duymaz: temel bir MVC veya katmanlı mimari yeterlidir, fazladan yapı sadece karmaşıklık ekler. Prototip ve MVP’ler için de durum benzerdir; DDD’nin modelleme yükü asıl ihtiyacınız olan geri bildirim döngüsünü yavaşlatır, bu yüzden önce doğrulayın, proje büyürse DDD’yi değerlendirin. Veri odaklı işler ayrı bir istisnadır: ETL pipeline’ları, raporlama araçları ve analitik sistemleri genellikle domain modellemesi yerine veri odaklı yaklaşımlarla daha iyi hizmet görür. Domain uzmanlarına erişemeyen küçük bir ekibin de bu yatırımı haklı çıkaracak kadar işi yoktur çoğunlukla.
Tactical Design Pattern’leri#
Bunlar kodda kullanacağınız somut yapı taşları: DDD’nin tactical yarısı, birazdan gelecek stratejik pattern’lerden ayrı.
Entity’ler#
Entity’ler, kimliği zaman içinde korunan nesnelerdir. Aynı veriye sahip ancak farklı ID’lere sahip iki entity farklı nesnelerdir.
// Entity: Benzersiz kimliğe sahip User
class 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;
}
}
id alanı bu entity’ye kimliğini verir: aynı e-postaya ama farklı ID’lere sahip iki User instance’ı yine de farklı kullanıcılardır. Entity’ler bir yaşam döngüsünden geçer (oluşturulur, değiştirilir, sonunda silinir) ve factory method’lar bu döngüyü açık hale getirir: create() yeni bir instance oluşturur, reconstitute() ise storage’dan mevcut birini yeniden kurar. changeEmail() gibi method’ların içinde email validasyonu gibi business kuralları uygulanır.
Value Object’ler#
Value Object’ler kimliği olmayan kavramları temsil eder. Aynı veriye sahip iki value object eşit kabul edilir.
// Value Object: Email adresi
class 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 Money
class 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’dır: setter yoktur, add() ve multiply() gibi operasyonlar orijinali değiştirmek yerine yeni instance’lar döner. Kendi kendini de doğrular: yukarıdaki Email.create() ve Money.create() örneklerinde olduğu gibi, veri geçersizse oluşturma anında başarısız olur. Değere göre karşılaştırılır: aynı miktar ve para birimine sahip iki Money nesnesi, farklı instance’lar olsalar bile eşittir.
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.
// Aggregate: OrderItem'ları olan Order
class 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 Root
class 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’in dışından sadece Order referans edilir; OrderItem içseldir ve yalnızca root üzerinden erişilebilir. Tutarlılık kuralları da bu root’ta yaşar: addItem(), confirm() ve cancel() bir değişiklik yapmadan önce mevcut durumu kontrol eder, böylece aggregate iki operasyon arasında hiçbir zaman geçersiz bir durumda kalmaz. Değişiklikler atomik olarak commit edilir ve diğer aggregate’ler buna doğrudan nesne referansı yerine ID ile referans verir.
Repository’ler#
Repository’ler, aggregate’lere erişim için bir soyutlama sağlar ve persistence detaylarını gizler.
// Repository interface'i
interface 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 implementasyon
class 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 implementasyonu
class 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');
}
}
Bu soyutlama bir collection gibi davranır: save(), findById(), delete(); SQL veya tablo adı sızdıran hiçbir şey yok. Domain katmanı persistence’tan habersiz kalır, Order hiçbir zaman bir veritabanı client’ı import etmez ve repository’ler aggregate root başına bir tane olacak şekilde aggregate odaklı kalır. Bu aynı habersizlik, InMemoryOrderRepository’yi testlerde PostgresOrderRepository yerine geçebilir kılan şeydir.
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.
// 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 koordinasyonu
class 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);
}
}
Yukarıdaki OrderFulfillmentService tipik bir örnek: envanter ve sevkiyat gibi, Order’ın kendisine ait olmayan iki farklı kaygıyı ele alır. Domain service’ler; birden fazla aggregate’e yayılan mantık, harici sistemlerin koordinasyonu veya tek bir entity’ye doğal olarak oturmayan hesaplamalar için yerini bulur. Kendi state’leri yoktur, sadece kendilerine geçirilen nesneler üzerinde dönüşüm yaparlar.
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.
Kod business paydaşlarından farklı terimler kullandığında, çeviri hataları sızar. Business buna “rezervasyon” derken kod “booking” diyorsa, gereksinimler kaçınılmaz olarak yanlış anlaşılır.
Kötü Örnek (Genel teknik terimler):
class DataManager {
processRequest(data: any): any {
// "process" business terimleriyle ne anlama geliyor?
}
}
İyi Örnek (Ubiquitous Language):
class ReservationService {
confirmReservation(reservation: Reservation): void {
// Net business operasyonu
}
cancelReservation(reservationId: string): void {
// Business paydaşları bunu anlar
}
}
Pratikte bu, domain uzmanlarıyla işbirlikçi modelleme oturumlarında oturmak, business ile teknolojinin ortaklaşa kullandığı bir terim sözlüğü tutmak, aynı isimleri kodda, dokümanda ve konuşmalarda kullanmak ve bu kelime dağarcığını ekibin domain anlayışı derinleştikçe iyileştirmek anlamına gelir.
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:
Kodda bunlar şöyle farklılaşır:
// Sales Context - Satın almaya odaklı Customer
namespace 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ı Customer
namespace 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());
}
}
}
Yukarıdaki Sales ve Support Customer sınıfları küçük kalır, çünkü her biri sadece kendi context’inin ihtiyacı olanı modeller: Sales support ticket’larını hiç düşünmez, Support ise shipping adreslerini. Bu odak, context’lerin birbirinden bağımsız evrimleşmesini sağlar ve ekiplere çalıştıkları modelin net sahipliğini verir.
Context Mapping#
Context Mapping, bounded context’ler arasındaki ilişkileri tanımlar. Yaygın pattern’ler:
Customer/Supplier ilişkisinde downstream context upstream’e bağlıdır, ekipler değişiklikleri müzakere eder:
// 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:
// External legacy sistem farklı modele sahip
interface LegacyCustomerDTO {
cust_id: number;
cust_name: string;
cust_email: string;
// İhtiyacımız olmayan birçok alan
}
// Anti-Corruption Layer
class 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’de iki context domain modelinin bir alt kümesini paylaşır, bu yüzden değişiklikler koordinasyon gerektirir:
// Sales ve Pricing arasında shared kernel
namespace SharedKernel {
export class Money {
// Paylaşılan implementasyon
}
export class ProductId {
// Paylaşılan value object
}
}
DDD Nerede Yanlış Gider#
DDD kod tabanlarında aynı birkaç hata tekrar tekrar karşımıza çıkar:
Anemic Domain Model’ler#
Entity’ler sadece getter/setter’lara sahip olduğunda tüm mantık service’lere kayar ve entity’nin kendisi hiçbir davranış taşımaz:
// Anemic - Bunu yapmayın
class 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';
}
}
Bu davranışı entity’ye geri taşımak domain modelini yeniden anlamlı kılar:
// Zengin domain model
class 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’lerde Aşırı Mühendislik#
Basit CRUD operasyonlarına DDD’nin tamamını uygulamak da kendi başına bir hatadır. Basit bir adres defteri, aggregate’lere, repository’lere ve domain service’lere ihtiyaç duymaz: validation’lı sade bir veri modeli yeterlidir.
Context Sınırlarını Görmezden Gelmek#
Tüm sistemi tek bir büyük model olarak ele almak, her kullanım durumuna hizmet etmeye çalışan 50 özellikli Customer gibi god object’lere yol açar. Sales, support ve billing, “customer”ın kendi bağlamlarına özgü bir dilimine ihtiyaç duyar.
Repository’yi Database Gateway Olarak Kullanmak#
Her kullanım durumu için ayrı bir sorgu methodu eklenen repository, zamanla repository olmaktan çıkıp ad-hoc bir database gateway’e dönüşür:
// Çok fazla sorgu methodu
interface 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
}
Repository’leri aggregate root’lara odaklı tutup geri kalanı ayrı bir read model’e taşımak bu büyümeyi kontrol altında tutar:
// Basit repository
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
delete(id: string): Promise<void>;
}
// Okumalar için ayrı query service
interface OrderQueryService {
searchOrders(criteria: OrderSearchCriteria): Promise<OrderDTO[]>;
}
Büyük Aggregate’ler#
Sürekli büyüyen aggregate’ler kendi performans sorunlarını yaratır. 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.
Aggregate’leri küçük tutmak ve diğer aggregate’lere ID ile referans vermek bunu önler:
class Order {
constructor(
private readonly id: string,
private readonly customerId: string, // ID ile referans
private items: OrderItem[]
) {}
}
Ubiquitous Language’ı Atlamak#
Geliştiriciler bazen business dilini kullanmak yerine kendi teknik terimlerini yaratır, bu uyumsuzluk hataların gizlendiği bir çeviri katmanı oluşturur. Kod “transaction processing” derken business “payment confirmation” dediğinde yanlış anlamalar başlar ve bu boşluğu kapatan da işbirlikçi modelleme oturumlarıdır.
DDD’yi Aşamalı Olarak Benimsemek#
DDD’yi benimsemek için her şeyi baştan yazmanız gerekmez; birkaç alışkanlık geçişi kolaylaştırır.
Önce bir karmaşık subdomain seçip DDD’yi orada uygulayın; ekibiniz için neyin işe yaradığını orada öğrenin, ardından sistemin geri kalanına genişletin. Geliştiricilerin ve domain uzmanlarının post-it’lerle business süreçlerini birlikte haritaladığı event storming oturumları, ubiquitous language’ı ve bounded context’leri kendiliğinden ortaya çıkarır. Testleri business dilinde yazmak da aynı işi görür: domain modelini güçlendirir ve kodun business niyetinden ayrıldığı anı yakalar. Aşağıdaki Order testleri hem geçersiz bir durumu hem de bir toplam hesaplamasını kontrol eder:
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);
});
});
Domain mantığı da service’lerden entity’lere aynı aşamalı şekilde taşınabilir, her çıkarma bir sonrakini biraz daha kolaylaştırır. Type’lar ve interface’ler bu geçiş sürerken domain kavramlarını belgeleyebilir.
Ek Kaynaklar#
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 (yeni sekmede açılır)’da - Temel pattern’lerin net açıklamaları
- DDD Community dddcommunity.org (yeni sekmede açılır)’da - Makaleler, vaka çalışmaları ve yerel meetup grupları
- Alberto Brandolini’nin EventStorming çalışması - İşbirlikçi modelleme tekniği
Pratik Örnekler:
- Microsoft’un .NET Microservices Architecture rehberi (yeni sekmede açılır) - Pratikte iyi DDD pattern’leri
Sonuç#
Domain-Driven Design size iki takım araç verir. Tactical pattern’ler (entity’ler, value object’ler, aggregate’ler, repository’ler ve domain service’ler) tek tek sınıfları temiz bir domain modeline dönüştürür. Stratejik pattern’ler (ubiquitous language, bounded context’ler ve context mapping) büyük bir sistemin nasıl bölüneceğine karar verir.
Business kuralları karmaşıksa, sistem yıllarca yaşayacaksa ve sizinle modelleme yapacak domain uzmanları varsa varsayılan seçim DDD’dir. Domain ince olduğunda bundan vazgeçin: bir CRUD yönetim paneli, bir raporlama pipeline’ı veya atmayı planladığınız bir prototip, aggregate ve repository katmanları altında güvenlik kazanmaz, sadece yavaşlar. Kararsızsanız DDD’yi önce tek bir karmaşık subdomain’de uygulayın, geri kalanını basit tutun.
Kaynaklar#
- Eric Evans’ın DDD Referansı (yeni sekmede açılır) - Orijinal DDD kitabındaki her tanım ve kalıbı özetleyen, DDD’nin yaratıcısı tarafından hazırlanmış ücretsiz resmi referans belgesi.
- Domain-Driven Design: Yazılımın Kalbindeki Karmaşıklıkla Yüzleşmek (Evans, 2003) (yeni sekmede açılır) - DDD’yi tanıtan temel kitap; entity, value object, aggregate, repository ve bounded context tanımlarının birincil kaynağı.
- Implementing Domain-Driven Design, Vaughn Vernon (yeni sekmede açılır) - Evans’ın kitabının pratik tamamlayıcısı: hexagonal mimari, CQRS ve olay güdümlü DDD’yi somut örneklerle ele alır.
- Martin Fowler: Bounded Context (yeni sekmede açılır) - Büyük modelleri ve ekipleri yönetmeye yönelik temel stratejik DDD kalıbı olan bounded context’i açıklayan özlü makale.
- DDD Topluluğu: Evans’ın Kitabı (yeni sekmede açılır) - Domain-driven design’ın kökenlerine ilişkin ek bağlamı içeren, Evans’ın 2003 kitabının topluluk sayfası.
- Martin Fowler: Domain-Driven Design (yeni sekmede açılır) - Fowler’ın ubiquitous language ve model güdümlü tasarım da dahil olmak üzere DDD’nin temel kavramlarına genel bakışı.
İlgili yazılar
SOLID prensiplerinin modern JavaScript'te uygulanışı: TypeScript, React hooks ve fonksiyonel pattern'lerle pratik örnekler, ayrıca ne zaman gereksiz.
typescript · javascript · react +4
AWS CDK projelerinde service-based, domain-based, feature-based ve layer-based organizasyon patternlerini karar çerçeveleri ve örneklerle ne zaman seçeceğini öğren.
aws-cdk · typescript · infrastructure-as-code +3
Singleton, Factory, Builder ve Prototype pattern'lerinin TypeScript'te evrimi: ES modülleri singleton'ı ne zaman, factory function'lar class'ı ne zaman geçer.
typescript · design-patterns · architecture +1
Decorator, Adapter, Facade, Composite ve Proxy patternlerinin React ve TypeScript'te evrimi: HOC'lar ne zaman hook'lara yol verir, adapterler API'ları nasıl izole eder.
typescript · react · design-patterns +2
CDK stack düzeni için yaşam döngüsü testi: bir kaynağın ömrü tek bir dağıtımdan uzunsa kendi uzun ömürlü stack'ine koyun, ona bilinen bir adla erişin.
aws-cdk · infrastructure-as-code · typescript +3