Clean Architecture: Sürdürülebilir Kod için İlkeler ve Uygulamalar

Kardel Rüveyda ÇETİN
20 min readJul 27, 2024

--

Modern yazılım geliştirme dünyasında, projelerin karmaşıklığı ve büyüklüğü hızla artmakta; bu da sistemlerin sürdürülebilirliğini ve bakımını zorlaştırmaktadır. Yazılım mühendisleri, bu zorlukların üstesinden gelmek ve projeleri daha yönetilebilir hale getirmek için çeşitli mimari yaklaşımlar geliştirmiştir. Bu yaklaşımlardan biri de Clean Architecture’dır. Peki, Clean Architecture nedir ve neden bu kadar önemlidir?

Clean Architecture, Robert C. Martin tarafından önerilen bir yazılım mimarisi modelidir. Bu model, uygulamanın bağımsız ve modüler bir şekilde yapılandırılmasını sağlayarak, hem yazılım geliştirme sürecini hem de yazılımın bakımını kolaylaştırır. Temel amacı, uygulamanın her bir bileşenini ve katmanını net bir şekilde tanımlayarak, değişikliklerin ve eklemelerin mevcut sistemi bozmasını önlemektir.

Bu makalede, Clean Architecture’ın temel prensiplerini, yapı taşlarını ve uygulama aşamalarını derinlemesine inceleyeceğiz.

( Bu yazıya ek olarak değerli ekip arkadaşım Hamza Ağar ile birlikte her katmanı aşamalı bir şekilde anlatmaya çalışacağımız yazı ve video serisi de oluşturmaya çalışacağız. Umarım bu seri herkese fayda sağlar. :) )

Peki neden önemli Clean Architecture?

Clean Architecture, yazılım projelerinde karmaşıklığı azaltmayı ve sürdürülebilirliği artırmayı hedefleyen bir yaklaşımdır. Bu mimari, sistemi bağımsız ve modüler katmanlara ayırarak her bir katmanın belirli bir sorumluluğa sahip olmasını sağlar. Örneğin, Domain katmanı iş mantığını ve kurallarını içerirken, veri erişim detayları Application katmanında yer alır. Bu yapı, bir katmanda yapılan değişikliklerin diğer katmanları etkilemesini önler ve dolayısıyla yazılımın sürdürülebilirliğini artırır.

Örneğin, bir e-ticaret uygulamasında ürünlerin yönetimi ve sipariş süreçleri Domain katmanında tanımlanabilir. Eğer veri erişim katmanında bir değişiklik yapılması gerekiyorsa, bu değişiklik yalnızca Application ve Infrastructure katmanlarını etkiler; Domain katmanındaki iş mantığı ve kuralları etkilenmeden kalır. Böylece, iş mantığının net bir şekilde odaklanması sağlanır ve veri erişim değişiklikleri uygulamanın genel işleyişini bozmadan yapılabilir.

Clean Architecture, yazılımın modülerliğini ve esnekliğini artırarak, bakım ve genişletme işlemlerini kolaylaştırır. Örneğin, yeni bir özellik eklemek veya mevcut bir özelliği güncellemek, yalnızca ilgili katmanda değişiklik yapılarak gerçekleştirilebilir, bu da yazılımın uzun ömürlü olmasına katkıda bulunur. Ayrıca, bu yapı test süreçlerini de kolaylaştırır; çünkü her bir katman bağımsız olarak test edilebilir ve böylece sistemin çeşitli parçaları üzerinde detaylı testler yapılabilir.

Sonuç olarak, Clean Architecture, yazılım geliştirme sürecini daha verimli hale getirir ve projelerin değişen gereksinimlere uyum sağlamasını kolaylaştırır. Bu mimari yaklaşım, yazılımın yüksek kaliteli ve sürdürülebilir olmasını sağlar, böylece uzun vadeli projelerde karşılaşılabilecek zorlukların üstesinden gelinmesini kolaylaştırır.

Hiç mi dezavantajı yok ?

Clean Architecture, birçok avantaj sunarak yazılım projelerinde düzen ve sürdürülebilirlik sağlar, ancak bazı dezavantajları da beraberinde getirebilir. Öncelikle, bu mimarinin öğrenme eğrisi oldukça dik olabilir. Özellikle yeni başlayanlar için, katmanlı yapıyı ve bağımlılıkları yönetmek karmaşık ve kafa karıştırıcı olabilir. Bu, özellikle temel bilgileri henüz tam kavrayamayan ekipler için zorlayıcı olabilir.

Başlangıçta, Clean Architecture’ı uygulamak daha fazla zaman ve çaba gerektirebilir. Projeye fazla katman eklemek, geliştirme sürecini uzatabilir ve ek maliyetlere yol açabilir. Özellikle küçük projelerde veya bütçe kısıtlamaları olan durumlarda, bu ekstra çaba bazen gereksiz görünebilir.

Performans açısından da bence dikkat edilmesi gereken noktalar var. Katmanlar arasında veri alışverişi yapıldığında, bu durum bazen performans sorunlarına yol açabilir. Yüksek performans gerektiren uygulamalarda, bu ek yükler gözle görülür şekilde etkileyebilir. Ancak, bu genellikle uygulamanın büyüklüğüne ve karmaşıklığına bağlıdır.

Ayrıca, Clean Architecture, aşırı soyutlama yapma eğilimindedir ve bu, projeyi gereksiz yere karmaşıklaştırabilir. Küçük ve basit uygulamalar için bu kadar fazla katman ve soyutlama kullanmak, kodun okunabilirliğini zorlaştırabilir ve gereksiz bir karmaşıklık yaratabilir.

Bağımlılıkların yönetimi de bazen kafa karıştırıcı olabilir. Katmanlar arasındaki bağımlılıkları doğru bir şekilde yönetmek, özellikle büyük ekiplerde sürekli dikkat ve koordinasyon gerektirebilir. Bu, bazen projenin yönetimini zorlaştırabilir. Bunun için farklı yöntemleri kullanmak gerekir. Aksi takdirde yoğun bir kod tekrarına sebebiyet verebiliriz.

