Takip et

Modern Yazılım Projelerinde Karşılaşılan Zorluklar Nelerdir?

Clean Architecture: Sürdürülebilir Yazılım Geliştirmenin Temelleri

Günümüz yazılım dünyasında projeler hızla büyürken, kod tabanının karmaşıklığı da paralel olarak artıyor. Peki, bu karmaşıklıkla nasıl başa çıkabiliriz? Zamanla bakımı zorlaşan, yeni özellik ekledikçe çökme eğilimi gösteren veya test edilmesi neredeyse imkansız hale gelen bir yazılımdan kurtulmak mümkün müdür? İşte bu noktada, Clean Architecture (Temiz Mimari) kavramı, geliştiricilere sürdürülebilir, esnek ve test edilebilir yazılımlar inşa etme konusunda sağlam bir yol haritası sunar. Bu makalede, Clean Architecture'ın temel prensiplerini, sağladığı faydaları ve gerçek dünya projelerinde nasıl uygulanabileceğini derinlemesine inceleyeceğiz.

Yazılım geliştirme süreçleri, özellikle büyük ölçekli ve uzun ömürlü projelerde, kendine özgü bir dizi zorlukla karşılaşır. İlk başta basit görünen bir uygulama, zamanla yeni özellikler eklendikçe ve farklı teknolojiler entegre edildikçe, kontrol edilemez bir karmaşaya dönüşebilir. Bu durum, "spaghetti code" (spagetti kod) olarak adlandırılan, her şeyin birbiriyle sıkıca bağlı olduğu ve bir yerdeki değişikliğin beklenmedik başka yerlerde sorunlara yol açtığı bir yapıya evrilebilir. Böyle bir kod tabanında hata ayıklamak kabusa dönerken, yeni bir geliştiricinin projeye adapte olması günler, hatta haftalar sürebilir. Dahası, kullanıcı talepleri veya iş kurallarındaki değişiklikler karşısında uygulama direnç gösterir; küçük bir iş kuralı değişikliği tüm sistemi baştan yazmayı gerektirebilir. Bu durum, projenin bakım maliyetlerini artırır ve uzun vadede geliştirme hızını ciddi şekilde yavaşlatır.

Ortaya çıkan bir diğer önemli sorun, yazılımın test edilebilirliğinin düşmesidir. Eğer bir modül, veritabanı, kullanıcı arayüzü veya harici bir servis gibi dış bağımlılıklarla sıkı sıkıya bağlıysa, o modülü izole bir şekilde test etmek neredeyse imkansız hale gelir. Bu durum, birim testlerinin etkinliğini azaltır ve projenin güvenilirliğini tehlikeye atar. Aynı zamanda, kullanılan belirli bir veritabanı teknolojisine, bir web framework'üne (örneğin ASP.NET Core veya Spring) veya bir kullanıcı arayüzü kütüphanesine (örneğin React veya Angular) aşırı derecede bağımlı olmak, gelecekte bu teknolojileri değiştirme ihtiyacı doğduğunda büyük bir maliyet ve çaba gerektiren bir sürece dönüşebilir. Bu tür bir "vendor lock-in" (tedarikçi kilitlenmesi), projenin esnekliğini ve uzun vadeli sürdürülebilirliğini olumsuz etkiler. Uygulamanın iş mantığı, veritabanı detayları veya UI elemanlarıyla iç içe geçtiğinde, projenin büyümesiyle birlikte her bir katman kendi sorumluluklarını aşıp diğer katmanların işlerine karışmaya başlar. Bu, geliştirici verimliliğini düşüren, hatalara açık bir ortam yaratır. İşte bu karmaşanın ortasında, Robert C. Martin (Uncle Bob) tarafından popülerleştirilen Clean Architecture, bu tür sorunlara sistematik ve prensipli bir çözüm önerisi sunar.

Clean Architecture Temel Kavramları: Katmanlı Yapı ve Bağımlılık Kuralı

