Mert Özen Her Satırda Daha İleri
Yazılım Mimarisi Clean, Onion ve Hexagonal Architecture — #09

Clean Architecture Ne Zaman Fazla Gelir? Template Kültü, Interface Patlaması ve Pragmatik Yol

Mert Özen 7 Kas 2026 13 dk 5 görüntülenme
Clean Architecture Ne Zaman Fazla Gelir? Template Kültü, Interface Patlaması ve Pragmatik Yol

Clean Architecture, korunacak iş kuralı olmayan CRUD ağırlıklı, kısa ömürlü ya da tek adapter'lı küçük servislerde fazla gelir; uzun ömürlü, kurallı ve çok adapter'lı bölgelerde ise karşılığını verir. Template kültü ve interface patlamasının belirtileri, aynı Dependency Rule'u tek project ve ArchUnitNET test'leriyle korumak, ve layered bir projeden FreezingArchRule ile kademeli geçiş.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore'un yönetim paneline "kitabın fiyatını güncelle" özelliği eklenecek. Ekip, Ordering için kurduğumuz yapıyı örnek alıyor ve aynı dört project'li düzeni Catalog'a da uyguluyor. Application'da bir service ve interface'i:

using Bookstore.Domain.Catalog;

namespace Bookstore.Application.Catalog;

public interface IBookService
{
    Task<BookDto?> UpdatePriceAsync(UpdateBookPriceCommand command, CancellationToken ct);
}

public sealed class BookService(IBookRepository repository, IBookMapper mapper) : IBookService
{
    public async Task<BookDto?> UpdatePriceAsync(UpdateBookPriceCommand command, CancellationToken ct)
    {
        Book? book = await repository.GetAsync(command.BookId, ct);
        if (book is null)
            return null;
        book.Price = command.NewPrice;
        await repository.UpdateAsync(book, ct);
        return mapper.ToDto(book);
    }
}

Yanında bir mapper ve onun interface'i, bir repository interface'i, bir DTO (Data Transfer Object) ve bir command. Infrastructure'da repository'nin implementasyonu, Web'de endpoint, request ve response record'ları:

using Bookstore.Application.Catalog;

namespace Bookstore.Web.Catalog;

public sealed record UpdateBookPriceRequest(decimal Price);
public sealed record BookResponse(int Id, string Title, decimal Price);

public static class BookEndpoints
{
    public static void MapBookEndpoints(this IEndpointRouteBuilder app) =>
        app.MapPut("/admin/books/{id:int}/price", async (int id, UpdateBookPriceRequest request, IBookService service, CancellationToken ct) =>
        {
            BookDto? dto = await service.UpdatePriceAsync(new UpdateBookPriceCommand(id, request.Price), ct);
            return dto is null ? Results.NotFound() : Results.Ok(new BookResponse(dto.Id, dto.Title, dto.Price));
        });
}

Her şey Clean Architecture'ın kurallarına uyuyor: Domain hiçbir şeye bağımlı değil, Application interface'lerini kendisi tanımlıyor, Web Infrastructure'ı yalnızca composition root'ta görüyor. Yarın ürün ekibi iki şey isteyecek: fiyat sıfır ya da negatif olamasın, ve kitaplara para birimi eklensin. Tahmin et: bu iki değişiklik için kaç dosyaya, kaç project'e dokunacaksın? "Fiyat sıfırdan büyük olmalı" kuralı bugün nerede yazıyor?

Bir Fiyat İçin Dokuz Dosya

Bu özelliğin dosyaları:

$ find src -name "*.cs" -not -path "*/obj/*" | sort | xargs wc -l
       4 src/Bookstore.Application/Catalog/BookDto.cs
      13 src/Bookstore.Application/Catalog/IBookMapper.cs
       9 src/Bookstore.Application/Catalog/IBookRepository.cs
      21 src/Bookstore.Application/Catalog/IBookService.cs
       8 src/Bookstore.Domain/Catalog/Book.cs
      20 src/Bookstore.Infrastructure/Catalog/BookRepository.cs
      17 src/Bookstore.Infrastructure/DependencyInjection.cs
      16 src/Bookstore.Web/Catalog/BookEndpoints.cs
      11 src/Bookstore.Web/Program.cs
     119 total