Son olarak, Clean Architecture, projeye ek katmanlar ekleyerek kod kütlesini artırabilir. Bu ekstra katmanlar, özellikle küçük projelerde, fazla kod ve yönetim overhead’ine neden olabilir. Bu durum, bakım ve proje yönetimini karmaşıklaştırabilir.

Yani bence ( yatırım tavsiyesi değildir. ) Clean Architecture büyük ve karmaşık projelerde birçok fayda sağlasa da, her durumda en iyi çözüm olmayabilir. Projeye ve ekibin ihtiyaçlarına uygun bir yaklaşımı seçmek her zaman en iyisidir diye düşünüyorum. :)

Clean Architecture için Mimari Prensipler ve Tasarım Prensipleri

Clean Architecture’ın temel amacı, yazılım projelerinde bağımsız, modüler ve sürdürülebilir bir yapı oluşturarak karmaşıklığı azaltmak ve bakım süreçlerini kolaylaştırmaktır. Bu mimari, katmanlı yapısı sayesinde iş mantığını veri erişimi veya kullanıcı arayüzünden izole eder, her katmanın bağımsız çalışabilmesini sağlar ve yazılımın daha kolay test edilebilir ve esnek olmasını mümkün kılar. Ayrıca, bağımlılıkların kontrol edilmesini ve kodun tekrar kullanılabilirliğini artırarak yazılımın uzun vadede daha sürdürülebilir olmasını hedefler.

Separation of Concerns (SoC)

Separation of Concerns, yazılımın farklı sorumluluklarının ayrı modüllere veya katmanlara ayrılması prensibidir. Bu prensip, kodun daha organize ve anlaşılır olmasını sağlar. Örneğin, veri erişim kodu, iş mantığı kodundan ayrılmalıdır. Bu şekilde, veri erişim katmanındaki değişiklikler iş mantığını etkilemez ve tersi de geçerlidir. SoC, kodun bakımını ve test edilmesini kolaylaştırır çünkü belirli bir sorumluluğa sahip olan modüller veya katmanlar daha kolay anlaşılır ve izole edilebilir.

Veri Erişim Katmanı (Data Access Layer)

public class ProductRepository
{
private readonly DbContext _dbContext;

public ProductRepository(DbContext dbContext)
{
_dbContext = dbContext;
}

public Product GetProductById(int id)
{
return _dbContext.Products.Find(id);
}

public void Save(Product product)
{
_dbContext.Products.Add(product);
_dbContext.SaveChanges();
}
}

İş Mantığı Katmanı (Business Logic Layer):

public class ProductService
{
private readonly ProductRepository _productRepository;

public ProductService(ProductRepository productRepository)
{
_productRepository = productRepository;
}

public Product GetProductDetails(int id)
{
return _productRepository.GetProductById(id);
}

public void AddProduct(Product product)
{
_productRepository.Save(product);
}
}

Encapsulation

Encapsulation, verilerin ve işlevlerin dışarıdan erişilmesini sınırlandırarak sadece gerekli olan bilgilerin açığa çıkarılması prensibidir. Bu, bir nesnenin iç durumunu ve işlevlerini korur ve sadece belirli bir arayüz üzerinden erişime izin verir.

Encapsulation, nesneler arasındaki bağımlılıkları azaltır ve kodun daha güvenli ve sağlam olmasını sağlar. Örneğin, bir sınıfın verileri doğrudan erişime açmak yerine, bu verilere erişim ve değişiklik yapma işlemlerini metodlar aracılığıyla yönetmek encapsulation örneğidir.

Veri ve işlevleri gizleyerek dışarıdan erişimi sınırlandırabiliriz.

public class BankAccount
{
private decimal _balance;

public void Deposit(decimal amount)
{
if (amount > 0)
{
_balance += amount;
}
}

public void Withdraw(decimal amount)
{
if (amount > 0 && amount <= _balance)
{
_balance -= amount;
}
}

public decimal GetBalance()
{
return _balance;
}
}

Bu kod örneğinde encapsulation, _balance değişkeninin doğrudan erişimini engelleyerek ve bu değişkene erişim ve değişiklik yapmanın kontrollü yollarını sağlayarak uygulanmıştır. Bu yaklaşım, nesnelerin iç durumunu korur, hata yapma olasılığını azaltır ve kodun daha güvenli ve sağlam olmasını sağlar.

Dependency Inversion

Dependency Inversion prensibi, bağımlılıkların daha soyut katmanlara doğru yöneltilmesini savunur. Bu prensip, üst seviye modüllerin alt seviye modüllere bağımlı olmaması gerektiğini belirtir. Bunun yerine, her iki modül de soyut arayüzlere bağımlı olmalıdır. Bu yaklaşım, kodun daha esnek ve genişletilebilir olmasını sağlar. Örneğin, bir veritabanı erişim katmanının iş mantığı katmanına doğrudan bağımlı olmaması, bunun yerine bir arayüz aracılığıyla iletişim kurması dependency inversion prensibine uygundur.

public interface IMessageSender
{
void SendMessage(string message);
}
public class EmailSender : IMessageSender
{
public void SendMessage(string message)
{

}
}
public class NotificationService
{
private readonly IMessageSender _messageSender;

public NotificationService(IMessageSender messageSender)
{
_messageSender = messageSender;
}

public void Notify(string message)
{
_messageSender.SendMessage(message);
}
}

Yukarıdaki örnekle bağımlılıkların soyut arayüzlere doğru yöneltilmesini sağlayarak Dependency Inversion prensibini uygulayabiliriz.

Explicit Dependencies

Explicit Dependencies prensibi, bir sınıfın tüm bağımlılıklarını açıkça belirtmesi gerektiğini savunur. Bu, bağımlılıkların dışarıdan sağlanması gerektiği anlamına gelir ve genellikle bağımlılık enjeksiyonu (Dependency Injection) yoluyla gerçekleştirilir. Bu yaklaşım, sınıfların daha kolay test edilmesini sağlar çünkü bağımlılıklar açıkça tanımlanmıştır ve kolayca taklit edilebilir veya değiştirilerek test edilebilir.