Clean Architecture, aslında hexagonal architecture, onion architecture ve ports and adapters gibi birçok katmanlı mimarinin ortak noktalarını bir araya getirerek genelleştirilmiş bir yapıdır. Temelinde, yazılımı bağımsız katmanlara ayırmak ve bu katmanlar arasında kesin bağımlılık kuralları tanımlamak yatar. Ana hedef, iş kurallarını (business rules) dış dünyadan (veritabanı, UI, framework'ler vb.) izole etmektir. Bu sayede, iş kuralları değiştiğinde dış katmanlardaki değişiklikler minimumda kalır, aynı şekilde dış katmanlar değiştiğinde de iç katmanlardaki iş kuralları etkilenmez. Bu, Mimar Bob Martin'in ünlü "Bağımlılık Kuralı" ile özetlenebilir.

Clean Architecture'ı genellikle ortasında en bağımsız ve dışına doğru gidildikçe daha bağımlı katmanların olduğu bir soğan (onion) veya hedeften (target) oluşan bir diyagramla temsil ederiz. En içteki katman, en kritik ve değişime en dirençli olanıdır.

Bu mimariyi oluşturan dört ana katman ve görevleri şunlardır:

  1. Entities (Varlıklar): En içteki katmandır. Uygulamanın temel iş objelerini ve bu objeleri yöneten en temel, kurumsal iş kurallarını içerir. Bunlar, herhangi bir uygulama, veritabanı veya kullanıcı arayüzü detayıyla ilgili olmayan saf iş objeleridir. Örneğin, bir e-ticaret uygulamasında "Ürün", "Müşteri" veya "Sipariş" gibi varlıklar bu katmanda yer alır. Bu katman, tamamen bağımsızdır ve hiçbir dış katmana bağımlılığı yoktur.
  2. Use Cases (Kullanım Senaryoları / Uygulama İş Kuralları): Entities katmanının hemen dışında yer alır. Bu katman, belirli bir uygulamanın iş kurallarını tanımlar ve organize eder. Yani, bir kullanıcının bir işlemi gerçekleştirmek için yaptığı adımları (örneğin, "sipariş oluşturma", "ürün listeleme", "kullanıcı girişi") ve bu adımların nasıl işleneceğini belirler. Use Case'ler, varlıklarla etkileşime girer ve varlıkların davranışlarını yönlendirir. Bu katman da nispeten bağımsızdır; UI veya veritabanı detaylarına değil, sadece uygulama içindeki iş akışlarına odaklanır. Use Case'ler, genellikle girdi ve çıktı modellerini (DTO'lar) tanımlayan arayüzler aracılığıyla dış katmanlarla iletişim kurar.
  3. Interface Adapters (Arayüz Bağdaştırıcıları): Use Cases katmanının dışında yer alır. Bu katmanın görevi, iç katmanlardaki formatı, dış katmanların anlayabileceği bir formata dönüştürmektir. Yani, veritabanı için SQL sorgularına, web için HTTP yanıtlarına veya UI için uygun veri yapılarına dönüştürme işlemlerini yapar. Bu katmanda genellikle şunlar bulunur:
    • Presenters: Kullanım senaryolarından gelen veriyi, UI'ın gösterebileceği formata dönüştürür.
    • Controllers/Gateways: Dış dünyadan (web istekleri, kullanıcı girişi, harici servisler) gelen veriyi alır, kullanım senaryolarının anlayabileceği formata dönüştürür ve kullanım senaryosunu tetikler. Veritabanı veya harici servislerle iletişimi sağlayan repository arayüzlerinin somut implementasyonları da burada yer alır.

    Bu katman, dış katmanlara (Frameworks & Drivers) bağımlıdır ancak iç katmanlara bağımlılığı yoktur. Bağımlılık Tersine Çevirme Prensibi (Dependency Inversion Principle) burada kilit rol oynar.

  4. Frameworks & Drivers (Çerçeveler ve Sürücüler): En dıştaki katmandır. Bu katman, veritabanı sistemleri (SQL Server, MongoDB), web framework'leri (ASP.NET Core, Spring Boot, Express.js), UI framework'leri (React, Angular, Vue.js), harici API'ler ve diğer üçüncü taraf araçlar gibi somut implementasyonları ve detayları içerir. Bu katman, iç katmanlardan tamamen bağımsızdır ve sadece iç katmanların tanımladığı arayüzleri uygular. Buradaki teknolojiler kolayca değiştirilebilir; örneğin, bir veritabanını PostgreSQL'den MongoDB'ye geçirmek, sadece bu katmanda değişiklik gerektirir ve iç katmanları etkilemez.

Bağımlılık Kuralı: Neden Tersine Çevirme Bu Kadar Önemli?

Clean Architecture'ın en temel ve kritik prensibi, Bağımlılık Kuralı'dır. Bu kural, mimarinin soğan diyagramını takip eder: Dış katmanlar her zaman iç katmanlara bağımlı olabilir, ancak iç katmanlar asla dış katmanlara bağımlı olamaz. Oklar daima içeri doğru işaret etmelidir. Bu, demektir ki, iç katmanlar (Entities, Use Cases) dış katmanlardaki herhangi bir şey hakkında hiçbir bilgiye sahip olmamalıdır. Örneğin, bir Use Case, bir veritabanının veya bir web framework'ünün varlığından haberdar olmamalıdır.

Peki bu nasıl sağlanır? İşte burada Bağımlılık Tersine Çevirme Prensibi (Dependency Inversion Principle - DIP) devreye girer. DIP, SOLID prensiplerinden biridir ve "üst seviye modüllerin alt seviye modüllere doğrudan bağımlı olmaması, ikisinin de soyutlamalara bağımlı olması gerektiğini" söyler. Ayrıca, "soyutlamaların detaylara değil, detayların soyutlamalara bağımlı olması gerektiğini" belirtir.

Pratikte bu şu anlama gelir: İç katmanlar (örneğin, bir Use Case) ihtiyaç duyduğu dış servisler (örneğin, veritabanı erişimi veya dış bir API çağrısı) için arayüzler (interface'ler) tanımlar. Bu arayüzler, iç katmanlarda (örneğin, Use Case katmanında) yer alır. Dış katmanlar (örneğin, Interface Adapters veya Frameworks & Drivers) ise bu arayüzleri uygular (implement eder). Böylece, Use Case, somut bir veritabanı implementasyonuna değil, kendi tanımladığı bir IRepository arayüzüne bağımlı olur. Veritabanı implementasyonu ise bu IRepository arayüzüne bağımlı olarak onu gerçekleştirir. Bu sayede, bağımlılık yönü tersine çevrilir: somut detaylar (veritabanı sürücüsü) soyutlamalara (arayüz) bağımlı hale gelirken, iş kuralları (Use Case) soyutlamalar üzerinden çalışmaya devam eder. Bu mekanizma, mimarinin esnekliğini, test edilebilirliğini ve değişimlere karşı direncini artırır.


// Entities Katmanı (veya Use Cases Katmanı) - En içteki katman
namespace MyApp.Application.Interfaces
{
    public interface IProductRepository
    {
        Task GetByIdAsync(Guid id);
        Task AddAsync(Product product);
        // ... diğer CRUD operasyonları
    }

    public class Product
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        // İş kuralları bu varlık içinde olabilir
        public void UpdatePrice(decimal newPrice)
        {
            if (newPrice < 0) throw new ArgumentException("Fiyat sıfırdan küçük olamaz.");
            Price = newPrice;
        }
    }
}

// Interface Adapters Katmanı (veya Frameworks & Drivers Katmanı) - Dış katman
namespace MyApp.Infrastructure.Data
{
    using MyApp.Application.Interfaces;
    using MyApp.Application.Entities; // Entities katmanına bağımlı
    using Microsoft.EntityFrameworkCore; // Dış teknolojiye bağımlı

    public class ProductRepository : IProductRepository
    {
        private readonly ApplicationDbContext _context;

        public ProductRepository(ApplicationDbContext context)
        {
            _context = context;
        }

        public async Task GetByIdAsync(Guid id)
        {
            return await _context.Products.FindAsync(id);
        }

        public async Task AddAsync(Product product)
        {
            await _context.Products.AddAsync(product);
            await _context.SaveChangesAsync();
        }
        // ... IProductRepository arayüzünün diğer implementasyonları
    }

    // Uygulamanın veritabanı bağlamı
    public class ApplicationDbContext : DbContext
    {
        public DbSet Products { get; set; }
        public ApplicationDbContext(DbContextOptions options) : base(options) { }
    }
}

Yukarıdaki C# örneğinde, IProductRepository arayüzü MyApp.Application.Interfaces namespace'inde (bir iç katmanda) tanımlanmıştır. ProductRepository sınıfı ise MyApp.Infrastructure.Data namespace'inde (bir dış katmanda) bu arayüzü implemente eder. ProductRepository, ApplicationDbContext (yani Entity Framework Core) gibi bir dış teknolojiye bağımlı olmasına rağmen, IProductRepository arayüzü bu dış teknolojiden haberdar değildir. Böylece bağımlılık kuralı korunur.

Neden Clean Architecture Tercih Edilmelidir? Sağladığı Avantajlar Nelerdir?

Clean Architecture'ı benimsemek, bir yazılım projesine sadece estetik bir düzen getirmekle kalmaz, aynı zamanda uzun vadeli başarı için hayati önem taşıyan bir dizi somut avantaj sunar. Bu avantajlar, projenin tüm yaşam döngüsü boyunca hissedilir ve geliştirme ekibinin verimliliğini, yazılımın kalitesini ve iş hedeflerine ulaşma yeteneğini doğrudan etkiler.

İlk olarak, ve belki de en önemlisi, Test Edilebilirlik (Testability) konusundaki iyileşmedir. Clean Architecture'ın katmanlı yapısı ve bağımlılık kuralı sayesinde, uygulamanın çekirdek iş mantığı (Entities ve Use Cases) dış bağımlılıklardan (veritabanı, UI, ağ servisleri vb.) tamamen izole edilir. Bu izolasyon, iş kurallarını içeren Use Case'lerin ve Entities'in, mocked (sahte) veya stubbed (taslak) bağımlılıklarla çok hızlı ve güvenilir bir şekilde birim testlerinden geçirilmesini sağlar. Geliştiriciler, bir veritabanı bağlantısına veya bir web sunucusuna ihtiyaç duymadan, saf C# veya Java kodunu test edebilirler. Bu durum, testlerin yazımını kolaylaştırır, test sürelerini kısaltır ve hata ayıklama maliyetlerini önemli ölçüde düşürür. Yüksek test kapsamı, yazılımın kalitesini artırır ve yeni özellikler eklerken veya mevcut kodu refactor ederken güven verir.

İkinci olarak, Bağımsızlık (Independence), Clean Architecture'ın temel direklerinden biridir. Bu mimari, yazılımın dört temel alanda bağımsız olmasını sağlar:

  1. Framework Bağımsızlığı: Uygulamanın iş kuralları, belirli bir framework'e (örneğin Spring, ASP.NET Core) bağlı değildir. Bu, framework'ü değiştirmenin nispeten kolay olduğu anlamına gelir.
  2. Veritabanı Bağımsızlığı: İş kuralları bir veritabanına bağlı değildir. Veritabanını değiştirmek (örneğin SQL'den NoSQL'e geçmek), sadece Interface Adapters ve Frameworks & Drivers katmanlarında minimal değişiklikler gerektirir.
  3. UI Bağımsızlığı: Uygulamanın iş mantığı, kullanıcı arayüzünden tamamen bağımsızdır. Bu, UI'ı (örneğin web, mobil, masaüstü) değiştirmeyi veya birden fazla UI ile çalışmayı kolaylaştırır.
  4. Harici Servis Bağımsızlığı: Harici servis sağlayıcıları veya API'ler değiştiğinde, iç katmanlar etkilenmez, sadece ilgili adaptörler güncellenir.

Bu bağımsızlık, projenin gelecekteki değişikliklere karşı son derece esnek olmasını sağlar ve teknolojik evrimin getirdiği zorluklara karşı dirençli kılar. Uzun vadede maliyetleri ve teknik borcu azaltır.

Üçüncü avantaj, Bakım Kolaylığı (Maintainability) ve Anlaşılırlık (Understandability)'tır. Katmanlı yapı ve net sorumluluk ayrımı sayesinde, bir geliştirici uygulamanın belirli bir bölümünü değiştirmesi gerektiğinde, değişikliğin etkileyeceği alanı kolayca belirleyebilir. Örneğin, bir iş kuralı değiştiğinde sadece ilgili Use Case katmanında değişiklik yapılır. Veritabanı şeması değiştiğinde ise sadece Interface Adapters'daki repository implementasyonları güncellenir. Bu, kodun nerede ve neden değiştiğini anlamayı kolaylaştırır. Yeni bir ekip üyesinin projeye adapte olması da daha hızlı olur çünkü mimari belirli bir düzeni ve beklentileri dayatır. Kodun okunabilirliği ve düzeni artar, bu da hataların daha kolay tespit edilmesini ve çözülmesini sağlar.

Son olarak, Ölçeklenebilirlik (Scalability) ve Geliştirici Verimliliği (Developer Productivity) de Clean Architecture'ın sunduğu önemli faydalardır. Katmanlar birbirinden bağımsız olduğu için, farklı ekipler farklı katmanlar üzerinde paralel olarak çalışabilir. Örneğin, bir ekip UI üzerinde çalışırken, başka bir ekip iş kurallarını geliştirebilir veya veritabanı entegrasyonlarını yapabilir. Bu, büyük ekiplerde geliştirme sürecini hızlandırır. Ayrıca, temiz ve modüler bir yapı, performans iyileştirmelerinin veya yeni özelliklerin daha güvenli ve hızlı bir şekilde eklenmesine olanak tanır. Uygulama büyüdükçe, bu yapı karmaşıklığı yönetilebilir tutar ve hızlı değişikliklere adapte olmayı kolaylaştırır.

Avantaj Açıklama Önem Derecesi
Yüksek Test Edilebilirlik İş mantığı dış bağımlılıklardan izole olduğu için birim testleri kolaylaşır. Çok Yüksek
Framework Bağımsızlığı Belirli bir teknolojiye bağlı kalmadan framework değiştirme esnekliği sunar. Yüksek
Bakım ve Geliştirme Kolaylığı Kodun anlaşılabilirliği ve değişikliklerin lokalize olması bakım maliyetlerini düşürür. Yüksek
Esneklik ve Uyarlanabilirlik İş kuralları, UI veya veritabanı değişikliklerine karşı dirençlidir. Yüksek
Geliştirici Verimliliği Ekip üyelerinin paralel çalışabilmesini ve hızlı geliştirme yapmasını sağlar. Orta

Clean Architecture Uygulamalı Adımlar: Katmanları Nasıl Bir Araya Getiririz?

Clean Architecture'ı bir projede uygulamak, özellikle ilk başta, biraz soyut gelebilir. Ancak adım adım yaklaşıldığında, bu prensipleri somut bir yapıya dönüştürmek mümkündür. Genellikle bir projeyi Clean Architecture ile kurgularken, projeyi fiziksel olarak farklı katmanlara karşılık gelen modüllere veya projelere ayırırız. Örneğin, .NET'te farklı class library projeleri, Java'da farklı modüller veya paketler bu ayrımı sağlar. İşte adım adım Clean Architecture uygulaması:

1. Adım: Entities (Varlıklar) Katmanını Tanımlayın

Her şeyden önce, uygulamanızın temel iş objelerini ve temel iş kurallarını içeren en iç katmanı oluşturun. Bu katman, hiçbir dış bağımlılığı olmayan saf POJO (Plain Old Java Object) veya POCO (Plain Old CLR Object) sınıflarını içermelidir. Bu sınıflar sadece veri ve bu veriyi manipüle eden temel davranışları barındırır.


// MyApp.Domain/Entities/Product.cs
namespace MyApp.Domain.Entities
{
    public class Product
    {
        public Guid Id { get; private set; }
        public string Name { get; private set; }
        public decimal Price { get; private set; }
        public int StockQuantity { get; private set; }

        public Product(string name, decimal price, int stockQuantity)
        {
            Id = Guid.NewGuid();
            SetName(name);
            SetPrice(price);
            SetStockQuantity(stockQuantity);
        }

        // İş kuralları varlık içinde
        private void SetName(string name)
        {
            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentException("Ürün adı boş olamaz.");
            Name = name;
        }

        private void SetPrice(decimal price)
        {
            if (price <= 0)
                throw new ArgumentException("Ürün fiyatı sıfırdan büyük olmalıdır.");
            Price = price;
        }

        private void SetStockQuantity(int quantity)
        {
            if (quantity < 0)
                throw new ArgumentException("Stok miktarı negatif olamaz.");
            StockQuantity = quantity;
        }

        public void AdjustStock(int amount) // Stok güncelleme iş kuralı
        {
            if (StockQuantity + amount < 0)
                throw new InvalidOperationException("Stok miktarı eksiye düşürülemez.");
            StockQuantity += amount;
        }
    }
}

2. Adım: Use Cases (Kullanım Senaryoları) Katmanını Oluşturun

Bu katman, uygulamanın özel iş kurallarını ve akışlarını tanımlar. Genellikle, her önemli kullanıcı eylemi veya sistem işlemi için bir kullanım senaryosu (Use Case) sınıfı oluşturulur. Bu sınıflar, Entities katmanındaki varlıklarla etkileşime girer ve dış katmanlarla iletişim kurmak için arayüzler tanımlar (örneğin, veri depolama için Repository arayüzleri). Bu katman, Use Case'lerin alacağı girdi (Request) ve döneceği çıktı (Response) modellerini de tanımlar. Bu katman, MyApp.Application gibi bir isimle adlandırılabilir.


// MyApp.Application/UseCases/CreateProduct.cs
namespace MyApp.Application.UseCases
{
    using MyApp.Domain.Entities;
    using MyApp.Application.Interfaces; // IProductRepository arayüzüne bağımlılık

    public class CreateProductRequest
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
        public int StockQuantity { get; set; }
    }

    public class CreateProductResponse
    {
        public Guid ProductId { get; set; }
        public string Message { get; set; }
    }

    public class CreateProductUseCase : ICreateProductUseCase // Arayüz de olabilir
    {
        private readonly IProductRepository _productRepository;

        public CreateProductUseCase(IProductRepository productRepository)
        {
            _productRepository = productRepository;
        }

        public async Task Execute(CreateProductRequest request)
        {
            // İş kuralı: Ürün adı ve fiyatı geçerli mi? (Varlık içinde de kontrol edilebilir)
            if (string.IsNullOrWhiteSpace(request.Name))
                return new CreateProductResponse { Message = "Ürün adı boş olamaz." };
            if (request.Price <= 0)
                return new CreateProductResponse { Message = "Fiyat sıfırdan büyük olmalıdır." };

            var product = new Product(request.Name, request.Price, request.StockQuantity);
            await _productRepository.AddAsync(product);

            return new CreateProductResponse
            {
                ProductId = product.Id,
                Message = "Ürün başarıyla oluşturuldu."
            };
        }
    }
}

// MyApp.Application/Interfaces/IProductRepository.cs
namespace MyApp.Application.Interfaces
{
    using MyApp.Domain.Entities; // Entities katmanına bağımlılık

    public interface IProductRepository
    {
        Task GetByIdAsync(Guid id);
        Task AddAsync(Product product);
        // ... diğer repository metotları
    }
    // Diğer kullanım senaryosu arayüzleri de burada tanımlanabilir
    public interface ICreateProductUseCase { /* ... */ }
}

3. Adım: Interface Adapters (Arayüz Bağdaştırıcıları) Katmanını Geliştirin

Bu katman, Use Cases katmanının tanımladığı arayüzleri dış katmanlarla (Frameworks & Drivers) köprüler. Burada API denetleyicileri (Controllers), veritabanı depolarının somut implementasyonları (Repository Implementations) ve verileri UI'a uygun hale getiren sunucular (Presenters) yer alır. Bu katman, Use Case'lerden gelen çıktıları dış dünyaya uygun hale getirir ve dış dünyadan gelen girdileri Use Case'lerin anlayacağı formata dönüştürür. Bu katman, MyApp.Infrastructure (veritabanı, harici servisler için) ve MyApp.Web (UI, API için) gibi projelere ayrılabilir.


// MyApp.Infrastructure/Data/ProductRepository.cs (Interface Adapters'ın bir parçası)
namespace MyApp.Infrastructure.Data
{
    using MyApp.Application.Interfaces; // Application katmanına bağımlılık
    using MyApp.Domain.Entities;       // Domain katmanına bağımlılık
    using Microsoft.EntityFrameworkCore; // Dış teknolojiye bağımlılık

    public class ProductRepository : IProductRepository
    {
        private readonly ApplicationDbContext _context; // Frameworks & Drivers'dan

        public ProductRepository(ApplicationDbContext context)
        {
            _context = context;
        }

        public async Task GetByIdAsync(Guid id)
        {
            return await _context.Products.FindAsync(id);
        }

        public async Task AddAsync(Product product)
        {
            await _context.Products.AddAsync(product);
            await _context.SaveChangesAsync();
        }
    }
}

// MyApp.Web/Controllers/ProductsController.cs (Interface Adapters'ın diğer parçası)
namespace MyApp.Web.Controllers
{
    using Microsoft.AspNetCore.Mvc; // Dış teknolojiye bağımlılık
    using MyApp.Application.UseCases; // Application katmanına bağımlılık
    using MyApp.Application.Interfaces; // Application katmanına bağımlılık

    [ApiController]
    [Route("[controller]")]
    public class ProductsController : ControllerBase
    {
        private readonly CreateProductUseCase _createProductUseCase; // Use Case'e bağımlılık

        public ProductsController(CreateProductUseCase createProductUseCase)
        {
            _createProductUseCase = createProductUseCase;
        }

        [HttpPost]
        public async Task CreateProduct([FromBody] CreateProductRequest request)
        {
            var response = await _createProductUseCase.Execute(request);
            if (response.ProductId != Guid.Empty)
            {
                return Ok(response);
            }
            return BadRequest(response.Message);
        }
    }
}

4. Adım: Frameworks & Drivers (Çerçeveler ve Sürücüler) Katmanını Konfigure Edin

Bu en dış katman, kullandığınız veritabanı, web framework'ü, UI framework'ü gibi somut teknolojileri içerir. Önceki adımda tanımlanan arayüz bağdaştırıcıları burada bu teknolojilerle bir araya getirilir. Bu katman genellikle uygulamanın başlangıç (startup) dosyasını ve bağımlılık enjeksiyonu (dependency injection) konfigürasyonlarını içerir.


// MyApp.Web/Startup.cs veya Program.cs (Asp.Net Core 6+)
namespace MyApp.Web
{
    using MyApp.Application.Interfaces;
    using MyApp.Application.UseCases;
    using MyApp.Infrastructure.Data;
    using Microsoft.EntityFrameworkCore;

    public class Startup
    {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            // Veritabanı yapılandırması - Dış teknolojiye bağımlılık
            services.AddDbContext(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            // Bağımlılık Enjeksiyonu ile Interface'leri somut implementasyonlarla eşleştirme
            // Application katmanındaki arayüzü, Infrastructure katmanındaki implementasyonla bağla
            services.AddScoped();
            services.AddScoped(); // Use Case'i doğrudan ekle
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // ... HTTP request pipeline yapılandırması
        }
    }
}

Bu adımları takip ederek, uygulamanızın çekirdek iş mantığını dış detaylardan soyutlamış olursunuz. Her katmanın belirli sorumlulukları vardır ve bağımlılık yönü her zaman iç katmanlara doğrudur. Bu yapı, değişime dirençli, test edilebilir ve sürdürülebilir bir yazılım mimarisi oluşturmanızı sağlar.

Vaka Analizi: E-ticaret Uygulamasında Clean Architecture

Bir e-ticaret uygulamasını ele alalım. Kullanıcıların ürünleri gezdiği, sepetine eklediği, sipariş verdiği ve ödeme yaptığı bir senaryo. Clean Architecture bu karmaşık süreçleri nasıl organize eder?

  1. Entities (Domain Katmanı):
    • Product: Ürün adı, fiyatı, stoğu. Stok güncelleme, fiyat değiştirme gibi temel iş kuralları burada.
    • Customer: Müşteri bilgileri, adresi.
    • Order: Siparişin durumu (beklemede, onaylandı, kargolandı), sipariş edilen ürünler, toplam fiyat. Siparişin durumunu değiştirme, toplam fiyatı hesaplama gibi iş kuralları.
    • OrderItem: Sipariş içindeki her bir ürün ve miktarı.

    Bu varlıklar tamamen saf sınıflardır, veritabanı ORM (Object-Relational Mapping) kütüphanesi veya UI framework'leri hakkında hiçbir bilgileri yoktur.

  2. Use Cases (Application Katmanı):
    • CreateOrderUseCase: Bir müşterinin sepetindeki ürünleri kullanarak nasıl sipariş oluşturduğunu yönetir.
      • Girdi: CreateOrderRequest (Müşteri ID'si, Ürün ID'leri ve miktarları).
      • İşlem: IProductRepository'den ürünleri çeker, stok kontrolü yapar, Order varlığını oluşturur, IOrderRepository aracılığıyla siparişi kaydeder.
      • Çıktı: CreateOrderResponse (Sipariş ID'si, durum mesajı).
    • ProcessPaymentUseCase: Oluşturulan bir siparişin ödemesinin nasıl işlendiğini yönetir.
      • Girdi: ProcessPaymentRequest (Sipariş ID'si, Ödeme bilgileri).
      • İşlem: IOrderRepository'den siparişi alır, IPaymentGateway aracılığıyla ödeme işlemini başlatır, siparişin durumunu günceller.
      • Çıktı: ProcessPaymentResponse (Başarı durumu, işlem ID'si).
    • GetProductListUseCase, AddProductToCartUseCase gibi diğer senaryolar da bu katmanda yer alır.

    Bu katman, IProductRepository, IOrderRepository, IPaymentGateway gibi arayüzleri tanımlar.

  3. Interface Adapters (Infrastructure/Web Katmanı):
    • Controllers:
      • OrderController: Web isteğini (HTTP POST /orders) alır. CreateOrderRequest'i oluşturur ve CreateOrderUseCase'i çağırır. Dönüş değerini HTTP yanıtına (200 OK veya 400 Bad Request) çevirir.
    • Repository Implementations:
      • EFCoreProductRepository: IProductRepository arayüzünü Entity Framework Core kullanarak implemente eder. Ürünleri veritabanından çeker veya kaydeder.
      • EFCoreOrderRepository: IOrderRepository arayüzünü implemente eder.
    • Gateway Implementations:
      • StripePaymentGateway: IPaymentGateway arayüzünü Stripe API'sini kullanarak implemente eder.
    • Presenters: Eğer bir MVC yapısı kullanılıyorsa, Use Case'lerden gelen veriyi View'e uygun hale getiren sınıflar.

    Bu katman, dış dünyadan gelen (HTTP isteği) veya dış dünyaya giden (veritabanı sorgusu, ödeme servisi çağrısı) verileri iç katmanların anlayacağı formata çevirir.

  4. Frameworks & Drivers (Başlangıç/Konfigurasyon Katmanı):
    • ASP.NET Core projesinin Startup.cs veya Program.cs dosyası.
    • Bağımlılık Enjeksiyonu (DI) konteyneri burada yapılandırılır. IProductRepository'nin EFCoreProductRepository ile, IPaymentGateway'in StripePaymentGateway ile eşleştirilmesi gibi işlemler yapılır.
    • Veritabanı bağlantı dizeleri, API anahtarları gibi dış konfigurasyonlar burada yönetilir.

Bu yapı sayesinde, örneğin ödeme sağlayıcınızı Stripe'tan PayPal'a değiştirmek istediğinizde, sadece StripePaymentGateway implementasyonunu ve DI yapılandırmasını değiştirirsiniz. ProcessPaymentUseCase ve Order varlığı bu değişiklikten etkilenmez. Aynı şekilde, web arayüzünden mobil arayüze geçiş yapmak veya konsol uygulaması eklemek, sadece Controller katmanında veya yeni bir UI katmanında değişiklik gerektirir, çekirdek iş mantığına dokunmazsınız.

İleri Seviye Konular ve İpuçları: Mimarinin Ötesine Geçmek

Clean Architecture'ın temelini anladıktan sonra, daha karmaşık veya büyük ölçekli projelerde mimarinizi daha da güçlendirecek ileri seviye konulara odaklanabiliriz. Bu bölümde, deneyimli geliştiricilerin projelerini daha da optimize etmek için kullanabileceği bazı ipuçlarına ve entegrasyon yöntemlerine değineceğiz.

1. SOLID Prensiplerinin Rolü: Clean Architecture, temelde SOLID prensiplerinin doğal bir sonucudur ve bu prensipleri uygulamayı teşvik eder. Özellikle Bağımlılık Tersine Çevirme Prensibi (DIP), katmanlar arası bağımsızlığı sağlayan kilit prensiptir. Ancak diğer SOLID prensipleri de mimariyi destekler:

  • Single Responsibility Principle (SRP): Her sınıfın veya modülün tek bir sorumluluğu olmasını sağlar. Bu, Clean Architecture'ın her katmanının ve içindeki her bileşenin belirli bir işlevi yerine getirmesiyle uyumludur. Örneğin, bir Use Case sadece bir iş akışını yönetirken, bir Entity sadece iş verilerini ve kurallarını içerir.
  • Open/Closed Principle (OCP): Yazılım varlıklarının (sınıflar, modüller vb.) geliştirmeye açık, ancak değişikliğe kapalı olması gerektiğini belirtir. Clean Architecture'da, yeni bir iş kuralı eklemek genellikle yeni bir Use Case oluşturarak veya mevcut birini genişleterek yapılır, var olan kodun içini değiştirmek zorunda kalmazsınız.
  • Liskov Substitution Principle (LSP): Temel sınıfların, türetilmiş sınıflarla değiştirilebilir olmasını sağlar. Bu, özellikle repository veya gateway arayüzleri gibi polimorfik davranışlar sergileyen durumlarda önemlidir.
  • Interface Segregation Principle (ISP): Bir sınıfın, kullanmadığı arayüzlere bağımlı olmamasını sağlar. Geniş, çok amaçlı arayüzler yerine, küçük ve spesifik arayüzler tanımlamak daha iyidir. Bu, Use Case'lerin sadece ihtiyaç duydukları servislerin arayüzlerine bağımlı olmasını sağlar.

SOLID prensiplerine uymak, Clean Architecture'ın sunduğu faydaları maksimize eder ve daha esnek, bakımı kolay bir kod tabanı oluşturur.

2. Domain-Driven Design (DDD) ile Entegrasyon: Clean Architecture ve Domain-Driven Design (DDD) birbirini mükemmel şekilde tamamlayan iki güçlü konsepttir. DDD, yazılımı iş alanının karmaşıklığına odaklanarak tasarlama yaklaşımıdır. Clean Architecture'ın Entities katmanı, DDD'deki Domain Model'e (Varlıklar, Değer Nesneleri, Aggregate'ler, Domain Servisleri) doğrudan karşılık gelir. DDD'nin Ubiquitous Language (Ortak Dil) kavramı, iş ve yazılım arasında bir köprü kurarken, Clean Architecture bu dilin yazılım içindeki yapısını ve etkileşimini düzenler. DDD'nin aggregate'leri, bir transaction (işlem) sınırı içinde tutulması gereken varlık gruplarını tanımlayarak, Use Case'lerin hangi varlıklar üzerinde işlem yapacağını daha net belirlemesine yardımcı olur. Clean Architecture, bu domain modelini dış detaylardan (Persistence, UI vb.) koruyarak, saf iş mantığının kalbini oluşturur.

3. Cross-Cutting Concerns (Çapraz Kesim Kaygıları): Logging (günlükleme), Authentication (kimlik doğrulama), Authorization (yetkilendirme), Caching (önbellekleme) ve Validation (doğrulama) gibi konular, genellikle uygulamanın birden fazla katmanını etkiler. Clean Architecture'da bu tür kaygıları genellikle Interface Adapters katmanında veya Frameworks & Drivers katmanında, Aspect-Oriented Programming (AOP) yaklaşımları veya middleware/filter'lar aracılığıyla ele almak en uygunudur. Örneğin:

  • Validation: Basit veri doğrulamaları (girdi boş olamaz gibi) Use Case Request modellerinde veya Use Case içinde yapılabilir. Daha karmaşık iş odaklı doğrulamalar Use Case'ler içinde veya Domain Entities içinde yer alabilir.
  • Logging: Use Case'ler loglama için bir ILogger arayüzü tanımlayabilirken, bunun somut implementasyonu (örneğin Serilog veya NLog) Infrastructure katmanında yer alır.
  • Authentication/Authorization: Genellikle API gateway'lerinde veya Controller katmanında (örneğin ASP.NET Core'daki authorize attribute'ları ile) ele alınır. İş kurallarının kendisi yetkilendirme kararları alabilir, ancak kullanıcının kimliğini veya rollerini kontrol etmek dış katmanların sorumluluğundadır.

4. Ne Zaman Clean Architecture Kullanmamalıyız? Her mimari desen gibi, Clean Architecture da her proje için en iyi çözüm değildir. Küçük, basit ve kısa ömürlü bir CRUD (Create, Read, Update, Delete) uygulaması için Clean Architecture'ın getirdiği soyutlama ve katmanlama karmaşıklığı, projenin büyüklüğüne göre aşırı gelebilir. "Over-engineering" (aşırı mühendislik) tuzağına düşmemek önemlidir. Eğer projenin gelecekteki değişiklikleri veya ölçeklenme potansiyeli düşükse, daha basit bir katmanlı mimari (örneğin geleneksel 3 katmanlı mimari) yeterli olabilir. Clean Architecture, özellikle projenin iş mantığının karmaşık olduğu, sık değişime uğrayan, uzun ömürlü olması beklenen ve yüksek test kapsamına ihtiyaç duyan projeler için en uygunudur.

5. Mobil Uyumlu HTML Örneği (Frameworks & Drivers Katmanında UI ile İlişkili):

Clean Architecture genellikle backend odaklı olsa da, Frameworks & Drivers katmanında bir UI veya Frontend Framework'ü (React, Angular vb.) bulunabilir. Bu noktada mobil uyumluluk, kullanıcı deneyimi açısından önemlidir. Aşağıda, Frameworks & Drivers katmanında, bir web arayüzünün nasıl mobil uyumlu hale getirilebileceğine dair küçük bir HTML ve CSS örneği sunulmuştur:





    
    
    Clean Architecture Örneği - Ürün Listesi
    


    

Ürün Listesi

Laptop Bilgisayar

Güçlü işlemci, 16GB RAM, 512GB SSD.

Fiyat: 25.000 TL

Akıllı Telefon

Yüksek çözünürlüklü ekran, gelişmiş kamera özellikleri.

Fiyat: 12.000 TL

Yukarıdaki örnekte, @media (max-width: 600px) kuralı, ekran genişliği 600 piksel veya daha az olduğunda farklı stiller uygulayarak sayfanın mobil cihazlarda daha iyi görünmesini sağlar. Bu, Clean Architecture'ın dış katmanlarında (Frameworks & Drivers) UI tarafında nasıl bir esneklik sunabileceğine dair bir ipucudur. İş mantığı (ürün fiyatını hesaplama gibi) iç katmanlarda kalırken, sunum detayları dış katmanda yönetilir.

Clean Architecture Hakkında Sıkça Sorulan Sorular

Clean Architecture'ı yeni öğrenen veya uygulamaya çalışan geliştiricilerin aklına takılan bazı yaygın sorular vardır. İşte bunlardan birkaçı ve cevapları:

1. Clean Architecture, "over-engineering" (aşırı mühendislik) midir?

Cevap: Clean Architecture, her proje için uygun olmayabilir. Küçük, basit ve kısa ömürlü bir CRUD uygulaması için getirdiği soyutlama ve katmanlama, projenin ihtiyacından fazla olabilir ve geliştirme sürecini gereksiz yere uzatabilir. Ancak, karmaşık iş mantığına sahip, sık değişime uğrayan, uzun ömürlü olması beklenen ve farklı arayüzler (web, mobil, masaüstü) tarafından kullanılması muhtemel büyük ve orta ölçekli projeler için kesinlikle "over-engineering" değildir. Bu tür projelerde, Clean Architecture'ın sağladığı esneklik, test edilebilirlik ve sürdürülebilirlik uzun vadede geliştirme maliyetlerini düşürür ve projenin başarılı olmasına katkıda bulunur. Kısacası, projenizin ihtiyaçlarına göre değerlendirilmelidir.

2. Her proje için ayrı bir katman (proje/modül) oluşturmak zorunda mıyız?

Cevap: Fiziksel olarak ayrı projeler (örneğin .NET'te Class Library'ler) oluşturmak, bağımlılıkları derleme zamanında kontrol etmenizi sağlar ve mimariyi daha net görselleştirmenize yardımcı olur. Ancak, bu zorunlu değildir. Daha küçük projelerde veya deneyimli ekiplerle, mantıksal katmanları aynı proje içinde farklı namespace'ler (paketler) veya klasörler aracılığıyla da ayırabilirsiniz. Önemli olan, bağımlılık kuralını (iç katmanlar asla dış katmanlara bağımlı olamaz) korumaktır. Fiziksel ayrım, özellikle büyük ve çok disiplinli ekiplerin çalıştığı projelerde, katmanlar arası sınırları daha net çizmek için tercih edilir.

3. DTO'lar (Data Transfer Objects) nerede yer almalı?

Cevap: DTO'lar, Clean Architecture'da genellikle Interface Adapters katmanında (örneğin web API katmanı veya sunum katmanı) veya Use Cases katmanında (request/response modelleri olarak) tanımlanır. Domain katmanındaki Entities, saf iş objeleridir ve genellikle DTO olarak kullanılmazlar. Interface Adapters katmanı, dış dünyadan gelen veriyi (Controller'a gelen JSON gibi) Use Case'lerin anlayacağı Request DTO'larına çevirir ve Use Case'lerden gelen Response DTO'larını dış dünyaya uygun bir formata (JSON yanıtı gibi) çevirir. Use Case katmanındaki Request ve Response DTO'ları ise, belirli bir kullanım senaryosunun beklediği girdi ve çıktıyı soyutlar.

4. Clean Architecture, ORM (Object-Relational Mapper) kütüphaneleriyle (Entity Framework Core, Hibernate vb.) nasıl çalışır?

Cevap: ORM kütüphaneleri, Frameworks & Drivers katmanında yer alan bir teknoloji detayıdır. Clean Architecture'da, Domain katmanındaki Entities, ORM'den bağımsız saf POCO/POJO sınıflarıdır. Interface Adapters katmanında, IRepository arayüzleri tanımlanır (Use Case katmanı tarafından). Bu arayüzlerin somut implementasyonları (örneğin EFCoreProductRepository), Infrastructure katmanında (Interface Adapters'ın bir parçası) ORM kütüphanelerini kullanarak veritabanı işlemlerini gerçekleştirir. Bu sayede, iş mantığı (Entities ve Use Cases), belirli bir ORM teknolojisinin varlığından haberdar olmaz. ORM'yi değiştirme ihtiyacı doğduğunda, sadece ilgili Repository implementasyonları güncellenir, iç katmanlara dokunulmaz.

5. Frontend ve mobil uygulamalar Clean Architecture'a nasıl entegre olur?

Cevap: Clean Architecture genellikle backend odaklıdır. Frontend ve mobil uygulamalar, Clean Architecture'ın Frameworks & Drivers katmanının bir parçası olarak kabul edilebilir. Bu uygulamalar, Interface Adapters katmanındaki REST API'leri veya GraphQL uç noktaları aracılığıyla Use Case'lerle iletişim kurar. Frontend tarafında da benzer mimari prensipler (örneğin, MVVM veya MVI gibi katmanlı yapılar) uygulanabilir, ancak bunlar genellikle backend'deki Clean Architecture'ın detaylarına doğrudan bağlı değildir. Önemli olan, frontend'in iş kurallarını değil, sadece veriyi sunma ve kullanıcıdan girdi alma sorumluluğunu taşımasıdır. İş mantığı, backend'deki Clean Architecture katmanlarında merkezi olarak yönetilir.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Mobil sürümden çık