Dört project'e dağılmış dokuz dosya, 119 satır. İçinde tek implementasyonlu üç interface (IBookService, IBookMapper, IBookRepository), aynı kitabın verisini farklı şekillerde taşıyan beş tip (Book, BookDto, UpdateBookPriceCommand, UpdateBookPriceRequest, BookResponse) ve iki mapping var. Para birimini eklemek bu beş tipin hepsine ve iki mapping'e dokunmak demek. İkinci sorunun cevabı daha rahatsız edici: "fiyat sıfırdan büyük olmalı" kuralı hiçbir yerde yok. Book'un public setter'ları var, BookService fiyatı olduğu gibi yazıyor. Bütün bu katmanlar, korunacak bir iş kuralı olmayan bir veri güncellemesinin etrafına örülmüş.

Bu, serinin son sorusu: Clean Architecture ne zaman fazla gelir? Önceki sekiz yazı fikrin kaynaklarını, .NET 10'da nasıl kurulduğunu, nasıl test edilip korunduğunu anlattı. Bu yazı aynı yapının bedelini, o bedelin karşılığını vermediği durumları ve kuralları koruyup töreni azaltan yolları konuşuyor.

Belirtiler: Mimari Mi, Tören Mi?

Aşağıdakilerden birkaçını aynı anda görüyorsan, yapı muhtemelen problemin büyüklüğünü aşmış:

  • Interface'lerin neredeyse hepsinin tek implementasyonu var ve test'lerde de sahtesi yazılmıyor. SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) ve Design Principles serisinin Dependency Inversion Principle yazısında tek implementasyonlu interface'in ne zaman gereksiz olduğunu konuşmuştuk.
  • Bir request, cevap olarak dönene kadar dört beş kez şekil değiştiriyor ve her geçişin bir mapping kodu var.
  • Handler'ların çoğu sadece repository'yi çağırıp sonucu geri veriyor. Mark Richards ve Neal Ford'un "Fundamentals of Software Architecture" kitabında architecture sinkhole anti-pattern'i olarak geçen durum bu; Mimariye Giriş serisinin "Layered Architecture: Presentation, Business ve Data Layer'larının Gerçek Sorumlulukları" yazısında ayrıntılı.
  • Domain class'ları sadece property'lerden oluşuyor, kurallar service'lerde. Design Patterns serisinin "Anti-Pattern'ler: God Object, Service Locator, Singleton Abuse, Anemic Domain Model ve Pattern Hastalığı" yazısındaki anemic domain model'in mimari ölçekteki karşılığı.
  • Her değişiklik bütün project'lere yayılıyor; bir alan eklemek için dört project'te dosya açıyorsun.

Hiçbiri tek başına "bu mimari yanlış" demek değil. Ama hepsinin ortak noktası, katmanların bir şeyi korumak yerine sadece veri taşıması.

Template Kültü

Bu durumun en yaygın sebebi, bir solution template'ini olduğu gibi kopyalamak. .NET'te en çok kullanılan iki template'in Kasım 2026'daki haline baktım. jasontaylordev/CleanArchitecture (v10.8.0, Mart 2026) src altında yedi project içeriyor: Domain, Application, Infrastructure, Web, Shared, ServiceDefaults ve Aspire için AppHost; yanında altı test project'i. Main branch'teki bağımlılıkları arasında MediatR 14, AutoMapper 16 ve FluentValidation 12 var. ardalis/CleanArchitecture (v11.1.1, Nisan 2026) Core, UseCases, Infrastructure ve Web project'lerine AspireHost ve ServiceDefaults ekliyor, dört test project'iyle geliyor; FastEndpoints, source generator tabanlı Mediator, Vogen ve Ardalis.Specification kullanıyor.