public class OrderService
{
private readonly IOrderRepository _orderRepository;

public OrderService(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}

public void PlaceOrder(Order order)
{
_orderRepository.Save(order);
}
}

Bu kodda, OrderService sınıfı bir IOrderRepository arayüzüne bağımlıdır. Bu bağımlılık, OrderService'in constructor'ında açıkça belirtilmiştir. Yani, OrderService sınıfı, IOrderRepository türünde bir bağımlılığa sahip olduğunu ve bu bağımlılığın dışarıdan sağlanması gerektiğini ifade eder. Bu sayede, OrderService sınıfı, bağımlılığını (yani IOrderRepository) doğrudan oluşturmak yerine, dışarıdan sağlanan bir nesne ile çalışır. Bu yaklaşım, sınıfın test edilmesini kolaylaştırır çünkü IOrderRepository arayüzünü implement eden farklı nesneler (örneğin, mock objeler) ile testler yapılabilir. Böylece, OrderService'in işlevselliği, gerçek veri erişim katmanı yerine test amaçlı oluşturulmuş nesnelerle doğrulanabilir. Bu, kodun esnekliğini ve test edilebilirliğini artırır.

Single Responsibility

Single Responsibility prensibi, her bir sınıfın veya modülün sadece bir sorumluluğa sahip olması gerektiğini belirtir. Bu, kodun daha modüler ve anlaşılır olmasını sağlar. Her sınıf veya modül, belirli bir işlevi yerine getirmek için tasarlanmıştır ve bu işlev dışında başka sorumlulukları yoktur. Örneğin, bir sipariş yönetim sisteminde, bir sınıfın sadece siparişleri yönetmesi, başka bir sınıfın ise ürünleri yönetmesi single responsibility prensibine uygun bir yaklaşımdır.

public class Order
{
public int OrderId { get; set; }
public DateTime OrderDate { get; set; }
public List<Product> Products { get; set; }
}

public class OrderRepository : IOrderRepository
{
private readonly DbContext _dbContext;

public OrderRepository(DbContext dbContext)
{
_dbContext = dbContext;
}

public void Save(Order order)
{
_dbContext.Orders.Add(order);
_dbContext.SaveChanges();
}

public Order GetById(int orderId)
{
return _dbContext.Orders.Find(orderId);
}
}

DRY (Don’t Repeat Yourself)

DRY prensibi, tekrar eden kod parçalarının önlenmesi gerektiğini savunur. Aynı kodun birden fazla yerde bulunması, bakım sürecini zorlaştırır ve hata yapma olasılığını artırır. DRY prensibi, kodun tekrar kullanımı yoluyla daha temiz ve yönetilebilir bir yapıya kavuşturulmasını sağlar. Örneğin, aynı iş mantığını birden fazla yerde kullanmak yerine, bu mantığı bir metod veya sınıf içine alarak tekrar kullanmak DRY prensibine uygun bir yaklaşımdır.

public class MathHelper
{
public static int Add(int a, int b)
{
return a + b;
}

public static int Subtract(int a, int b)
{
return a - b;
}

// Diğer matematiksel işlemler...
}

Bu kodda, Add ve Subtract metodları, iki sayıyı toplama ve çıkarma işlemlerini gerçekleştiren temel işlevleri temsil eder. Bu işlemler, MathHelper sınıfında tek bir yerde tanımlanmıştır. Eğer bu işlevler uygulamanın farklı yerlerinde ihtiyaç duyulursa, her defasında aynı iş mantığını tekrar yazmak yerine, bu metodlar doğrudan çağrılabilir. Bu yaklaşım, kodun tekrarını önler ve aynı işlevselliği tek bir yerde toplar, böylece bakım ve yönetim işlemleri daha kolay hale gelir.

public class Calculator
{
public int ComputeSum(int x, int y)
{
return MathHelper.Add(x, y);
}

public int ComputeDifference(int x, int y)
{
return MathHelper.Subtract(x, y);
}
}

Burada, Calculator sınıfı, MathHelper sınıfındaki metodları tekrar kullanarak aynı işlevleri sağlar. Böylece, matematiksel işlemlerle ilgili kod tekrarını önlemiş ve bakımını daha basit hale getirmiş olur. Ayrıca, eğer matematiksel işlemlerinde herhangi bir değişiklik yapılması gerekirse, bu değişiklikler MathHelper sınıfında tek bir yerde yapılabilir, dolayısıyla kodun diğer yerlerinde de aynı değişiklik otomatik olarak uygulanır. Bu, DRY prensibinin etkin bir şekilde uygulandığı bir örnektir.

Persistence Ignorance

Persistence Ignorance prensibi, iş mantığının veri erişim katmanından bağımsız olması gerektiğini savunur. İş mantığı, verilerin nasıl saklandığını veya alındığını bilmemelidir. Bu, veri erişim katmanındaki değişikliklerin iş mantığını etkilememesini sağlar. Örneğin, iş mantığı katmanında kullanılan nesnelerin, veritabanı ile ilgili detayları içermemesi persistence ignorance prensibine uygun bir yaklaşımdır.

public class OrderService
{
private readonly IOrderRepository _orderRepository;

public OrderService(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}

public void PlaceOrder(Order order)
{
_orderRepository.Save(order);
}

public Order GetOrder(int orderId)
{
return _orderRepository.GetById(orderId);
}
}

Bu yaklaşım, iş mantığının veri erişim detaylarından bağımsız olmasını sağlar, böylece veri erişim katmanında yapılacak değişiklikler iş mantığını etkilemez. Ayrıca, bu yapı iş mantığının test edilmesini de kolaylaştırır çünkü veri erişim katmanının implementasyonu taklit edilerek testler yapılabilir.

Bounded Context (DDD)

