Skip to content

Bölüm 13 / 15

Mimari

Katmanlı mimari, Clean Architecture, DDD'ye kısa bakış.

12 dakikalık okuma

Şu ana kadar sınıf ve nesne ölçeğinde çalıştın: ilkeler, desenler, bağımlılık yönü. Bu ders aynı fikirleri tüm sistemin ölçeğine taşıyor. Buradaki kararlar sonradan değiştirmesi en pahalı kararlardır — bir metodu yeniden yazmak yarım saat, katman sınırını değiştirmek aylar sürer. DIP dersinde çizdiğin bağımlılık oku, Facade dersinde koyduğun sınır ve desen dersinde adı geçen "anemik alan modeli" bu derste birleşiyor.

01Ne öğreneceksin

  • Mimarinin tanımı: neyin karara bağlandığı ve neden geri dönüşü pahalı olduğu
  • Klasik katmanlı mimari, kuralları ve yapısal kusuru
  • Bağımlılık kuralı: Ports & Adapters / Onion / Clean Architecture aynı fikrin üç adı
  • Proje yapısı: hangi tip hangi projede durur, referans okları nereye bakar
  • Uygulama servisi (kullanım senaryosu) katmanı ve DTO sınırı
  • DDD'nin kavram sözlüğü: varlık, değer nesnesi, toplam kök, alan servisi, alan olayı, sınırlı bağlam
  • Anemik alan modeli anti-deseni ve gerçek karşılığı
  • Modüler monolit ile mikroservis kararı; mimari kuralı testle korumak

02Mimari nedir

Kullanışlı tanım şudur: mimari, sistemin parçalara bölünüş biçimi ve bu parçaların birbirine bağlanma kurallarıdır — yani "sonradan değiştirmesi pahalı" olan kararlar kümesi. Bir sınıfın adını değiştirmek ucuzdur; "iş kuralları veritabanına bağlı olsun mu" kararını üç yıl sonra değiştirmek, o üç yılda yazılmış her dosyaya dokunmayı gerektirir.

Mimarinin işi kod yazmayı zorlaştırmak değildir; yanlış kodu yazmayı zorlaştırmaktır. İyi bir mimaride "veri erişimini iş kuralının içine yazmak" disiplin meselesi olmaktan çıkar, derleme hatasına dönüşür. Bu ölçüt bu dersin geri kalanında tekrar tekrar kullanılacak.