İkisi de iyi düşünülmüş, yazarlarının yıllarca savunduğu kararları içeren başlangıç noktaları. Sorun, template'i kopyalayınca bu kararların hepsini de almış olman: hangi library'yi kullanacağını, validation'ın nerede duracağını, mapping'i nasıl yapacağını. Bu kararların bazıları zamanla değişiyor da. AutoMapper 15 ve MediatR 13 ile başlayarak iki library de 2 Temmuz 2025'te Lucky Penny Software'in RPL (Reciprocal Public License) 1.5 ve ticari lisans ikilisine geçti; ardalis template'i v11'de MediatR'ı bu yüzden bıraktı. Template'i kopyalayan bir ekip, template sahibinin yeniden düşündüğü bir kararı farkında olmadan taşımaya devam edebiliyor.

İlginç olan, ardalis'in aynı repository'de ikinci bir template, min-clean da sunması: tek bir Web project'i, içinde Domain, Infrastructure ve feature klasörleri. Template'in kendi ADR'ı (architecture decision record) bu seçimin bedelini açıkça yazıyor: geliştiricilerin klasör sınırlarına kendilerinin uyması gerekiyor, çünkü derleyici bunu zorlamıyor. 08. yazıda tam bu boşluğu dolduran aracı gördük.

Ne Zaman Değer, Ne Zaman Değil

Serinin üç kaynağı da bu soruya doğrudan değiniyor. Alistair Cockburn 2005'te Hexagonal Architecture'ı, uygulamayı birden çok farklı sürücüden (kullanıcı, test, batch script) eşit şekilde çalıştırabilmek için önerdi; tek bir HTTP (Hypertext Transfer Protocol) API'sinden (Application Programming Interface) ve tek bir veritabanından ibaret bir uygulamada bu ihtiyaç zayıf. Jeffrey Palermo 2008'deki ilk Onion yazısında açıkça yazıyor: "This architecture is not appropriate for small websites." Aynı yazıya göre uygun olduğu yer uzun ömürlü iş uygulamaları ve karmaşık davranışlı uygulamalar. Martin Fowler'ın 2007 tarihli "Design Stamina Hypothesis" yazısı da dengeyi şöyle kuruyor: iyi tasarımın bedeli bir noktadan sonra hız olarak geri döner, Fowler'a göre bu nokta çoğu zaman aylar değil haftalar sonra gelir; ama bunun bir hipotez olduğunu, ölçülerek kanıtlanamadığını da kendisi söylüyor.

DurumClean Architecture'ın tamamıDaha hafif bir yol
ÖmürYıllarca yaşayacak, birçok ekip el değiştirecekKampanya sitesi, prototip, birkaç ay yaşayacak bir araç
DomainKurallar, invariant'lar, durum geçişleri (sipariş, ödeme, stok)Ağırlıkla CRUD (create, read, update, delete); kural yok denecek kadar az
Adapter sayısıBirden çok driving (API, worker, console) ve driven (veritabanı, kargo, ödeme) adapterTek API, tek veritabanı
EkipBirden çok ekip aynı kod tabanındaTek küçük ekip, tek küçük servis
Test ihtiyacıİş kurallarını veritabanı olmadan, hızlı test etmek kritikUçtan uca birkaç integration test yetiyor

Bookstore'un kendisi bu tablonun iki sütununa da düşüyor. Ordering ortadaki sütuna oturuyor: stok, kopya limiti, concurrency, kargo firması, birden çok adapter. Catalog yönetimi ise neredeyse tamamen CRUD, yani sağdaki sütun. Bu da asıl cevabı veriyor: karar uygulama başına değil, bölge başına verilebilir.

Pragmatik Yol 1: Tek Project, Aynı Kural

Catalog için aynı özelliği tek bir project'te, klasörlerle yazalım. Domain klasöründeki Book bu sefer kuralını kendisi taşıyor:

namespace Bookstore.Catalog.Domain;