Bounded Context prensibi, bir sistemin farklı alanlarının belirli sınırlar içinde tanımlanmasını savunur. Bu prensip, Domain Driven Design (DDD) yaklaşımının bir parçasıdır ve bir sistemin farklı alanlarının bağımsız olarak yönetilebilmesini sağlar. Her bounded context, kendi iş mantığı ve veri modeli ile çalışır ve diğer bounded context’ler ile belirli arayüzler aracılığıyla iletişim kurar. Bu, karmaşık sistemlerin daha yönetilebilir ve anlaşılır olmasını sağlar.

public class CustomerContext
{
// Müşteri ile ilgili iş mantığı ve veri modeli
}

public class OrderContext
{
// Sipariş ile ilgili iş mantığı ve veri modeli
}

// Müşteri ve sipariş context'leri arasında iletişim arayüzleri
public interface ICustomerService
{
Customer GetCustomerById(int id);
}

public interface IOrderService
{
Order GetOrderById(int id);
}

Clean Architecture Katmanları

Clean Architecture, yazılım projelerinde sürdürülebilirliği, modülerliği ve bakım kolaylığını sağlamak için geliştirilmiş bir mimari yaklaşımdır. Bu mimari, sistemi dört ana katmana ayırarak her bir katmanın belirli sorumlulukları üstlenmesini sağlar: Domain Layer, Application Layer, Infrastructure Layer ve Presentation Layer.

Her katman, belirli görevleri yerine getirir ve diğer katmanlardan bağımsız çalışabilir. Bu mimariyi daha iyi anlamak için Clean Architecture’ın her katmanını ve bileşenini basit bir e ticaret sistemi örneğiyle farklı parçalarıyla ilişkilendirerek açıklayalım.

  • Domain Layer: Sipariş ve sipariş öğesi gibi temel (entity)varlıkları, para birimi gibi değer nesnelerini, sipariş yerleştirme gibi önemli olayları ve sipariş iş mantığını kontrol eden servisleri içerir.
  • Application Layer: Siparişleri yerleştirme ve yönetme işlemlerini organize eden uygulama servislerini, komut ve sorgu işlemlerini içerir.
  • Infrastructure Layer: Veritabanı, mesajlaşma sistemleri ve e-posta sağlayıcıları gibi dış sistemlerle etkileşimi yöneten katmandır. Örneğin, siparişlerin veritabanına kaydedilmesi ve sipariş onay e-postalarının gönderilmesi bu katmanda gerçekleştirilir.
  • Presentation Layer: Kullanıcıların sisteme erişimini sağlayan katmandır. Örneğin, REST API uç noktaları üzerinden siparişlerin oluşturulması ve sorgulanması sağlanır. Bu katman ayrıca, ara katman yazılımları ve bağımlılık enjeksiyonu ayarlarını içerir.

Domain Layer: İş Mantığı ve Kuralların Bulunduğu Katman

Domain Layer, iş mantığı ve kurallarının tanımlandığı katmandır. Bu katman, diğer katmanlardan bağımsızdır ve uygulamanın kalbi olarak kabul edilir.

Entities (Varlıklar)

Entities, iş mantığının temel taşlarıdır ve genellikle bir kimliğe (ID) sahiptirler.

public class Order
{
public int Id { get; private set; }
public string CustomerName { get; private set; }
public DateTime OrderDate { get; private set; }
public List<OrderItem> Items { get; private set; }

public Order(int id, string customerName, DateTime orderDate)
{
Id = id;
CustomerName = customerName;
OrderDate = orderDate;
Items = new List<OrderItem>();
}

public void AddItem(OrderItem item)
{
Items.Add(item);
}
}

public class OrderItem
{
public int Id { get; private set; }
public string ProductName { get; private set; }
public int Quantity { get; private set; }
public decimal Price { get; private set; }

public OrderItem(int id, string productName, int quantity, decimal price)
{
Id = id;
ProductName = productName;
Quantity = quantity;
Price = price;
}
}

Bu örnekte, Order (Sipariş) ve OrderItem (Sipariş Öğesi) adlı iki entity(varlık ) oluşturduk. Order varlığı, bir siparişin temel özelliklerini (kimlik, müşteri adı, sipariş tarihi ve sipariş öğeleri listesi) içerir. OrderItem varlığı ise sipariş öğesinin temel özelliklerini (kimlik, ürün adı, miktar ve fiyat) içerir.

Value Objects (Değer Nesneleri)

Value Objects (Değer Nesneleri), kimliklerinden ziyade sahip oldukları değerlerle tanımlanan nesnelerdir. Bu tür nesneler, genellikle değişmezdir (immutable) ve iki nesnenin eşit olup olmadığını belirlemek için değerleri kullanılır. Değer nesneleri, yazılımda tutarlılığı ve doğruluğu sağlamak için kullanılır.

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

public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}

public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Different currencies!");

return new Money(Amount + other.Amount, Currency);
}
}

Money sınıfı, belirli bir miktar ve para birimi içeren değişmez bir değer nesnesidir. Add metodu, aynı para birimindeki iki Money nesnesini birleştirerek yeni bir Money nesnesi oluşturur. Bu, para birimi dönüştürme işlemlerinin düzgün ve güvenli bir şekilde yönetilmesini sağlar.

Domain Events (Alan Olayları)

Sistem içinde meydana gelen önemli olaylardır.

public class OrderPlacedEvent
{
public int OrderId { get; }
public DateTime PlacedDate { get; }

public OrderPlacedEvent(int orderId, DateTime placedDate)
{
OrderId = orderId;
PlacedDate = placedDate;
}
}

Bu örnekte, OrderPlacedEvent (Sipariş Verildi Olayı) adlı bir domain olayı oluşturduk. Bu olay, bir siparişin verildiği zamanı ve sipariş kimliğini içerir.

Domain Services (Alan Servisleri)

Domain Services (Alan Servisleri), yazılımın iş mantığını ve kurallarını kapsayan, bağımsız ve tekrar kullanılabilir servislerdir. Bu servisler, belirli iş kurallarını ve mantığını uygulamak için kullanılır ve genellikle bir veya birden fazla entity üzerinde operasyonlar gerçekleştirir.

