Ana içeriğe geç

Domain Layer

Domain katmanı uygulamanın kalbini oluşturur ve iş kurallarını barındırır; yanlış tasarımlar anemic model ve dışa bağımlı domain yapılarına yol açar.


1. Anemic Domain Model

Yanlış Kullanım: Entity’leri sadece property torbası olarak tasarlamak.

public class Order
{
    public int Id { get; set; }
    public List<OrderItem> Items { get; set; } = new();
    public decimal TotalPrice { get; set; }
    public OrderStatus Status { get; set; }
}

// İş mantığı servis katmanına dağılır
public class OrderService
{
    public void AddItem(Order order, Product product, int quantity)
    {
        order.Items.Add(new OrderItem { ProductId = product.Id, Quantity = quantity });
        order.TotalPrice = order.Items.Sum(i => i.Price * i.Quantity);
    }

    public void Cancel(Order order)
    {
        if (order.Status == OrderStatus.Shipped) throw new Exception("Kargolanmış sipariş iptal edilemez");
        order.Status = OrderStatus.Cancelled;
    }
}

İdeal Kullanım: Rich Domain Model ile iş mantığını entity içinde barındırın.

public class Order
{
    private readonly List<OrderItem> _items = new();

    public int Id { get; private set; }
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
    public decimal TotalPrice { get; private set; }
    public OrderStatus Status { get; private set; }

    public void AddItem(Product product, int quantity)
    {
        var item = new OrderItem(product.Id, product.Price, quantity);
        _items.Add(item);
        RecalculateTotal();
    }

    public void Cancel()
    {
        if (Status == OrderStatus.Shipped)
            throw new DomainException("Kargolanmış sipariş iptal edilemez.");

        Status = OrderStatus.Cancelled;
    }

    private void RecalculateTotal()
    {
        TotalPrice = _items.Sum(i => i.Price * i.Quantity);
    }
}

2. Value Object Kullanmamak

Yanlış Kullanım: Primitive obsession ile değer nesnelerini string/int olarak tutmak.

public class Customer
{
    public string Email { get; set; }     // Herhangi bir string olabilir
    public string PhoneNumber { get; set; } // Format kontrolü yok
    public decimal Balance { get; set; }   // Negatif olabilir
}

İdeal Kullanım: Value Object ile iş kurallarını değer düzeyinde uygulayın.

public record Email
{
    public string Value { get; }

    public Email(string value)
    {
        if (!value.Contains('@'))
            throw new DomainException("Geçersiz e-posta adresi.");
        Value = value;
    }
}

public record Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        if (amount < 0) throw new DomainException("Tutar negatif olamaz.");
        Amount = amount;
        Currency = currency;
    }

    public Money Add(Money other)
    {
        if (Currency != other.Currency) throw new DomainException("Para birimleri uyuşmuyor.");
        return new Money(Amount + other.Amount, Currency);
    }
}

public class Customer
{
    public Email Email { get; private set; }
    public Money Balance { get; private set; }
}

3. Domain Event Kullanmamak

Yanlış Kullanım: Yan etkileri doğrudan entity içinden çağırmak.

public class Order
{
    private readonly IEmailService _emailService;
    private readonly IInventoryService _inventoryService;

    public void Complete()
    {
        Status = OrderStatus.Completed;
        _emailService.SendOrderConfirmation(this);    // Domain altyapıya bağımlı
        _inventoryService.ReduceStock(this.Items);
    }
}

İdeal Kullanım: Domain Event ile yan etkileri ayırın.

public abstract class Entity
{
    private readonly List<IDomainEvent> _domainEvents = new();
    public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
    protected void AddDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent);
    public void ClearDomainEvents() => _domainEvents.Clear();
}

public class Order : Entity
{
    public void Complete()
    {
        Status = OrderStatus.Completed;
        AddDomainEvent(new OrderCompletedEvent(Id, Items));
    }
}

public class OrderCompletedHandler : INotificationHandler<OrderCompletedEvent>
{
    public async Task Handle(OrderCompletedEvent notification, CancellationToken ct)
    {
        // E-posta gönder, stok azalt, vb.
    }
}

4. Domain Katmanında Framework Bağımlılığı

Yanlış Kullanım: Domain entity’lerinde EF Core attribute’ları kullanmak.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public class Product
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Required]
    [MaxLength(200)]
    public string Name { get; set; }

    [Column(TypeName = "decimal(18,2)")]
    public decimal Price { get; set; }
}

İdeal Kullanım: Domain entity’lerini saf tutun, mapping’i Infrastructure’da yapın.

// Domain Layer
public class Product
{
    public int Id { get; private set; }
    public string Name { get; private set; }
    public decimal Price { get; private set; }

    public Product(string name, decimal price)
    {
        if (string.IsNullOrWhiteSpace(name)) throw new DomainException("Ürün adı boş olamaz.");
        if (price <= 0) throw new DomainException("Fiyat sıfırdan büyük olmalıdır.");
        Name = name;
        Price = price;
    }
}

// Infrastructure Layer
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
    public void Configure(EntityTypeBuilder<Product> builder)
    {
        builder.HasKey(p => p.Id);
        builder.Property(p => p.Name).HasMaxLength(200).IsRequired();
        builder.Property(p => p.Price).HasColumnType("decimal(18,2)");
    }
}

5. Aggregate Root Sınırlarını Belirlememek

Yanlış Kullanım: İç entity’lere doğrudan erişim izni vermek.

public class Order
{
    public List<OrderItem> Items { get; set; } = new();
}

// Dışarıdan iç entity doğrudan değiştirilebilir
order.Items.Add(new OrderItem());
order.Items[0].Quantity = -5; // Tutarsız durum
order.Items.Clear(); // Aggregate kuralları bypass edilir

İdeal Kullanım: Aggregate Root üzerinden tüm erişimi kontrol edin.

public class Order
{
    private readonly List<OrderItem> _items = new();
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();

    public void AddItem(int productId, decimal price, int quantity)
    {
        if (quantity <= 0) throw new DomainException("Miktar pozitif olmalıdır.");

        var existing = _items.FirstOrDefault(i => i.ProductId == productId);
        if (existing != null)
            existing.IncreaseQuantity(quantity);
        else
            _items.Add(new OrderItem(productId, price, quantity));
    }

    public void RemoveItem(int productId)
    {
        var item = _items.FirstOrDefault(i => i.ProductId == productId)
            ?? throw new DomainException("Ürün bulunamadı.");
        _items.Remove(item);
    }
}