public sealed class Book(int id, string title, decimal price)
{
    public int Id { get; } = id;
    public string Title { get; } = title;
    public decimal Price { get; private set; } = price;

    public void ChangePrice(decimal newPrice)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(newPrice);
        Price = newPrice;
    }
}

Feature'ın kendisi tek bir dosya. Endpoint DbContext'i doğrudan kullanıyor, çünkü arada korunacak bir akış yok:

using System.ComponentModel.DataAnnotations;
using Bookstore.Catalog.Domain;
using Bookstore.Catalog.Infrastructure;

namespace Bookstore.Catalog.Features;

// Bir feature = bir dosya: request, endpoint ve akış yan yana
public static class UpdatePrice
{
    public sealed record Request([Range(0.01, 10_000)] decimal Price);
    public sealed record Response(int Id, string Title, decimal Price);

    public static void Map(IEndpointRouteBuilder app) =>
        app.MapPut("/admin/books/{id:int}/price", async (int id, Request request, CatalogDbContext db, CancellationToken ct) =>
        {
            Book? book = await db.Books.FindAsync([id], ct);
            if (book is null)
                return Results.NotFound();

            book.ChangePrice(request.Price);
            await db.SaveChangesAsync(ct);
            return Results.Ok(new Response(book.Id, book.Title, book.Price));
        });
}
$ find src -name "*.cs" -not -path "*/obj/*" | sort | xargs wc -l
      14 src/Bookstore.Catalog/Domain/Book.cs
      24 src/Bookstore.Catalog/Features/UpdatePrice.cs
      17 src/Bookstore.Catalog/Infrastructure/CatalogDbContext.cs
      23 src/Bookstore.Catalog/Program.cs
      78 total

Dört dosya, tek project, sıfır interface. Üstelik dört project'li versiyonda olmayan iki şey burada var: fiyatın sıfırdan büyük olması kuralı Domain'de, input validation da request'te. Microsoft'un "Architect Modern Web Applications with ASP.NET Core and Azure" e-kitabı da en küçük mimarinin tek project olduğunu ve bu durumda separation of concerns'ün klasörlerle sağlandığını anlatıyor. Çalışırken:

PUT /admin/books/1/price {"price":230}
HTTP 200
{"id":1,"title":"Dune","price":230}
PUT /admin/books/1/price {"price":0}
HTTP 400
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"Price":["The field Price must be between 0.01 and 10000."]}}
PUT /admin/books/42/price {"price":10}
HTTP 404

Tek project'in zayıf noktası, 08. yazının ilk ihlalinin aksine derleyicinin artık hiçbir oku korumaması. Domain klasöründen Infrastructure'a bir using eklemek tamamen geçerli C#. Kuralı derleyiciden architecture test'ine taşıyoruz:

using ArchUnitNET.Domain;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using Bookstore.Catalog.Domain;
using static ArchUnitNET.Fluent.ArchRuleDefinition;

namespace Bookstore.Catalog.ArchitectureTests;

// Tek project, tek assembly: sınırı derleyici değil bu test'ler koruyor
public sealed class FolderRuleTests
{
    private static readonly Architecture Architecture =
        new ArchLoader().LoadAssemblies(typeof(Book).Assembly).Build();

    [Fact]
    public void Domain_folder_knows_nothing_else() =>
        Types().That().ResideInNamespaceOrBelow("Bookstore.Catalog.Domain").Should()
            .NotDependOnAny(Types(true).That()
                .ResideInNamespaceOrBelow("Bookstore.Catalog.Infrastructure")
                .Or().ResideInNamespaceOrBelow("Bookstore.Catalog.Features")
                .Or().ResideInNamespaceOrBelow("Microsoft.EntityFrameworkCore")
                .Or().ResideInNamespaceOrBelow("Microsoft.AspNetCore"))
            .Check(Architecture);