public class OrderService
{
public bool CanPlaceOrder(Order order)
{
// İş kurallarını kontrol et
return order.Items.Any() && order.OrderDate <= DateTime.Now;
}
}

OrderService sınıfı, bir siparişin geçerli olup olmadığını belirlemek için iki iş kuralı uygulayan bir domain servisidir. Siparişin geçerli olabilmesi için:

  • Siparişte en az bir ürün bulunmalıdır.
  • Sipariş tarihi, mevcut tarihten ileri bir tarih olmamalıdır.

Bu tür domain servisleri, iş mantığını merkezi bir noktada toplar ve uygulamanın diğer bölümlerinden izole ederek daha temiz ve yönetilebilir bir kod yapısı sağlar.

Interfaces (Arayüzler)

Interfaces (Arayüzler), bir sınıfın hangi yöntemleri veya özellikleri sağlaması gerektiğini belirleyen sözleşmelerdir. Arayüzler, özellikle bağımlılıkların yönetiminde ve sınıfların birbirleriyle nasıl etkileşime girdiğini tanımlamada kullanılır. Arayüzler, bağımlılıkların somut sınıflara değil, soyutlamalara dayandırılmasını sağlar. Bu, kodun daha esnek ve test edilebilir olmasına yardımcı olur.

public interface IOrderRepository
{
void Save(Order order);
Order GetById(int orderId);
}
public class SqlOrderRepository : IOrderRepository
{
public void Save(Order order)
{
// Siparişi SQL veritabanına kaydet
}

public Order GetById(int orderId)
{
// Verilen kimlik ile siparişi SQL veritabanından al ve döndür
return new Order();
}
}

Arayüzler, yazılımda esneklik ve bağımsızlık sağlamak için kullanılır. Bir arayüz, belirli bir işlevselliği sağlayan metodları tanımlar ve bu metodların nasıl uygulanacağını belirlemez. Bu, kodun daha modüler, test edilebilir ve sürdürülebilir olmasını sağlar.

Exceptions (İstisnalar)

Özel durumlar (exceptions), sistemde meydana gelen hataların veya olağan dışı durumların yönetilmesini sağlar. Bu durumlar, uygulamanın düzgün çalışmasını sağlamak için özel olarak tasarlanmış hata sınıflarını içerir.

Örneğin, bir siparişin geçersiz olduğu durumlarda özel bir durum sınıfı oluşturabiliriz:

public class InvalidOrderException : Exception
{
public InvalidOrderException(string message) : base(message)
{
}

public InvalidOrderException(string message, Exception innerException) : base(message, innerException)
{
}
}

Enums (Sabit Değerler)

Enums (Sabit Değerler), sabit değerlerin tanımlandığı yapılardır ve belirli bir grup sabit değeri temsil ederler. Enums, kodun okunabilirliğini artırır ve belirli bir grup içindeki geçerli değerleri sınırlayarak hata olasılığını azaltır. Örneğin, bir siparişin durumu için kullanılacak sabit değerleri enum ile tanımlamak, geçersiz durumların kullanılmasını önler ve kodun daha anlaşılır olmasını sağlar.

public enum OrderStatus
{
Pending, // Sipariş beklemede
Processed, // Sipariş işlendi
Shipped, // Sipariş gönderildi
Delivered, // Sipariş teslim edildi
Canceled // Sipariş iptal edildi
}
public class Order
{
public int Id { get; set; }
public OrderStatus Status { get; set; }
public DateTime OrderDate { get; set; }
public List<OrderItem> Items { get; set; }
}

public class OrderService
{
public void UpdateOrderStatus(Order order, OrderStatus newStatus)
{
// Sipariş durumunu güncelle
order.Status = newStatus;
Console.WriteLine($"Order {order.Id} status updated to {order.Status}");
}

public bool IsOrderShipped(Order order)
{
// Siparişin gönderilip gönderilmediğini kontrol et
return order.Status == OrderStatus.Shipped;
}
}

Enums, sabit değerlerin yönetiminde güçlü bir araçtır. Kodun okunabilirliğini artırır ve belirli bir grup içindeki geçerli değerleri sınırlandırarak hata olasılığını azaltır. Enums kullanarak, belirli bir durum veya değeri temsil eden sabit değerleri daha anlamlı ve organize bir şekilde yönetebilirsiniz. Bu, özellikle iş süreçlerinde veya durum geçişlerinde çok kullanışlıdır ve yazılımın sürdürülebilirliğine önemli katkı sağlar.

Application Layer: İş Mantığını Yöneten Katman

Application Layer, Domain Layer’ı yöneten ve iş mantığını organize eden katmandır. Bu katman, kullanıcıların isteklerini işler ve gerekli iş mantığını çalıştırır.

Application Services (Uygulama Servisleri)

Application Services (Uygulama Servisleri), iş mantığını uygulama katmanında yönetmek ve organize etmek için kullanılan servislerdir. Bu servisler, genellikle bir iş sürecini kapsayan işlemleri gerçekleştirmek, çeşitli domain servislerini çağırmak ve veritabanı etkileşimlerini yönetmek için kullanılır. Uygulama servisleri, iş mantığını içerir ve domain katmanından gelen iş kurallarını uygulayarak bu kuralların sağlandığından emin olur. Ayrıca, bu katman diğer katmanlar arasındaki iletişimi koordine eder ve genellikle kullanıcı girdilerini alıp domain modellerine dönüştürür.

public class OrderApplicationService
{
private readonly IOrderRepository _orderRepository;
private readonly OrderService _orderService;

public OrderApplicationService(IOrderRepository orderRepository, OrderService orderService)
{
_orderRepository = orderRepository;
_orderService = orderService;
}

public void PlaceOrder(Order order)
{
if (!_orderService.CanPlaceOrder(order))
{
throw new InvalidOrderException("Order cannot be placed due to validation issues.");
}

_orderRepository.Save(order);
var orderPlacedEvent = new OrderPlacedEvent(order.Id, DateTime.Now);
// Event publishing code here
}
}

