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 | İçindekiler | Bilmediğ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ınrecordvereadonly record structdersi 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ızSiparisü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
- İskelet:
Ticaret.Domain,Ticaret.Application,Ticaret.Infrastructuresınıf kütüphaneleri ve bir konsol projesi oluştur. Referansları yalnız içeri bakacak biçimde ekle (dotnet add reference). - 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.
- Zengin varlık:
Siparissınıfınıprivate setözellikleriyle yaz;OnaylaveIptalmetotlarını geçersiz geçişleri reddedecek biçimde uygula. DışarıdanDurum'u atamayı dene — derlenmediğini gör. - Değer nesnesi:
Para(decimal Tutar, string ParaBirimi)değer nesnesinireadonly record structile yaz; farklı para birimlerinin toplanmasını istisna ile engelle. - Uygulama servisi:
SiparisiOnaylasenaryosunu yaz. Gövdesinde hiçifolmadığını doğrula; olduysa o kuralın alan modeline mi ait olduğunu sor. - Adaptör: Infrastructure'da
ISiparisDeposu'nun bellek içi uygulamasını yaz; konsol projesinde (kompozisyon kökü) hepsini bağla. - Anemik karşılaştırma: aynı senaryoyu bir de anemik modelle yaz —
kurallar bir
SiparisManagersı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."