    [Fact]
    public void Infrastructure_does_not_reach_into_features() =>
        Types().That().ResideInNamespaceOrBelow("Bookstore.Catalog.Infrastructure").Should()
            .NotDependOnAny(Types().That().ResideInNamespaceOrBelow("Bookstore.Catalog.Features"))
            .Check(Architecture);
}

Biri Book'a "kendini bulsun" diye bir kısayol eklediğinde:

// tek project'te derleyici buna itiraz etmez
public static ValueTask<Book?> FindAsync(CatalogDbContext db, int id) => db.Books.FindAsync(id);
Failed Bookstore.Catalog.ArchitectureTests.FolderRuleTests.Domain_folder_knows_nothing_else [76 ms]
ArchUnitNET.xUnit.FailedArchRuleException : "Types that reside in namespace with full name "Bookstore.Catalog.Domain" or below should not depend on any Types (including referenced) that reside in namespace with full name "Bookstore.Catalog.Infrastructure" or below or reside in nam..." failed:
	Bookstore.Catalog.Domain.Book does depend on "Bookstore.Catalog.Infrastructure.CatalogDbContext" and "Microsoft.EntityFrameworkCore.DbSet`1"

Derleyici sesini çıkarmadı, test çıkardı. Clean Architecture'ın özü, Dependency Rule, project sayısına bağlı değil. Project'ler bu kuralı derleyiciye zorlatmanın bir yolu; architecture test'leri bir diğeri. Hangisinin daha ucuz olduğu kod tabanının büyüklüğüne bağlı.

Pragmatik Yol 2: Karmaşıklığa Göre Karıştırmak

Robert C. Martin'in 2017 tarihli "Clean Architecture" kitabının "Partial Boundaries" bölümü, tam bir sınırın pahalı olduğunu kabul ediyor ve üç ara yol tarif ediyor: sınırın bütün hazırlığını yapıp parçaları yine de aynı component'te tutmak, sınırın yalnızca bir tarafında interface kullanan tek yönlü bir sınır, ve bir facade arkasına saklanmak. Kitabın mesajı, sınırların nerede tam, nerede kısmi olacağına mimarın bilerek karar vermesi gerektiği; her yere tam sınır çizmek de hiç çizmemek kadar bir karar.

Bookstore için bu şu anlama geliyor: Ordering, 05–08. yazılardaki gibi Domain, Application, Infrastructure ve Web project'leriyle, handler'ları, port'ları ve architecture test'leriyle kalıyor. Catalog yönetimi tek bir project'te, endpoint'ten doğrudan DbContext'e giden ince dilimlerle ama aynı Dependency Rule test'iyle yaşıyor. Bir gün Catalog'a gerçek kurallar gelirse (fiyat geçmişi, kampanya, onay akışı) o bölgeyi Ordering'in şekline taşırsın; ardalis'in tek project'li template'inin dokümanı da bu yolu "ihtiyaç olunca birden çok project'e ayrılabilir" diye tarif ediyor.

Bu yaklaşımın bir adı da var: Vertical Slice Architecture. Jimmy Bogard'ın 2018'de anlattığı fikir, kodu teknik katmanlara göre değil, feature'lara göre dilimlemek ve her dilimin kendi ihtiyacı kadar abstraction kullanmasına izin vermek. Clean Architecture'ın iç çemberiyle birleştirilmesi, MediatR kararı ve slice'lar arası paylaşım ilerideki Vertical Slice Architecture serisinin konusu.

Layered Projeden Kademeli Geçiş

Çoğu zaman sıfırdan başlamıyorsun; elinde Mimariye Giriş serisinde anlattığımız türden, Business'ın doğrudan Data'ya bağlı olduğu bir layered proje var. Hepsini bir sprint'te Clean Architecture'a çevirmeye çalışmak, yarım kalmış bir refactoring ile iki mimarinin aynı anda yaşadığı bir kod tabanı bırakır. Kademeli bir plan:

  • Önce bugünkü durumu dondur: yön kuralını architecture test'i olarak yaz, mevcut ihlalleri kabul et, yenilerini engelle.
  • En çok değişen ya da en çok hata çıkan use case'ten başla; onun için Business'ta bir port tanımla, Data'daki class o port'u implement etsin.
  • Bu use case'in kurallarını service'ten entity'lere taşı ve veritabanısız core test'lerini yaz.
  • Port'ları ve entity'leri yeterince birikince ayrı Domain ve Application project'lerine taşı; derleyici o andan itibaren oku seninle birlikte korur.
  • CRUD olarak kalan bölgeleri zorla taşıma; kural yoksa katman da gerekmez.