CQRS (Komut ve Sorgu İşlemleri)

CQRS (Command and Query Responsibility Segregation), komut (write) ve sorgu (read) işlemlerini birbirinden ayıran bir tasarım desenidir. Bu desen, sistemin okuma ve yazma işlemlerini ayrı modeller üzerinden yapmasını sağlar. Bu sayede, veri mutasyonları ve sorguları farklı sorumluluklara sahip bileşenler tarafından ele alınır, böylece kodun okunabilirliği ve bakımı kolaylaşır.

Neden CQRS Kullanmalıyız?

  • Okuma ve Yazma İşlemlerinin Ayrılması: CQRS, okuma ve yazma işlemlerinin ayrı modeller üzerinden yapılmasını sağlar. Bu, her iki işlem türünün de kendi ihtiyaçlarına göre optimize edilmesine olanak tanır.
  • Performans ve Ölçeklenebilirlik: Okuma ve yazma işlemlerinin ayrılması, her iki işlem için farklı veri depolama çözümlerinin kullanılmasını mümkün kılar. Bu da performans ve ölçeklenebilirlik avantajları sağlar.
  • Geliştirilmiş Kod Bakımı: Komutlar ve sorguların ayrı ayrı ele alınması, kodun daha anlaşılır ve bakımının daha kolay olmasını sağlar.
  • Paralel Geliştirme: CQRS deseninde, okuma ve yazma modelleri bağımsız olarak geliştirilebilir ve test edilebilir. Bu, ekiplerin aynı anda farklı bölümler üzerinde çalışabilmesini sağlar.
// Command
public class PlaceOrderCommand
{
public Order Order { get; }

public PlaceOrderCommand(Order order)
{
Order = order;
}
}

// Query
public class GetOrderByIdQuery
{
public int Id { get; }

public GetOrderByIdQuery(int id)
{
Id = id;
}
}
  • PlaceOrderCommand sınıfı, bir sipariş yerleştirme işlemini temsil eder. Bu komut, Order nesnesini içerir ve siparişin veritabanına kaydedilmesi gibi yazma işlemlerini gerçekleştirir.
  • Order nesnesi, bu komutun çalıştırılması sırasında gerekli olan tüm sipariş bilgilerini kapsar.
  • GetOrderByIdQuery sınıfı, belirli bir siparişin bilgilerini sorgulama işlemini temsil eder. Bu sorgu, Id alanını içerir ve bu kimlik bilgisine sahip siparişin detaylarını getirir.
  • Id alanı, sorgunun hangi siparişin bilgilerini getireceğini belirtir.
public class PlaceOrderCommandHandler
{
private readonly IOrderRepository _orderRepository;
private readonly OrderService _orderService;

public PlaceOrderCommandHandler(IOrderRepository orderRepository, OrderService orderService)
{
_orderRepository = orderRepository;
_orderService = orderService;
}

public void Handle(PlaceOrderCommand command)
{
if (!_orderService.CanPlaceOrder(command.Order))
{
throw new InvalidOrderException("Order cannot be placed due to validation issues.");
}

_orderRepository.Save(command.Order);
var orderPlacedEvent = new OrderPlacedEvent(command.Order.Id, DateTime.Now);
// Event publishing code here
}
}
  • PlaceOrderCommandHandler sınıfı, PlaceOrderCommand komutunu işler. Bu işlem sırasında, siparişin geçerliliğini kontrol eder ve geçerli ise siparişi veritabanına kaydeder.
  • OrderService sınıfı kullanılarak siparişin geçerliliği kontrol edilir. Eğer sipariş geçersizse, InvalidOrderException fırlatılır.
  • Sipariş geçerli ise, IOrderRepository aracılığıyla veritabanına kaydedilir ve bir OrderPlacedEvent oluşturularak ilgili etkinlik yayınlanır.
public class GetOrderByIdQueryHandler
{
private readonly IOrderRepository _orderRepository;

public GetOrderByIdQueryHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}

public Order Handle(GetOrderByIdQuery query)
{
return _orderRepository.GetById(query.Id);
}
}
  • GetOrderByIdQueryHandler sınıfı, GetOrderByIdQuery sorgusunu işler. Bu işlem sırasında, sorgunun kimliğine sahip sipariş bilgilerini getirir.
  • IOrderRepository kullanılarak, belirtilen kimlik bilgisine sahip sipariş veritabanından çekilir ve geri döndürülür.

CQRS, komut ve sorgu işlemlerini ayırarak yazılımın modülerliğini ve performansını artıran bir tasarım desenidir. Bu desen, veri mutasyonları ve sorgularını farklı bileşenler üzerinden ele alarak kodun daha okunabilir ve bakımı kolay olmasını sağlar. Örnek kodlar, sipariş yerleştirme ve sorgulama işlemlerini nasıl ayrı ayrı ele alabileceğimizi ve bu işlemleri nasıl yöneteceğimizi göstermektedir. Bu yaklaşım, yazılım projelerinde daha temiz, sürdürülebilir ve yönetilebilir bir yapı oluşturmak için önemli bir adım sağlar.

Orchestrators (Orkestratörler)

Orkestratörler, iş mantığını organize eden ve yöneten bileşenlerdir. Bu bileşenler, farklı hizmetler ve iş mantığı arasındaki etkileşimi koordine eder, böylece daha karmaşık iş akışlarını yönetmeyi kolaylaştırır. Orkestratörler, belirli bir iş sürecini yürütmek için gerekli olan adımları belirler ve sırasıyla bu adımları çağırır. Bu sayede, iş mantığı daha düzenli ve yönetilebilir hale gelir.

