Modern yazılım geliştirme, sürdürülebilir, test edilebilir ve ölçeklenebilir sistemler inşa etmeyi gerektirir. C# projelerinde bu hedeflere ulaşmak için Clean Architecture ve Domain-Driven Design (DDD) yaklaşımları kritik öneme sahiptir. Bu makale, bu iki güçlü tasarım prensibini C# bağlamında derinlemesine inceleyecek, temel kavramlarını açıklayacak ve gerçek dünya uygulamalarında nasıl entegre edilebileceklerini gösterecektir. Amacımız, karmaşık iş gereksinimlerini karşılayan sağlam yazılımlar geliştirmektir.
Clean Architecture Nedir?
Clean Architecture (Temiz Mimari), Robert C. Martin (Uncle Bob) tarafından popülerleştirilen, yazılım sistemlerinin bağımsızlığını, test edilebilirliğini ve sürdürülebilirliğini artırmayı hedefleyen bir dizi tasarım prensibidir. Temel amacı, iş kurallarını (domain logic) dış katmanlardan (veritabanı, UI, çerçeveler vb.) izole etmek ve bağımlılıkların her zaman içe doğru olmasını sağlamaktır. Bu sayede, dış katmanlarda yapılacak değişiklikler iç katmanları etkilemez ve sistemin temel iş mantığı her zaman kararlı kalır.
Clean Architecture Katmanları
Clean Architecture genellikle dört ana katmanla temsil edilir ve bu katmanlar içten dışa doğru bağımlılık kuralına uyar:
-
Domain (Etki Alanı)
Bu, uygulamanın çekirdek iş mantığını ve varlıklarını (Entities, Value Objects, Aggregates) içeren en içteki katmandır. Herhangi bir dış bağımlılığı olmamalıdır. İş kuralları burada tanımlanır ve sistemin kalbini oluşturur. Örneğin, bir sipariş sistemi için
Order,OrderItemgibi varlıklar ve siparişin durumunu değiştiren iş kuralları bu katmanda yer alır.// MyApplication.Domain/Entities/Order.cs public class Order { public Guid Id { get; private set; } public Customer Customer { get; private set; } public List<OrderItem> Items { get; private set; } public OrderStatus Status { get; private set; } private Order() { /* EF Core required */ } public Order(Customer customer, IEnumerable<OrderItem> items) { Id = Guid.NewGuid(); Customer = customer ?? throw new ArgumentNullException(nameof(customer)); Items = items?.ToList() ?? throw new ArgumentNullException(nameof(items)); Status = OrderStatus.Pending; // İş kuralları: Boş sipariş oluşturulamaz, vb. if (!Items.Any()) { throw new InvalidOperationException("Order must contain at least one item."); } } public void ConfirmOrder() { if (Status != OrderStatus.Pending) { throw new InvalidOperationException("Only pending orders can be confirmed."); } Status = OrderStatus.Confirmed; } // Diğer iş mantığı metotları... } -
Application (Uygulama)
Uygulamaya özgü iş kurallarını ve kullanım senaryolarını (use cases) içerir. Bu katman, Domain katmanını kullanarak uygulama operasyonlarını düzenler. Örneğin, bir siparişin oluşturulması, güncellenmesi veya sorgulanması gibi işlemler burada tanımlanır. Komutlar (Commands), sorgular (Queries) ve bunların işleyicileri (Handlers), uygulama servisleri (Application Services) bu katmanda bulunur. Domain katmanına bağımlıdır ama dış katmanlardan (Infrastructure, UI) bağımsızdır.
// MyApplication.Application/Features/Orders/CreateOrderCommand.cs public class CreateOrderCommand : IRequest<Guid> { public Guid CustomerId { get; set; } public List<OrderItemDto> Items { get; set; } } // MyApplication.Application/Features/Orders/CreateOrderCommandHandler.cs public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid> { private readonly IOrderRepository _orderRepository; private readonly ICustomerRepository _customerRepository; public CreateOrderCommandHandler(IOrderRepository orderRepository, ICustomerRepository customerRepository) { _orderRepository = orderRepository; _customerRepository = customerRepository; } public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken) { var customer = await _customerRepository.GetByIdAsync(request.CustomerId); if (customer == null) throw new NotFoundException($"Customer with ID {request.CustomerId} not found."); var orderItems = request.Items.Select(item => new Domain.ValueObjects.OrderItem(item.ProductId, item.Quantity, item.UnitPrice)); var order = new Domain.Entities.Order(customer, orderItems); await _orderRepository.AddAsync(order); await _orderRepository.UnitOfWork.CommitAsync(cancellationToken); // Save changes return order.Id; } } -
Infrastructure (Altyapı)
Dışsal bağımlılıkların (veritabanı erişimi, dosya sistemi, harici servisler, mesaj kuyrukları, ORM’ler gibi) uygulanmasını içerir. Bu katman, Application katmanında tanımlanan arayüzleri (interfaces) uygular. Örneğin,
IOrderRepositoryarayüzünün Entity Framework Core ile gerçekleştirimi bu katmanda yer alır. Application ve Domain katmanlarına bağımlıdır.// MyApplication.Infrastructure/Persistence/Repositories/OrderRepository.cs public class OrderRepository : IOrderRepository { private readonly ApplicationDbContext _context; public OrderRepository(ApplicationDbContext context) { _context = context; } public async Task<Order> GetByIdAsync(Guid id) { return await _context.Orders.Include(o => o.Items).FirstOrDefaultAsync(o => o.Id == id); } public async Task AddAsync(Order order) { await _context.Orders.AddAsync(order); } // ... Diğer repository metotları } -
Presentation/UI (Sunum/Kullanıcı Arayüzü)
Kullanıcının uygulama ile etkileşim kurduğu katmandır (ASP.NET Core Web API, MVC, Blazor, Console uygulaması vb.). Bu katman, kullanıcı isteklerini Application katmanına yönlendirir ve Application katmanından gelen sonuçları kullanıcıya sunar. En dış katmandır ve Application katmanına bağımlıdır.
// MyApplication.Presentation.WebAPI/Controllers/OrdersController.cs [ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly IMediator _mediator; // MediatR for sending commands/queries public OrdersController(IMediator mediator) { _mediator = mediator; } [HttpPost] public async Task<ActionResult<Guid>> CreateOrder([FromBody] CreateOrderCommand command) { var orderId = await _mediator.Send(command); return Ok(orderId); } // ... Diğer API endpointleri }
Domain-Driven Design (DDD) Nedir?
Domain-Driven Design (DDD), karmaşık iş alanlarında yazılım geliştirmeyi basitleştirmeyi ve iş gereksinimlerine daha iyi uyum sağlamayı amaçlayan bir yazılım geliştirme yaklaşımıdır. Eric Evans’ın “Domain-Driven Design: Tackling Complexity in the Heart of Software” adlı kitabıyla popülerleşmiştir. DDD, yazılımın temel odağını teknik detaylardan çok iş alanının (domain) kendisine kaydırır. İş uzmanları ve geliştiriciler arasında ortak bir dil (Ubiquitous Language) oluşturarak iletişimdeki yanlış anlaşılmaları azaltmayı hedefler.
DDD’nin Temel Kavramları
DDD’nin hem stratejik hem de taktiksel desenleri vardır. Stratejik desenler iş alanını büyük ölçekte anlamak ve bölmekle ilgilenirken, taktiksel desenler bu alan içindeki bileşenleri modellemek için kullanılır.
-
Ubiquitous Language (Ortak Dil)
İş uzmanları ve geliştiriciler arasında paylaşılan, belirli bir iş alanı bağlamında kesin ve tutarlı bir dildir. Bu dil, yazılım koduna doğrudan yansıtılmalı ve iş konuşmalarında da kullanılmalıdır. Örneğin, bir e-ticaret sisteminde “sipariş”, “ürün”, “müşteri” gibi terimler hem kodda hem de günlük iletişimde aynı anlamı taşımalıdır.
-
Bounded Context (Sınırlı Bağlam)
Karmaşık bir sistemin farklı bölümlerini birbirinden ayıran mantıksal sınırlardır. Her Bounded Context’in kendi Ubiquitous Language’i, kendi iş kuralları ve kendi model tanımı vardır. Örneğin, bir e-ticaret uygulamasında “Sipariş Yönetimi”, “Envanter Yönetimi” ve “Müşteri Desteği” farklı Bounded Context’ler olabilir. Bu, büyük sistemlerin yönetilebilir parçalara ayrılmasını sağlar.
-
Entities (Varlıklar)
Benzersiz bir kimliği olan ve yaşam döngüsü boyunca bu kimliği koruyan nesnelerdir. Kimlikleri önemli olduğu için değerleri değişse bile aynı varlık olarak kalırlar. Örneğin, bir
Customer(Müşteri) veyaOrder(Sipariş) birer varlıktır. C# dilinde genellikle bir ID özelliği ve iş mantığı içeren sınıflar olarak modellenirler.// Örnek: Customer Entity public class Customer : BaseEntity // BaseEntity kimlik sağlar { public string Name { get; private set; } public string Email { get; private set; } private Customer() { } // EF Core için public Customer(Guid id, string name, string email) { Id = id; Name = name; Email = email; } public void UpdateEmail(string newEmail) { // İş kuralı: Email formatı doğru olmalı, vb. if (string.IsNullOrWhiteSpace(newEmail) || !IsValidEmail(newEmail)) { throw new ArgumentException("Invalid email format."); } Email = newEmail; } } -
Value Objects (Değer Nesneleri)
Kimliği olmayan, yalnızca özniteliklerinin değerleriyle tanımlanan nesnelerdir. İki değer nesnesi, tüm öznitelikleri aynıysa eşit kabul edilir. Genellikle değişmez (immutable) olmalıdırlar. Örneğin, bir adres (
Address), para birimi (Money) veya tarih aralığı (DateRange) birer değer nesnesidir. C# dilinde genellikle sadece özellikler içeren ve eşitlik karşılaştırması için özelleştirilmiş sınıflar veya kayıtlar (records) olarak tasarlanır.// Örnek: Address Value Object public record Address(string Street, string City, string PostalCode, string Country); // Örnek: Money Value Object public record Money { public decimal Amount { get; init; } public string Currency { get; init; } public Money(decimal amount, string currency) { if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amount)); if (string.IsNullOrWhiteSpace(currency)) throw new ArgumentNullException(nameof(currency)); Amount = amount; Currency = currency; } public Money Add(Money other) { if (Currency != other.Currency) throw new InvalidOperationException("Cannot add money with different currencies."); return new Money(Amount + other.Amount, Currency); } } -
Aggregates (Kümeler)
Veri tutarlılığını sağlamak için birlikte ele alınması gereken Entity ve Value Object’lerin bir kümesidir. Her Aggregate’in bir kök varlığı (Aggregate Root) bulunur. Dışarıdan Aggregate’e erişim sadece kök varlık üzerinden olmalıdır. Bu, iş kurallarının tek bir noktadan yönetilmesini sağlar. Örneğin, bir
Order(sipariş) ve onun altındakiOrderItem‘lar bir Aggregate oluşturabilir veOrderAggregate Root’u olur.// Order Aggregate'i, Order Aggregate Root olarak davranır. // OrderItem'lar Order'ın içinde Value Object veya Entity olarak tanımlanabilir. public class Order // Aggregate Root { public Guid Id { get; private set; } public Guid CustomerId { get; private set; } // Sadece kimliği tutar, Customer Entity'sini tutmaz private readonly List<OrderItem> _items; public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly(); public OrderStatus Status { get; private set; } // ... Yapıcı metot ve iş mantığı ... } -
Domain Services (Etki Alanı Servisleri)
Belirli bir varlığın veya değer nesnesinin sorumluluğuna uymayan, ancak iş alanı içinde önemli olan operasyonlardır. Genellikle birden fazla Aggregate veya Varlık üzerinde işlem yaparlar. Örneğin, bir para transferi hizmeti veya bir siparişin birden fazla farklı kargo sistemini içeren karmaşık bir doğrulama süreci Domain Service olabilir.
-
Repositories (Depolar)
Aggregate’leri kaydetme, yükleme ve sorgulama işlemlerini soyutlayan arayüzlerdir. Veritabanı gibi kalıcılık mekanizmalarının detaylarını gizlerler ve Domain/Application katmanlarının bu detaylardan bağımsız kalmasını sağlarlar. Her Aggregate Root için bir Repository tanımlanması yaygın bir DDD prensibidir.
// MyApplication.Domain/Repositories/IOrderRepository.cs public interface IOrderRepository : IRepository<Order> // Geniş bir IRepository arayüzü olabilir { Task<Order> GetByIdAsync(Guid id); Task AddAsync(Order order); // ... Diğer Aggregate'e özgü sorgu metotları }
Clean Architecture ve DDD’nin C# ile Entegrasyonu
Clean Architecture ve DDD, birbirini mükemmel şekilde tamamlayan iki güçlü yaklaşımdır. Clean Architecture, sistemin katmanlı yapısını ve bağımlılık akışını tanımlayarak mimari bir çerçeve sağlarken, DDD bu çerçeve içindeki Domain katmanının nasıl zenginleştirileceğine odaklanır. C# projelerinde bu entegrasyon, uygulamanın hem teknik olarak sağlam hem de iş mantığı açısından zengin olmasını sağlar.
Entegrasyonun temel noktaları şunlardır:
- Domain Katmanı ve DDD: Clean Architecture’ın Domain katmanı, DDD’nin temelini oluşturur. Entities, Value Objects, Aggregates ve Domain Services gibi DDD kavramları bu katmanda C# sınıfları, kayıtları ve arayüzleri olarak modellenir. Ubiquitous Language, kodun içindeki isimlendirmelerde ve sınıf tasarımlarında kendini gösterir.
-
Application Katmanı ve DDD: Application katmanı, Domain katmanındaki iş mantığını kullanarak kullanım senaryolarını gerçekleştirir. DDD’nin iş odaklı yaklaşımı sayesinde, bu katmandaki komutlar ve sorgular doğrudan iş gereksinimlerini yansıtır. Örneğin, bir
CreateOrderCommand, bir sipariş Aggregate’ini oluşturmak için Domain Service’lerini veya Repository’leri kullanır. -
Infrastructure Katmanı ve DDD: DDD’nin Repository desenleri, Infrastructure katmanında somutlaştırılarak kalıcılık mekanizmalarının Domain katmanından soyutlanmasını sağlar.
IOrderRepositoryarayüzü Domain katmanında tanımlanırken, Entity Framework Core veya Dapper gibi ORM’lerle yapılan somut uygulamaları Infrastructure katmanında yer alır. Bu, veritabanı teknolojisi değişse bile Domain ve Application katmanlarının etkilenmemesini sağlar. - Bounded Context’ler ve Modülerlik: Her Bounded Context, kendi içindeki Clean Architecture yapısına sahip ayrı bir C# projesi veya çözüm klasörü olarak tasarlanabilir. Bu, büyük sistemlerin modüler ve yönetilebilir parçalara ayrılmasına olanak tanır.
Tipik bir C# çözüm yapısı şu şekilde görünebilir:
MyApplication.sln
├── src
│ ├── MyApplication.Domain (DDD Entities, Value Objects, Aggregates, Domain Services, Repository Interfaces)
│ ├── MyApplication.Application (Use Cases, Commands, Queries, Handlers, Application Services, DTOs)
│ ├── MyApplication.Infrastructure (EF Core, Third-Party APIs, Repository Implementations, Caching)
│ ├── MyApplication.Presentation.WebAPI (ASP.NET Core Controllers, ViewModels, Dependency Injection Setup)
│ └── MyApplication.CrossCutting (Shared concerns like IoC container setup, common helpers)
└── tests
├── MyApplication.Domain.Tests
├── MyApplication.Application.Tests
├── MyApplication.Infrastructure.Tests
└── MyApplication.Presentation.WebAPI.Tests
Neden C# Projelerinde Kullanılmalılar?
C# projelerinde Clean Architecture ve DDD’yi birleştirmek, bir dizi önemli avantaj sunar:
- Sürdürülebilirlik: Katmanlı yapı ve iş mantığının izole edilmesi, kod tabanını daha düzenli ve anlaşılır hale getirir. Bu, yeni özelliklerin eklenmesini ve mevcut özelliklerin bakımını kolaylaştırır.
- Test Edilebilirlik: Bağımlılıkların içe doğru akması ve arayüzlerin kullanılması, her katmanın ve bileşenin kolayca test edilmesini sağlar. Özellikle Domain ve Application katmanları dış bağımlılıklardan arındığı için hızlı ve güvenilir birim testleri yazılabilir.
- Ölçeklenebilirlik ve Esneklik: İş mantığı altyapı detaylarından bağımsız olduğu için, veritabanı veya UI teknolojisi gibi dış katmanlar gerektiğinde daha kolay değiştirilebilir veya ölçeklendirilebilir.
- İş Odaklı Geliştirme: DDD, geliştiricilerin iş alanını ve iş gereksinimlerini daha derinlemesine anlamasına yardımcı olur. Ortak Dil ve Bounded Context’ler, iş uzmanları ve geliştiriciler arasındaki iletişimi güçlendirir.
- Daha İyi İletişim: Ubiquitous Language, iş terimlerinin kodda doğrudan yansıtılmasıyla herkesin aynı dili konuşmasını sağlar, bu da yanlış anlaşılmaları ve hataları azaltır.
- Yüksek Kaliteli Yazılım: Her iki yaklaşım da, özellikle karmaşık iş alanlarında yüksek kaliteli, sağlam ve hatasız yazılımlar geliştirmeye odaklanır.
Özetle, Clean Architecture ve DDD, C# projelerinde güçlü, sürdürülebilir ve esnek yazılım çözümleri geliştirmek için vazgeçilmez yaklaşımlardır. Clean Architecture katmanlı yapısıyla bağımlılıkları yönetirken, DDD iş alanının karmaşıklığını modellemeyi sağlar. Bu iki metodolojinin birleşimi, iş mantığına odaklanmayı, test edilebilirliği artırmayı ve uzun vadede bakımı kolaylaştırmayı garanti eder. Böylece, değişen gereksinimlere kolayca adapte olabilen yüksek kaliteli sistemler inşa edilebilir.