Karar verirken bakılan üç şey vardır: değişim eksenleri (sistemin hangi parçası hangi sebeple değişiyor — SRP'nin sistem ölçeği), ekip yapısı (bir modülün sahibi kim; birden çok ekip aynı dosyaya yazıyorsa sınır yanlış çizilmiştir) ve işletme gerçekleri (yük, kullanılabilirlik hedefi, dağıtım sıklığı).

03Katmanlı mimari ve kusuru

En yaygın başlangıç düzeni üç katmandır:

Sunum   (Controller / UI)
   ↓
İş      (Servisler, kurallar)
   ↓
Veri    (Repository, DbContext, SQL)

Kuralı basittir: her katman yalnız bir alttakini tanır, atlamalı çağrı (sunumdan doğrudan veriye) ve yukarı doğru bağımlılık yasaktır. Kazancı da gerçektir: sorumluluklar ayrışır, kod nerede aranacağı bilinir, ekip ortak bir haritayı paylaşır.

Yapısal kusuru ise şudur: okun yönü. İş katmanı, veri katmanına bağlıdır. Yani sistemin en değerli ve en uzun ömürlü parçası (kurallar), en oynak ve en teknik parçaya (veritabanı, ORM) bağımlıdır. Sonuçları tanıdıktır: iş kurallarını test etmek için veritabanı gerekir; ORM'in tipleri ve öznitelikleri iş sınıflarına sızar; veri şeması değişince kural dosyaları açılır.

DIP dersinin cümlesi burada sistem ölçeğine çıkar: ok, ayrıntıdan kurala doğru dönmelidir. Bunu yapmanın yolunu zaten biliyorsun — arayüzü çağıran katmana koymak. 12. dersteki ISiparisDeposu tam olarak buydu: arayüz iş katmanında, uygulaması altyapıda.

04Bağımlılık kuralı: Clean Architecture

Aynı fikrin üç yaygın adı vardır ve üçü de aynı kuralı söyler: Ports & Adapters (Hexagonal), Onion, Clean Architecture. Kural tek cümledir: kaynak kod bağımlılıkları yalnız içeri, merkeze doğru bakabilir. İçerideki hiçbir şey dışarıdaki bir şeyin adını bilmez.

        ┌───────────────────────────────────────────┐
        │  Altyapı / Sunum  (EF Core, HTTP, kuyruk)  │   ← değişken, teknik
        │   ┌───────────────────────────────────┐    │
        │   │  Uygulama  (kullanım senaryoları) │    │
        │   │    ┌─────────────────────────┐    │    │
        │   │    │  Alan (Domain)          │    │    │   ← kurallar, en kararlı
        │   │    │  varlıklar, kurallar    │    │    │
        │   │    └─────────────────────────┘    │    │
        │   └───────────────────────────────────┘    │
        └───────────────────────────────────────────┘
             bağımlılık okları: hep içeri  →

.NET'te bu, proje referanslarıyla derleyici tarafından zorlanır — mimari belge olmaktan çıkıp kurala dönüşür:

Ticaret.Domain          → (referans yok)
Ticaret.Application     → Ticaret.Domain
Ticaret.Infrastructure  → Ticaret.Application, Ticaret.Domain
Ticaret.Api             → Ticaret.Application, Ticaret.Infrastructure (yalnız Program.cs için)

Ne nereye konur:

KatmanİçindekilerBilmediği şeyler
Domain Varlıklar, değer nesneleri, alan servisleri, alan olayları, depo arayüzleri, iş kuralı istisnaları EF Core, HTTP, JSON, DI konteyneri — hiçbir dış paket
Application Kullanım senaryoları (uygulama servisleri / komut-sorgu işleyicileri), DTO'lar, dış servis arayüzleri (IBildirici, IOdemeServisi) Hangi veritabanı, hangi kuyruk, hangi HTTP çerçevesi
Infrastructure Depo uygulamaları, DbContext, dosya/e-posta/ödeme adaptörleri, önbellek —
Api / UI Denetleyiciler, model bağlama, kimlik doğrulama, Program.cs (kompozisyon kökü) İş kuralı (hiç olmamalı)

Sunum ve altyapının aynı halkada olması ilk bakışta şaşırtır ama tutarlıdır: ikisi de dış dünyaya bakan adaptörlerdir — biri kullanıcının, diğeri veritabanının adaptörü. Bu yüzden desenin diğer adı Ports & Adapters'tır: içerideki arayüzler "port", dışarıdaki uygulamalar "adaptör"dür. 10. dersteki Adapter deseni burada mimari role dönüşür.

Tek istisna kompozisyon köküdür: Program.cs her şeyi tanır, çünkü nesneleri birbirine orada bağlarsın. Bu, DIP dersinde konuşulan kuraldır — bağımlılıklar tek bir yerde bilinir, geri kalan her yerde arayüz görünür.

Bedeli dürüstçe söylenmeli: dört proje, ek eşleme (varlık ↔ DTO), daha çok dosya ve küçük bir işte fark edilir bir yavaşlama. Beş uç noktalı bir CRUD servisi için bu düzen israftır; orada tek proje, klasörlerle ayrılmış bir yapı daha doğrudur. Karar ölçütü: iş kuralının ağırlığı ve sistemin öngörülen ömrü.

05Uygulama servisi ve DTO sınırı

Facade dersinde bir sınır çizmiştik: facade yalnız yönlendirme ve sıralama yapar, iş kuralı içermeye başladığı anda Tanrı sınıfına döner. Uygulama servisi (ya da kullanım senaryosu / komut işleyicisi) o facade'ın mimari karşılığıdır:

public sealed class SiparisiOnayla                        // uygulama servisi
{
    private readonly ISiparisDeposu _siparisler;
    private readonly IOdemeServisi _odeme;
    private readonly IIsBirimi _isBirimi;

    public async Task<SiparisOzetiDto> Calistir(int siparisNo, CancellationToken ct)
    {
        var siparis = await _siparisler.Getir(siparisNo, ct)
            ?? throw new SiparisBulunamadi(siparisNo);

        var tahsilat = await _odeme.Tahsil(siparis.OdenecekTutar, ct);

        siparis.Onayla(tahsilat.Referans);                 // kural nesnenin kendisinde
        await _isBirimi.Kaydet(ct);

        return SiparisOzetiDto.From(siparis);
    }
}

Gövdede koşul ve hesap yok; sıra var. "Sipariş onaylanabilir mi, hangi durumdan hangisine geçebilir, tutar nasıl hesaplanır" sorularının cevabı Siparis sınıfının içindedir. Uygulama servisinin sorumluluğu senaryoyu yürütmektir: nesneyi bul, dış dünyayla konuş, alan nesnesine kararı ver, sonucu kaydet, dışarı dönecek biçimi hazırla.

DTO sınırı de bu katmandadır. Alan nesneleri dışarı sızmamalıdır: sızarsa serileştirme öznitelikleri alan sınıflarına girer, API sözleşmesi veri modeline yapışır ve şemadaki her değişiklik istemcileri kırar. Kural: dışarı çıkan ve içeri giren her şey DTO'dur; eşleme uygulama katmanında yapılır. Elle eşleme uzun görünse de açıktır ve derleme zamanında kontrol edilir; otomatik eşleyiciler yazımı kısaltır ama hataları çalışma zamanına erteler.

06DDD'ye kısa bakış ve anemik alan modeli

Alan güdümlü tasarım (Domain-Driven Design), karmaşık iş alanlarını modellemek için bir yaklaşımdır. Buradaki hedef DDD'yi öğrenmek değil, mülakatta ve kod incelemesinde geçen sözlüğünü doğru kullanmaktır:

  • Ortak dil (ubiquitous language) — kodda kullanılan adlar, iş biriminin kullandığı adlarla aynıdır. "Sipariş iptal edildi" iş dilindeyse kodda siparis.Iptal() vardır, UpdateStatus(3) değil.
  • Varlık (entity) — kimliği olan ve durumu zamanla değişen nesne (Siparis). Eşitliği kimliğine bakılarak belirlenir.
  • Değer nesnesi (value object) — kimliği olmayan, değeriyle tanımlanan, değişmez nesne (Para, Adres, Email). 3. hattın record ve readonly record struct dersi bunun dildeki karşılığıdır; 1. dersteki "ilkel takıntısı" kokusunun çözümü de budur.
  • Toplam (aggregate) ve toplam kök (aggregate root) — birlikte tutarlı kalması gereken nesne kümesi ve dışarının erişebildiği tek giriş noktası. SiparisKalemi'ne yalnız Siparis üzerinden erişilir; 12. derste "depo toplam kök başına yazılır" kuralının sebebi budur.
  • Alan servisi (domain service) — tek bir varlığa ait olmayan kural (iki hesap arasındaki para transferi gibi). İş kuralıdır, uygulama servisi değildir.
  • Alan olayı (domain event) — "olan bitmiş bir iş gerçeği": SiparisOnaylandi. Observer deseninin alan dilindeki hâlidir ve yan etkileri (bildirim, muhasebe kaydı) ana akıştan ayırır.
  • Sınırlı bağlam (bounded context) — aynı kelimenin farklı anlamlar taşıdığı sınır. Satış için "müşteri" ile muhasebe için "müşteri" aynı tablo olabilir ama aynı model değildir. Mikroservis sınırları çizilirken doğru ölçüt budur.

Anemik alan modeli (8. derste adı geçmişti) bu resmin karşıtıdır: varlıklar yalnız get; set; özelliklerinden oluşur, tüm kurallar ...Manager / ...Service sınıflarındadır. Fowler bunu bir anti-desen olarak adlandırır çünkü nesne yönelimli görünen ama olmayan bir yapıdır: veri ile onu değiştiren davranış ayrılmıştır — 3. dersteki kapsülleme ihlalinin sistem ölçeğindeki hâli.

// anemik: nesne kendini koruyamaz, kural dışarıda ve tekrar edilir
siparis.Durum = SiparisDurumu.Onaylandi;
siparis.OnayTarihi = DateTime.UtcNow;

// zengin model: geçersiz duruma girmek imkânsız, kural tek yerde
public void Onayla(string odemeReferansi)
{
    if (Durum != SiparisDurumu.Beklemede)
        throw new GecersizDurumGecisi(Durum, SiparisDurumu.Onaylandi);
    if (Kalemler.Count == 0)
        throw new BosSiparisOnaylanamaz(No);

    Durum = SiparisDurumu.Onaylandi;
    OnayTarihi = DateTime.UtcNow;
    OdemeReferansi = odemeReferansi;
    _olaylar.Add(new SiparisOnaylandi(No));
}

Farkı yaratan şey satır sayısı değil, geçersiz durumun temsil edilemez olmasıdır: özellikler dışarıya private set ile kapalıdır, tek geçiş yolu metottur, kural tek yerde durur ve unutulamaz.

Dürüst not: her sistem zengin alan modeli gerektirmez. İş kuralı ince olan, esas olarak veri taşıyan bir serviste "anemik" yapı pragmatik doğru cevaptır — orada DDD altyapısı bedel üretir, kazanç üretmez. Ölçüt yine aynı: kuralların yoğunluğu.

07Monolit, modüler monolit, mikroservis

Mimari sorusunun ikinci yarısı, sistemin kaç parçaya dağıtılacağıdır. Üç seçenek ve gerçek ölçütleri:

  • Monolit — tek dağıtım birimi. Basit, hızlı, tek işlem içinde tutarlılık. Kusuru, sınırların zamanla erimesi ve her değişikliğin tüm sistemi yeniden dağıtmayı gerektirmesidir.
  • Modüler monolit — tek dağıtım birimi ama içeride sınırları kod düzeyinde korunan modüller (her modül kendi alan modeli, kendi şeması, dışarıya yalnız açık sözleşmesi). Bugün çoğu ekip için doğru başlangıçtır: dağıtık sistemin bedelini ödemeden modülerlik kazandırır ve gerçekten gerektiğinde bir modül servise ayrılabilir.
  • Mikroservis — ayrı dağıtılan, ayrı veriye sahip servisler. Kazancı teknik değil örgütseldir: bağımsız ekiplerin bağımsız dağıtım yapabilmesi. Bedeli ağ gecikmesi, kısmi hata, dağıtık işlem yokluğu (nihai tutarlılık), izlenebilirlik ve işletim yüküdür.

En sık görülen hata dağıtık monolittir: servisler ayrılmış ama birbirine sıkı bağlıdır — biri değişince ötekiler de dağıtılmak zorundadır, üstelik artık ağ üzerinden konuşurlar. Yani monolitin bağımlılığı ile dağıtık sistemin bedeli aynı anda ödenir. Bunun ilacı, sınırları ölçüye vurmaktır: iki parça sık sık birlikte değişiyorsa, onlar bir parçadır. 19. hatta bu konuyu iletişim biçimleri, tutarlılık modelleri ve dayanıklılık kalıplarıyla ayrıntılandıracaksın.

Son olarak, mimari kuralı yazılı belgede bırakmak yerine sınanabilir kılmak gerekir. En ucuz yol proje referanslarını doğru kurmaktır — Domain projesi Infrastructure'ı referans almıyorsa yanlış kod derlenmez. Bunun ötesinde mimari kurallarını doğrulayan testler yazılır ("Domain ad alanındaki hiçbir tip Microsoft.EntityFrameworkCore ad alanına referans vermemeli"); 15. hatta test türlerini işlerken bu tür sınamalara döneceğiz.

08Mini alıştırma — Katmanları ayır

Yaklaşık 20 dakika · dotnet new classlib ×3 + konsol

  1. İskelet: Ticaret.Domain, Ticaret.Application, Ticaret.Infrastructure sınıf kütüphaneleri ve bir konsol projesi oluştur. Referansları yalnız içeri bakacak biçimde ekle (dotnet add reference).
  2. Kuralı sına: Domain projesinden Infrastructure'a referans eklemeyi dene — döngüsel referans hatası al. Mimari kuralın neden derleyici tarafından zorlandığını bir cümleyle yaz.
  3. Zengin varlık: Siparis sınıfını private set özellikleriyle yaz; Onayla ve Iptal metotlarını geçersiz geçişleri reddedecek biçimde uygula. Dışarıdan Durum'u atamayı dene — derlenmediğini gör.
  4. Değer nesnesi: Para(decimal Tutar, string ParaBirimi) değer nesnesini readonly record struct ile yaz; farklı para birimlerinin toplanmasını istisna ile engelle.
  5. Uygulama servisi: SiparisiOnayla senaryosunu yaz. Gövdesinde hiç if olmadığını doğrula; olduysa o kuralın alan modeline mi ait olduğunu sor.
  6. Adaptör: Infrastructure'da ISiparisDeposu'nun bellek içi uygulamasını yaz; konsol projesinde (kompozisyon kökü) hepsini bağla.
  7. Anemik karşılaştırma: aynı senaryoyu bir de anemik modelle yaz — kurallar bir SiparisManager sınıfında. İkinci bir çağıran ekle ve kuralı unutmanın ne kadar kolay olduğunu gör.

09Kendini sına

Klasik katmanlı mimarinin yapısal kusuru nedir, nasıl düzeltilir?

Kusur bağımlılık okunun yönüdür: iş katmanı veri katmanına bağlıdır, yani sistemin en değerli ve en uzun ömürlü parçası (kurallar) en oynak ve teknik parçaya (veritabanı, ORM) bağımlıdır. Sonuçları: kuralları test etmek için veritabanı gerekir, ORM tipleri ve öznitelikleri iş sınıflarına sızar, şema değişince kural dosyaları açılır. Düzeltme DIP'in sistem ölçeğidir: arayüz çağıran katmana konur — depo arayüzü iş katmanında tanımlanır, uygulaması altyapıdadır — böylece ok ayrıntıdan kurala doğru döner. Clean Architecture / Onion / Ports & Adapters aynı kuralın üç adıdır: kaynak kod bağımlılıkları yalnız içeri bakabilir. .NET'te bu proje referanslarıyla derleyici tarafından zorlanır; Domain projesi hiçbir dış pakete referans vermez. Tek istisna kompozisyon köküdür — Program.cs her şeyi tanır, çünkü bağlama orada yapılır.

Uygulama servisi ile alan modeli arasındaki iş bölümü nedir?

Uygulama servisi senaryoyu yürütür: nesneyi bulur, dış dünyayla konuşur, alan nesnesine kararı verdirir, iş birimini kaydeder ve dışarı dönecek DTO'yu hazırlar. Gövdesinde koşul ve hesap değil, sıra vardır — Facade dersindeki sınırın mimari karşılığı budur ve iş kuralı içermeye başladığında Tanrı sınıfına döner. Kural ise alan modelindedir: "hangi durumdan hangisine geçilebilir, tutar nasıl hesaplanır" sorularının cevabı varlığın metotlarındadır. Sınırın ikinci parçası DTO'dur: alan nesneleri dışarı sızmamalıdır, yoksa serileştirme öznitelikleri alan sınıflarına girer ve API sözleşmesi veri modeline yapışır. Eşleme uygulama katmanında yapılır; elle eşleme derleme zamanında denetlenir, otomatik eşleyiciler hatayı çalışma zamanına erteler.

Anemik alan modeli nedir, neden anti-desen sayılır, her zaman kötü müdür?

Varlıkların yalnız get; set; özelliklerinden oluştuğu, tüm kuralların Manager/Service sınıflarında toplandığı yapıdır. Anti-desen sayılmasının sebebi nesne yönelimli görünüp olmamasıdır: veri ile onu değiştiren davranış ayrılmıştır — kapsülleme ihlalinin sistem ölçeği. Pratik sonucu, nesnenin kendini koruyamaması ve aynı kuralın birden çok çağıranda tekrar edilip er geç birinde unutulmasıdır. Zengin modelde özellikler private set ile kapalıdır, geçiş yalnız metotla yapılır ve geçersiz durum temsil edilemez hâle gelir. Her zaman kötü değildir: iş kuralı ince olan, esas olarak veri taşıyan servislerde anemik yapı pragmatik doğrudur; DDD altyapısı orada bedel üretir, kazanç üretmez. Ölçüt kuralların yoğunluğudur.

Mikroservise ne zaman geçilir, "dağıtık monolit" nedir?

Mikroservisin kazancı teknik değil örgütseldir: bağımsız ekiplerin bağımsız dağıtım yapabilmesi. Bedeli ağ gecikmesi, kısmi hata, dağıtık işlemin olmaması (nihai tutarlılık), izlenebilirlik ve işletim yüküdür. Bu bedeli ödemeyi gerektiren bir örgütsel ihtiyaç yoksa doğru başlangıç modüler monolittir: tek dağıtım birimi ama içeride sınırları kod düzeyinde korunan, kendi modeli ve şeması olan modüller; gerçekten gerektiğinde bir modül servise ayrılır. Dağıtık monolit ise en sık görülen hatadır: servisler ayrılmıştır ama sıkı bağlıdır, biri değişince ötekiler de dağıtılmak zorundadır ve artık ağ üzerinden konuşurlar — yani monolitin bağımlılığı ile dağıtık sistemin bedeli birlikte ödenir. Sınır ölçütü şudur: iki parça sık sık birlikte değişiyorsa onlar tek parçadır; doğru sınır, DDD'nin sınırlı bağlam kavramıyla çizilir. Mimari kuralı belgede bırakmam — proje referansları ve mimari testleriyle sınanabilir kılarım.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) Tanım: "Katmanlı mimariyi ve katmanlarını açıklayınız"; her katmanın sorumluluğu. (2) Kural: "Clean Architecture'ın bağımlılık kuralı nedir" — tek cümlede "bağımlılıklar yalnız içeri bakar". (3) Yerleştirme: verilen tiplerin (DbContext, Controller, Repository arayüzü, varlık, DTO) hangi katmana ait olduğu. (4) İsimlendirme: Hexagonal / Ports & Adapters / Onion / Clean Architecture ilişkisi. (5) DDD sözlüğü: varlık ile değer nesnesi farkı; toplam kök nedir; sınırlı bağlam nedir; alan servisi ile uygulama servisi farkı. (6) Anti-desen: anemik alan modeli tanımı ve neden eleştirildiği. (7) Karşılaştırma: monolit ile mikroservis artı/eksileri; ne zaman mikroservis. (8) Kod okuma: iş kuralı içeren bir denetleyici verilip sorunun ve düzeltmenin yazılması.

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Tanım — "mimari, sistemin parçalara bölünüşü ve bağlanma kurallarıdır; işi kod yazmayı zorlaştırmak değil, yanlış kodu yazmayı zorlaştırmaktır." (2) Sorun — "klasik katmanlı yapıda iş katmanı veri katmanına bağlıdır; en değerli parça en oynak parçaya bağımlı olur." (3) Çözüm — "bağımlılık kuralı: oklar yalnız içeri bakar. Depo arayüzünü Domain'e koyar, uygulamasını Infrastructure'a bırakırım; .NET'te bunu proje referanslarıyla derleyiciye zorlatırım." (4) Katman içeriği — "Domain'de varlıklar ve kurallar, Application'da kullanım senaryoları ve DTO'lar, Infrastructure'da EF Core ve adaptörler, Api'de yalnız denetleyici ve kompozisyon kökü." (5) DDD dokunuşu — "varlık kimliğiyle, değer nesnesi değeriyle tanımlanır; erişim toplam kök üzerinden olur, o yüzden depoyu kök başına yazarım. Anemik alan modelinden kaçınırım: siparis.Durum = ... yerine siparis.Onayla(...)." (6) Kıdem vuruşu — "bedelini de söylerim: dört proje ve eşleme yükü küçük bir CRUD servisi için israftır; orada tek proje kurarım. Aynı şekilde mikroservise teknik hevesle değil, bağımsız dağıtım ihtiyacı doğduğunda geçerim — yoksa dağıtık monolit çıkar."