Orchestrator Kullanımının Avantajları:

  • Karmaşıklığın Yönetimi: İş mantığını organize ederek ve farklı servisler arasındaki etkileşimi yöneterek kodun karmaşıklığını azaltır.
  • Modülerlik: İş mantığı adımlarını ayrı bileşenler olarak ele alarak modüler bir yapı sağlar. Bu, bileşenlerin bağımsız olarak geliştirilebilmesini ve test edilebilmesini kolaylaştırır.
  • Bakım Kolaylığı: İş akışlarının merkezi bir noktadan yönetilmesi, kodun bakımını ve genişletilmesini kolaylaştırır.
  • Yeniden Kullanılabilirlik: Belirli iş akışlarını temsil eden orkestratörler, farklı süreçlerde tekrar kullanılabilir, böylece kodun yeniden kullanılabilirliği artırılır.
public class OrderOrchestrator
{
private readonly OrderApplicationService _orderService;

public OrderOrchestrator(OrderApplicationService orderService)
{
_orderService = orderService;
}

public void HandlePlaceOrderCommand(PlaceOrderCommand command)
{
_orderService.PlaceOrder(command.Order);
}
}
  • OrderOrchestrator sınıfı, sipariş yerleştirme komutunu işlemek için kullanılan bir orkestratördür. Bu sınıf, OrderApplicationService aracılığıyla sipariş yerleştirme işlemini koordine eder.
  • Bu sınıf, iş akışını organize etmek ve yönlendirmekle sorumludur. Belirli bir iş süreci (örneğin, sipariş yerleştirme) gerçekleştirilirken gerekli adımları belirler ve sırasıyla bu adımları çağırır.
  • OrderOrchestrator sınıfının konstrüktörü, OrderApplicationService nesnesini bağımlılık olarak alır. Bu, orkestratörün gerekli iş mantığı hizmetlerini kullanmasını sağlar.
  • HandlePlaceOrderCommand metodu, PlaceOrderCommand komutunu işleyen bir yöntemdir. Bu yöntem, OrderApplicationService aracılığıyla sipariş yerleştirme işlemini gerçekleştirir.
  • Bu metod, komutun içerdiği siparişi alır ve OrderApplicationService sınıfının PlaceOrder metodunu çağırarak siparişi yerleştirir.

Orkestratörler, iş mantığını organize eden ve yöneten bileşenlerdir. OrderOrchestrator sınıfı, sipariş yerleştirme sürecini koordine eden bir örnek olarak kullanılmıştır. Bu sınıf, OrderApplicationService aracılığıyla iş akışını yönetir ve belirli bir iş sürecini (örneğin, sipariş yerleştirme) gerçekleştirmek için gerekli adımları sırasıyla çağırır. Orkestratörler, karmaşık iş süreçlerini daha yönetilebilir hale getirir, modülerlik sağlar ve kodun bakımını kolaylaştırır. Bu, yazılım projelerinde daha temiz, sürdürülebilir ve yönetilebilir bir yapı oluşturmak için önemli bir adımdır.

Infrastructure Layer: Dış Sistemlerle Etkileşimi Yöneten Katman

Infrastructure Layer, dış sistemlerle (veritabanları, mesajlaşma sistemleri, e-posta sağlayıcıları, kimlik doğrulama sistemleri vb.) etkileşimi yöneten katmandır.

Databases (Veritabanları)

Veritabanları, verilerin saklandığı ve yönetildiği sistemlerdir. Infrastructure katmanında yer alan OrderRepository sınıfı, veritabanı işlemlerini yöneterek verilerin saklanması ve erişilmesi görevini üstlenir. Bu sınıf, IOrderRepository arayüzünü uygulayarak sipariş verilerini veritabanına kaydeder ve belirli bir siparişi kimliğine göre bulur. Infrastructure katmanı, uygulamanın veri erişim ve dış dünya ile etkileşim detaylarını kapsayarak, diğer katmanların bu detaylardan bağımsız çalışmasını sağlar. Bu sayede, uygulamanın modülerliği ve sürdürülebilirliği artar.

public class OrderRepository : IOrderRepository
{
private readonly DbContext _context;

public OrderRepository(DbContext context)
{
_context = context;
}

public void Save(Order order)
{
_context.Orders.Add(order);
_context.SaveChanges();
}

public Order GetById(int id)
{
return _context.Orders.Find(id);
}
}

OrderRepository, IOrderRepository arayüzünü uygulayarak siparişleri veritabanına kaydeder ve alır. Save metodu, yeni bir siparişi veritabanına ekler ve değişiklikleri kaydeder. GetById metodu, bir siparişi kimliğine göre bulur.

Messaging (Mesajlaşma Sistemleri)

Messaging (mesajlaşma sistemleri), uygulamalar arasında veya uygulama içindeki bileşenler arasında veri iletimini ve iletişimi yönetmek için kullanılır. Bu sistemler, genellikle asenkron iletişim, olay tabanlı mimariler ve dağıtık sistemler için önemlidir. Mesajlaşma sistemleri, uygulamanın farklı parçaları arasında gevşek bağlılık ve asenkron işlem yapma yeteneği sağlar.

public class MessageBus
{
public void Publish<T>(T message)
{
// Mesaj yayınlama kodu
}
}

MessageBus sınıfı, mesajları yayınlayan bir yapıdır ve uygulama bileşenleri arasında gevşek bağlılık ve etkili iletişim sağlar. Bu sistemler, uygulamanın modülerliğini, esnekliğini ve ölçeklenebilirliğini artırır.

Email Providers (E-posta Sağlayıcıları)

E-posta sağlayıcıları, uygulamaların e-posta gönderme ve alma işlemlerini yönetmesini sağlar. Bu hizmetler genellikle kullanıcı bildirimleri, sistem uyarıları, doğrulama e-postaları ve diğer e-posta tabanlı iletişimler için kullanılır. E-posta sağlayıcıları, genellikle SMTP (Simple Mail Transfer Protocol) gibi protokoller kullanarak e-posta gönderimini gerçekleştirir.

public class EmailService
{
public void SendEmail(string recipient, string subject, string body)
{
// E-posta gönderme kodu
Console.WriteLine($"E-posta gönderildi: {recipient}");
Console.WriteLine($"Konu: {subject}");
Console.WriteLine($"İçerik: {body}");
}
}