İlk adım için ArchUnitNET'in FreezingArchRule özelliği birebir. Eski kodda iş kuralı doğrudan Data'daki somut class'a bağlı:

using Bookstore.Legacy.Data;

namespace Bookstore.Legacy.Business;

// Eski kod: iş kuralı doğrudan Data'daki somut class'a bağlı
public sealed class OrderService(OrderRepository repository)
{
    public void Place(Guid orderId, decimal total) => repository.Save(orderId, total);
}

Kural yazılıyor ve donduruluyor:

public sealed class FrozenRuleTests
{
    private static readonly Architecture Architecture =
        new ArchLoader().LoadAssemblies(typeof(OrderService).Assembly).Build();

    [Fact]
    public void Business_does_not_depend_on_data_no_new_violations()
    {
        IArchRule rule = Types().That().ResideInNamespaceOrBelow("Bookstore.Legacy.Business").Should()
            .NotDependOnAny(Types().That().ResideInNamespaceOrBelow("Bookstore.Legacy.Data"));

        // bugünkü ihlaller dosyaya yazılır; yalnızca yenileri test'i kırar
        FreezingArchRule.Freeze(rule).Check(Architecture);
    }
}

İlk koşuda test geçiyor ve bugünkü ihlali test project'indeki ArchUnitNET/Storage/FrozenRules.json dosyasına yazıyor. Bu dosya repository'ye commit edilir:

[
  {
    "ArchRuleDescription": "Types that reside in namespace with full name \"Bookstore.Legacy.Business\" or below should not depend on any Types that reside in namespace with full name \"Bookstore.Legacy.Data\" or below",
    "Violations": [
      "Bookstore.Legacy.Business.OrderService"
    ]
  }
]

Bir hafta sonra biri aynı alışkanlıkla yeni bir class yazıyor:

using Bookstore.Legacy.Data;

namespace Bookstore.Legacy.Business;

// Yeni kod, eski alışkanlık
public sealed class InvoiceService
{
    public void Issue(Guid orderId) => SqlHelper.Execute($"INSERT invoice for {orderId}");
}
Failed Bookstore.Legacy.ArchitectureTests.FrozenRuleTests.Business_does_not_depend_on_data_no_new_violations [48 ms]
ArchUnitNET.xUnit.FailedArchRuleException : "Types that reside in namespace with full name "Bookstore.Legacy.Business" or below should not depend on any Types that reside in namespace with full name "Bookstore.Legacy.Data" or below" failed:
	Bookstore.Legacy.Business.InvoiceService does depend on "Bookstore.Legacy.Data.SqlHelper"

Test yalnızca yeni ihlali gösteriyor; OrderService listede yok, çünkü o donmuş durumda. OrderService'i bir port'a çevirip InvoiceService'i sildiğim bir kopyada test yeniden geçti ve dosya kendiliğinden küçüldü:

[
  {
    "ArchRuleDescription": "Types that reside in namespace with full name \"Bookstore.Legacy.Business\" or below should not depend on any Types that reside in namespace with full name \"Bookstore.Legacy.Data\" or below",
    "Violations": []
  }
]

Boş liste hedefin kendisi: o noktada Freeze çağrısını kaldırıp kuralı normal bir kural olarak bırakırsın.

Sık Düşülen Tuzaklar

