Çoğu mühendis clean architecture resmini görmüştür: ortaya doğru oklarla işaret eden dört renkli halka. Gerçek bir özellik için her halkaya ne gireceğini, import'ların hangi yöne bakması gerektiğini ya da SQL'in nerede durduğunu söyleyebilenlerin sayısı çok daha az.
Bu boşluk code review'da ortaya çıkar: ORM'i import eden bir domain klasörü, HTTP durum kodu döndüren bir use case, bir form ile bir tablo arasındaki beş mapper.
Bu yazı Robert C. Martin'in dairelerini tek bir use case, yani sipariş verme için bir komponent diyagramına dönüştürüyor: katmanlar, bağımlılık kuralı, kod ve klasörler, sınır geçişleri, clean ile hexagonal ve onion karşılaştırması, hatalar, ne zaman gereksiz olduğu ve nasıl çizileceği.
Dört katman
Martin clean architecture'ı 2012'de The Clean Architecture blog yazısında anlattı ve 2017'de Clean Architecture kitabında genişletti. Bunu, hexagonal ve onion architecture dahil daha önceki fikirlerin bir birleşimi olarak sundu. Merkezden dışa doğru:
| Katman | İçerdiği | Örnekte | Ne zaman değişir |
|---|---|---|---|
| Entities | Birden çok uygulamada yeniden kullanılabilen kurumsal iş kuralları | Order ve bir siparişin en az bir satır gerektirmesi kuralı |
Bir iş kuralı değiştiğinde |
| Use cases | Uygulama iş kuralları, her biri tek bir iş akışı | PlaceOrder, iki arayüzü, request ve response modelleri |
İş akışı değiştiğinde |
| Interface adapters | Controller'lar, presenter'lar, gateway'ler ve tüm SQL | OrderController, OrderPresenter, PostgresOrderRepository |
API ya da şema değiştiğinde |
| Frameworks and drivers | Araçlar ve yapıştırıcı kod: web framework'ü, veritabanı, sürücüler | HTTP framework'ü, PostgreSQL, pg, main.ts |
Bir aracı değiştirdiğinizde |
Daireler şematiktir; Martin dörtten fazlasına ihtiyaç duyabileceğinizi söylüyor. Onları birbirine bağlayan kural ise hiç değişmez.
Bağımlılık kuralı
Martin'in sözleriyle, «kaynak kod bağımlılıkları yalnızca içeri doğru işaret edebilir.» İç dairedeki kod, dış dairede tanımlanmış bir fonksiyonu, sınıfı ya da değişkeni anmaz, veri biçimlerini de kullanmaz. Bu örnek için:
entitiesdiğer katmanlardan hiçbir şey import etmez.use-casesyalnızcaentitiesimport eder: web framework'ü, ORM ya da sürücü yok.adaptersiçeri doğru import eder, bir de uyarladıkları kütüphaneyi.- Her şeyi yalnızca
main.tsgörür.
Kural çalışma zamanı çağrılarını değil kaynak kodu kapsar: çalışma zamanında PlaceOrder dışarıya, presenter'a ve veritabanına çağrı yapar. Martin bunu dependency inversion principle ile çözer: use case kendisinin sahip olduğu bir arayüzü çağırır, bir dış sınıf onu uygular. Çağrı dışarı gider; bağımlılık içeri bakar.
Bir bağımlılık gerçekten dışarı bakar: adapter veritabanı sürücüsünü import eder. Bu bilinçli bir tercih. Martin tüm SQL'i adapter katmanına koyar ve onun içindeki hiçbir kodun «veritabanı hakkında hiçbir şey bilmemesi» gerekir.
Kuralı CI'da zorunlu kılın: JVM'de ArchUnit içindeki onionArchitecture() kontrolüyle, TypeScript'te dependency-cruiser ile, Python'da import-linter'ın layers contract'ıyla.
Uygulamalı örnek: sipariş verme
Bir POST /orders isteği şu yolu izler:
- Web framework'ü onu
OrderController'a yönlendirir. - Controller gövdeyi, use case katmanının sahip olduğu düz bir nesne olan
PlaceOrderRequest'e ayrıştırır, birOrderPresenteroluşturur vePlaceOrder.execute()çağırır. PlaceOrder, iş kurallarınıOrder.place()'e uygulatır,OrderRepositoryüzerinden kaydeder ve birPlaceOrderResponse'uPlaceOrderOutput'a verir.OrderPresenterbu arayüzü uygular ve bir view model oluşturur: 201 durumu, birLocationbaşlığı ve bir JSON gövdesi.- Controller view model'i döndürür, framework de onu yazar.
Sahip olduğu iki arayüzle birlikte use case katmanı:
// src/orders/use-cases/place-order.ts
import { Order } from '../entities/order';
export interface OrderRepository { // implemented by an adapter
nextId(): Promise<string>;
save(order: Order): Promise<void>;
}
export interface PlaceOrderOutput { // implemented by the presenter
present(response: PlaceOrderResponse): void;
}
export type PlaceOrderRequest = {
customerId: string;
lines: { sku: string; quantity: number }[];
};
export type PlaceOrderResponse = { orderId: string; itemCount: number };
export class PlaceOrder {
constructor(private readonly orders: OrderRepository) {}
async execute(request: PlaceOrderRequest, output: PlaceOrderOutput): Promise<void> {
const order = Order.place(await this.orders.nextId(), request.customerId, request.lines);
await this.orders.save(order);
output.present({ orderId: order.id, itemCount: order.itemCount() });
}
}
Yalnızca entity'yi import eder. Order.place(), siparişte satır yoksa ya da miktar birin altındaysa hata fırlatır ve entity dosyası hiçbir şey import etmez. Adapter içeri doğru import eder, bir de sürücüyü:
// src/orders/adapters/postgres/postgres-order-repository.ts
import { randomUUID } from 'node:crypto';
import type { Pool } from 'pg'; // adapters may import the driver
import type { Order } from '../../entities/order';
import type { OrderRepository } from '../../use-cases/place-order';
export class PostgresOrderRepository implements OrderRepository {
constructor(private readonly pool: Pool) {}
async nextId(): Promise<string> { return randomUUID(); }
async save(order: Order): Promise<void> {
await this.pool.query('INSERT INTO orders (id, customer_id, lines) VALUES ($1, $2, $3)',
[order.id, order.customerId, JSON.stringify(order.lines)]);
}
}
Tüm somut sınıfları yalnızca composition root bilir:
// src/main.ts
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const controller = new OrderController(new PlaceOrder(new PostgresOrderRepository(pool)));
app.post('/orders', async (req, res) => {
const view = await controller.post(req.body); // the presenter's view model
res.status(view.status).set(view.headers).json(view.body);
});
PlaceOrder için bir unit test yalnızca bir in-memory repository ve girdisini kaydeden bir presenter ister: sunucu yok, veritabanı yok.
Klasör yapısı
src/
orders/
entities/
order.ts Order and its rules
use-cases/
place-order.ts PlaceOrder, both interfaces, request and response models
adapters/
http/
order-controller.ts
order-presenter.ts
postgres/
postgres-order-repository.ts
main.ts composition root: builds and wires everything
Önce özelliğe, sonra katmana göre gruplayın. Martin'in screaming architecture savı: üst düzey klasörler sistemin hangi framework'ü kullandığını değil ne yaptığını (orders, payments) söylemeli. Simon Brown'ın aynı kitaptaki bölümü organizasyonu kapsüllemeden ayırır: klasörler kodu düzenler, erişim kuralları ya da bir build kontrolü ise onu zorunlu kılar.
Sınırları geçmek
Her sınır geçişi, bir katmanın sahip olduğu bir tür taşır:
| Geçiş | Ne taşınır | Türün sahibi | Mekanizma |
|---|---|---|---|
| Framework → controller | HTTP isteği | Framework | main.ts içindeki yapıştırıcı kod |
| Controller → use case | PlaceOrderRequest |
Use cases | Doğrudan çağrı |
| Use case → presenter | PlaceOrderResponse |
Use cases | PlaceOrderOutput arayüzü |
| Use case → veritabanı | Order |
Entities | OrderRepository arayüzü |
| Presenter → framework | View model | Adapters | Controller tarafından döndürülür |
Üç alışkanlık bu geçişleri temiz tutar:
- Düz request ve response modelleri. Martin net konuşuyor: «Entity'leri ya da veritabanı satırlarını geçirerek hile yapmak istemeyiz.» Presenter
PlaceOrderResponsealır, aslaOrderalmaz; bu yüzden bir entity alanını yeniden adlandırmak API'ye dokunmaz. Repository'ler yaygın bir istisnadır: birçok kod tabanı onların entity kabul etmesine izin verir ve bu yine içeri bakar. Daha katı ekipler adapter içinde bir persistence kaydına eşler. - Dışa yapılan her çağrıda bir arayüz; iç katmana ait olur ve teknolojinin değil use case'in ihtiyacının adını taşır.
- Tek bir composition root. Mark Seemann composition root'u, giriş noktasına yakın «modüllerin bir araya getirildiği (tercihen) tekil bir konum» olarak tanımlar. Martin
main'e «nihai ayrıntı» der. Geri kalan her şey bağımlılıkları constructor'lar üzerinden alır.
Martin'in şemasında interactor'ın önünde bir Use Case Input Port arayüzü de var. Tek controller ve tek uygulama olduğunda sınıfı doğrudan çağırmak yaygın bir sadeleştirmedir.
Clean, hexagonal ve onion architecture karşılaştırması
| Hexagonal (ports and adapters) | Onion | Clean | |
|---|---|---|---|
| Tanımlayan metin | Alistair Cockburn, 2005 makalesi | Jeffrey Palermo, 2008 blog serisi | Robert C. Martin, 2012 blog yazısı, 2017 kitabı |
| Resim | Altıgenin içinde uygulama, dışında adapter'lar | Domain modelinin çevresinde halkalar | Dört eşmerkezli daire |
| İç yapı | Belirtilmemiş | Domain model, domain service'leri, application service'leri | Entities, use cases |
| Repository uygulaması | İkincil (driven) adapter | Infrastructure, dış halka | Interface adapters |
| Temel kural | İçerideki kod dışarı sızmamalı; port'lar üzerinden konuşur | Tüm bağlaşım merkeze doğru işaret eder | Kaynak kod bağımlılıkları içeri bakar |
Üçü de iş kurallarını ortaya koyar ve kenarların onlara bağımlı olmasını sağlar. Farkları, içeride ne kadar yapı dayattıklarında: Cockburn'ün özgün makalesi yalnızca içeriyi dışarıdan ayırır, Palermo'nun onion architecture'ı domain modelinin çevresindeki halkaları adlandırır, clean architecture ise presenter'ları ve request ile response modellerini ekler. Hexagonal architecture diyagramı benzer bir sipariş örneğini ports-and-adapters terimleriyle anlatıyor.
Sık yapılan hatalar
- Entity'lerde framework türleri.
Orderüzerindeki ORM decorator'ları ya da serileştirme öznitelikleri en içteki katmanı en dıştakine bağlar. Eşlemeyi adapter'da yapın. - Her şey için bir mapper. Tek satır kaydeden bir form için aynı alanların altı kopyası. Yalnızca şekiller farklı olduğunda eşleyin.
- Davranışsız katmanlar. Getter ve setter'dan ibaret entity'ler, Martin Fowler'ın 2003'te anlattığı anemic domain model ve yalnızca
save()çağıran use case'ler. - Her yerde arayüz. Arayüzler sınırlara aittir, her sınıfa değil.
- Use case içinde HTTP. Durum kodları presenter'a aittir.
Ne zaman gereksizdir
Palermo onion architecture'ın «küçük web siteleri için uygun olmadığını» yazdı; Martin'in kısmi sınırlara ayrılan bölümü ise «Tam teşekküllü mimari sınırlar pahalıdır» cümlesiyle açılır.
| Durum | Daha uygun çözüm |
|---|---|
| Az kuralı olan CRUD servisi | İnce katmanlı bir uygulama; az sayıdaki kuralı controller'ların dışında tutun |
| Prototip ya da kısa ömürlü araç | Katman yok |
| Ödeme sağlayıcısı gibi tek değişken bağımlılık | Bunun için bir arayüz ve bir adapter |
| Gerçek kuralları ve birden çok giriş noktası olan uzun ömürlü ürün | Dört katmanın hepsi |
Bu bölüm daha ucuz kısmi sınırları da anlatır; ikinci bir uygulama ya da bir test gerektirdiğinde birini yükseltin.
Clean architecture diyagramı
Diyagram içeriği
- Frameworks and drivers
- Interface adapters
- Use cases
- Entities
- Web framework
- PostgreSQL
- OrderPresenter
- OrderController
- PostgresOrderRepository
- PlaceOrderOutput
- PlaceOrder
- OrderRepository
- Order
- POST /orders
- execute(request)
- Order.place()
- reads view model
- present(response)
- save(order)
- implements
- SQL via pg driver
Nasıl okunur:
- Dört UML paketi, en dıştaki solda. Sağa gitmek içeri gitmek demektir.
- Düz oklar çağrılar ve import'lardır. Üçü, entity'lere doğru bir sınırı geçer. Diğer üçü tek bir katmanın içinde kalır:
present(response)vesave(order), use case katmanının sahip olduğu iki arayüzü çağırır. - İçi boş üçgenli kesikli oklar uygular anlamına gelir. Kontrol dışarı akarken presenter ve repository bu arayüzleri içeri doğru işaret eder.
- Noktalı çizgi sürücü üzerinden giden SQL'dir ve bir adapter'da başlar.
- Notlar ve Markdown bloğu
main.ts'in nerede durduğunu, entity'nin neyi zorunlu kıldığını ve her çizgi stilinin ne anlama geldiğini söyler.
Clean architecture diyagramlarını net çizmek
- Halkaları değil, tek bir use case'i çizin. Gerçek bir özelliği gerçek sınıf adlarıyla izleyin.
- Her arayüzü sahibi olan katmana koyun.
OrderRepository, uygulamasının yanında değilPlaceOrder'ın yanında durur. - İki ok stili kullanın: çağrılar için düz, uygular için içi boş üçgenli kesikli; UML realization'daki gibi. UML sınıf diyagramı örneği gösterimi anlatıyor.
- İçeriye sayfada tek bir yön verin. Geriye bakan bir ok ya bir ihlaldir ya da etiketlenecek bir çalışma zamanı çağrısı.
- Oklara yöntemi ve sınırı geçen modeli yazın.
- Dışarıda bırakın: DTO alanlarını, framework iç yapısını ve DI container'ı.
Bu kod düzeyindeki görünümün çevresindeki sistem için C4 model diyagramı örneğine ve yazılım mimarisi diyagramı çizme rehberine bakın.
Archboard'da oluşturun
- Google veya GitHub ile giriş yapın ve bir diyagram oluşturun.
- UML pack'inden yan yana dört Paket node'u ekleyin: Frameworks and drivers, Interface adapters, Use cases ve Entities; mavi, yeşil, kırmızı ve kehribar vurgularla.
- Frameworks and drivers içine UML pack'inden Web framework adlı bir Bileşen ve Veri pack'inden PostgreSQL adlı bir Postgres node'u ekleyin.
- Interface adapters içine OrderPresenter ve OrderController adlı iki Bileşen node'u ve PostgresOrderRepository adlı bir Sınıf ekleyin.
- Use cases içine, PlaceOrderOutput ve OrderRepository adlı iki Arayüz node'unun arasına PlaceOrder adlı bir Sınıf ekleyin.
- Entities içine alanları ve yöntemleriyle Order adlı bir Sınıf ekleyin.
- Çağrılar için etiketli altı düz bağlantı, uygular için üçgen ok uçlu iki kesikli bağlantı ve PostgresOrderRepository'den PostgreSQL'e noktalı bir bağlantı çizin.
- Temel pack'inden kural ve çizgi stillerini içeren bir Markdown bloğu ve her katman için Temel pack'inden Metin notları ekleyin.
Katmanlarınızın bir Mermaid akış şeması, Mermaid'den düzenlenebilir diyagrama aracıyla ilk taslağa dönüşür. Free planda diyagram başına 15 diagram node'a izin verilir; bu yüzden orada her diyagramda tek bir use case izleyin.
Örnek 14 diagram node kullanıyor ve Free planın sınırları içinde kalıyor.
SSS
Clean architecture'ın dört katmanı nedir? Entities (kurumsal iş kuralları), use cases (uygulama iş kuralları), interface adapters (controller'lar, presenter'lar ve gateway'ler) ve frameworks and drivers (web framework'ü, veritabanı ve diğer araçlar). Martin daireleri şematik olarak niteler: dörtten fazlasına ihtiyacınız olabilir.
Bağımlılık kuralı nedir? Kaynak kod bağımlılıkları yalnızca içeri doğru işaret eder. İç katmandaki kod, dış katmanda tanımlanmış hiçbir şeyi anmaz; bu yüzden use case'ler controller'lardan, SQL'den ya da web framework'ünden habersizdir.
Clean architecture ile hexagonal architecture arasındaki fark nedir? İkisi de iş kurallarını ortada tutar ve adapter'ları onlara bağımlı kılar. Hexagonal architecture içeriyi dışarıdan yalnızca port'lar aracılığıyla ayırır. Clean architecture ayrıca içeriyi entities ve use cases olarak böler, presenter'ları ve request ile response modellerini adlandırır.
Clean architecture'da repository arayüzü nereye konur? Use case katmanına, ona ihtiyaç duyan use case'in yanına. SQL'li uygulama interface adapters katmanına aittir ve composition root onu başlangıçta içeri geçirir.
Clean architecture küçük projeler için gereksiz midir? Çoğu zaman. CRUD servisleri, prototipler ve kısa ömürlü araçlar için fazladan arayüzler ve eşlemeler getirdiğinden fazlasına mal olur. Değişmesini beklediğiniz bağımlılıklar için arayüz ekleyin, katmanları gerçek iş kuralları ortaya çıktığında büyütün.