EmailService sınıfı, temel e-posta gönderimi işlevlerini içerir ve genellikle e-posta tabanlı bildirimler, sistem uyarıları ve kullanıcı doğrulamaları gibi işlemler için kullanılır. Gerçek bir uygulamada, e-posta gönderme kodu SMTP veya diğer e-posta protoko

Storage Services (Depolama Servisleri)
Depolama servisleri, uygulamaların dosya ve veri saklama işlemlerini yönetmek için kullanılan bileşenlerdir. Bu servisler, uygulamanın ihtiyaç duyduğu dosyaları ve verileri güvenli bir şekilde depolamak, almak ve yönetmek için kullanılır. Depolama servisleri, genellikle dosya sistemine, bulut depolama hizmetlerine veya veritabanlarına dosya ve veri kaydetme işlemlerini içerir.

public class FileStorageService
{
public void SaveFile(string path, byte[] content)
{
System.IO.File.WriteAllBytes(path, content);
}
}

FileStorageService sınıfı, dosya sistemine dosya kaydetme işlevini içerir ve dosya yükleme, rapor saklama gibi işlemler için kullanılır. Depolama servisleri, dosya ve verilerin güvenli, düzenli ve erişilebilir bir şekilde saklanmasına yardımcı olur ve uygulamanın veri yönetimi ihtiyaçlarını karşılar.

Identity (Kimlik Doğrulama Sistemleri)
Kimlik doğrulama sistemleri, uygulamaların güvenliğini sağlamak için kullanıcıların kimliğini doğrulamak amacıyla kullanılan bileşenlerdir. Bu sistemler, kullanıcıların kimlik bilgilerini kontrol ederek yetkisiz erişimi önler ve kullanıcıların sadece izin verilen kaynaklara erişmesini sağlar. Kimlik doğrulama, kullanıcıların giriş yaparken sağladığı bilgilerin doğrulanması sürecidir ve güvenlik açısından kritik bir rol oynar.

public class AuthenticationService
{
public bool ValidateUser(string username, string password)
{
// Kullanıcı kimlik doğrulama mantığı
if (username == "admin" && password == "password123")
{
return true;
}
return false;
}
}

AuthenticationService sınıfı, basit bir kimlik doğrulama işlevi sunar ve kullanıcı adı ve parola bilgilerini kontrol eder. Kimlik doğrulama sistemleri, uygulamanın güvenliğini sağlar, kullanıcı oturumlarını yönetir ve kaynak erişim kontrolü yapar. Bu sistemler, kullanıcıların kimlik bilgilerini güvenli bir şekilde yönetir ve doğrular.

System Clock (Sistem Saati)

Sistem saati, zaman bilgilerini yönetir.SystemClock, geçerli UTC zamanını sağlayan bir sınıftır. Now özelliği, mevcut zamanı döner.

public class SystemClock
{
public DateTime Now => DateTime.UtcNow;
}

Presentation Layer: Kullanıcıların Sisteme Erişimini Sağlayan Katman

Presentation Layer, sistemin dış dünyaya açılan kapısıdır. Kullanıcılar, bu katman aracılığıyla sisteme erişir ve uygulamayı kullanır.Genellikle REST API’ler veya kullanıcı arayüzleri içerir.

API Endpoints (API Uç Noktaları)
API uç noktaları, uygulamanın diğer sistemlerle etkileşimini sağlar.OrdersController, siparişleri işlemek için bir API uç noktası sağlar. PlaceOrder metodu, bir siparişi işlemek için OrderApplicationService'i kullanır. Sipariş geçersizse InvalidOrderException özel durumu fırlatılır ve kullanıcıya hata mesajı döner.

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly OrderApplicationService _orderService;

public OrdersController(OrderApplicationService orderService)
{
_orderService = orderService;
}

[HttpPost("place")]
public IActionResult PlaceOrder(Order order)
{
try
{
_orderService.PlaceOrder(order);
return Ok();
}
catch (InvalidOrderException ex)
{
return BadRequest(ex.Message);
}
}
}

Middleware (Ara Katman Yazılımları)
Middleware, HTTP isteklerini işleyen yazılımlardır.RequestLoggingMiddleware, HTTP isteklerini loglamak içiniçin kullanılan bir ara katman yazılımıdır. InvokeAsync metodu, isteğin ayrıntılarını günlüğe kaydeder ve isteği bir sonraki işleyiciye yönlendirir.

public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;

public RequestLoggingMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task InvokeAsync(HttpContext context)
{
// İstek ayrıntılarını günlüğe kaydetme kodu
await _next(context);
}
}

DI Setup (Bağımlılık Enjeksiyonu Ayarları)
Bağımlılık enjeksiyonu, uygulamanın bağımlılıklarını yönetir.Startup sınıfı, bağımlılıkları yapılandırmak için kullanılır. ConfigureServices metodu, bağımlılıkların çözülmesi için gerekli olan servisleri ekler. Bu, IOrderRepository arayüzünü OrderRepository sınıfıyla ve diğer servislerle ilişkilendirir.

using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// DI ayarları burada yapılır
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<OrderApplicationService>();
builder.Services.AddScoped<OrderService>();

var app = builder.Build();

// Diğer uygulama yapılandırmaları

app.Run();

Bu yazıda, Clean Architecture’ın temel prensiplerini, katmanlarını ve gerçek hayattan örneklerle uygulama adımlarını detaylı bir şekilde ele almaya çalıştık. Bir sonraki yazılarda Domain,Application, Infrastructure ve Presentation katmanlarını birlikte kodlamaya çalışacağız! Ancak bunları kodlarken her ayrıntıya değinip detaylarına inmeye çalışacağız. Çünkü bu mimariyi anlamak için kullanılan yöntemleri detaylı bir şekilde anlamak gerektiğini düşünüyorum. :)

Umuyorum herkes için faydalı bir seri olur!

Bir sonraki yazımda görüşmek üzere!

--

--

Kardel Rüveyda ÇETİN

Expert Software Engineer @DogusTeknoloji | Microsoft MVP | Mathematical Engineer | Speaker | Blogger | Founder&Organizer @thecoderverse