Mimariyi template'in klasör yapısı sanmak. Domain, Application, Infrastructure ve Web adında dört project açmak Dependency Rule'u kendiliğinden sağlamaz; Application'da IQueryable dönen bir repository ya da Domain'de bir Entity Framework Core (EF Core) attribute'u, isimler doğru olsa da kuralı deler. Kontrol edilecek şey isimler değil, oklar.

"Her ihtimale karşı" interface. Yarın başka bir veritabanına geçeriz diye her class'ın önüne bir interface koymak, SOLID serisinde ayrıntılı işlediğimiz YAGNI'nın (You Aren't Gonna Need It) tam karşısı. Interface'i bir ikinci implementasyon, bir test sahtesi ya da bir sınır gerçekten gerektirdiğinde ekle; IDE'nin (integrated development environment) "Extract Interface" refactoring'i bir dakika sürüyor.

Tersine aşırılık: hiç sınır çizmemek. "Clean Architecture fazla" sonucundan "her şeyi endpoint'e yazalım" sonucuna atlamak da bir hata. Ordering gibi kurallı bir bölgede bu, 06. yazının açılışındaki yarım siparişe geri dönmek demek.

Geçişi yarıda bırakmak. Bir module'ü yeni yapıya taşıyıp diğerlerini eski düzende bırakmak sorun değil; sorun, hangi bölgenin hangi kurala tabi olduğunun yazılı olmaması. Frozen rule dosyası ve architecture test'leri bunun en somut kaydı.

Seriyi Toparlamak

Dokuz yazıda tek bir fikrin üç anlatımını ve .NET 10'daki karşılığını izledik. Layered Architecture'ın tıkandığı yerden başladık: iş kuralları veritabanına ve framework'lere kilitlendiğinde. Cockburn'ün port ve adapter'ları uygulamayı dış dünyadan yalıttı, Palermo'nun halkaları iç yapıya da söz söyledi, Martin'in Dependency Rule'u ikisini tek cümleye indirdi: kaynak kod bağımlılıkları yalnızca içe bakar. Sonra Bookstore'un Ordering'ini bu kurala göre kurduk: tek amaçlı handler'lar ve tek transaction sınırı, Infrastructure'da kalan EF Core mapping'i ve typed HttpClient adapter'ı, veritabanısız core test'leri, Testcontainers ile adapter test'leri ve kuralı CI'da (continuous integration) koruyan ArchUnitNET test'leri. Bu son yazıda da aynı kuralın bazen dört project, bazen tek project ve bir test dosyası kadar yer kapladığını gördük. Akılda kalması gereken cümle şu: Clean Architecture bir klasör yapısı değil, bağımlılıkların yönü hakkında bir karar; o kararı ne kadar törenle uygulayacağın ise problemin büyüklüğüne bağlı.

Küçük Bir Deneme

★☆☆ Dört project'li Catalog örneğine "fiyat sıfırdan büyük olmalı" kuralını ekle. Kuralı hangi dosyaya koydun ve kaç dosyaya dokundun? Aynısını tek project'li versiyonda say.

★★☆ Kendi projelerinden birinde belirtiler listesini uygula: tek implementasyonlu interface'leri ve pass-through handler'ları say. Hangilerini silmek, hangilerini tutmak istersin, neden?

★★★ Bir layered projeye FreezingArchRule ile "Business Data'ya bağımlı olmasın" kuralını ekle. Donmuş ihlal listesindeki ilk class'ı bir port'a çevir ve FrozenRules.json'ın küçüldüğünü gör. Kademeli geçiş planındaki hangi adımı tamamlamış oldun?

Bu yazıyla Clean, Onion ve Hexagonal Architecture serisi bitiyor. Yolun devamı, bu yazıda birkaç kez kapısını araladığımız soruda: kodu katmanlara göre değil feature'lara göre dilimlemek. Yazılım Mimarisi kategorisinin sıradaki serisi Vertical Slice Architecture, MediatR'lı ve MediatR'sız slice'larla, Clean Architecture'la nerede buluşup nerede ayrıldığıyla başlıyor.