Heksagonal Yazılım Mimarisi (Ports and Adapters) Nedir?
Heksagonal Yazılım Mimarisi (Ports and Adapters) Nedir?
Yazılım dünyasında karşılaştığımız en büyük zorluklardan biri, zamanla büyüyen ve karmaşıklaşan projelerin bakımını yapmaktır. Başlangıçta çok hızlı geliştirilen bir proje, birkaç yıl sonra adeta bir "spagetti koda" dönüşebilir. Veritabanını değiştirmek, yeni bir ödeme entegrasyonu eklemek veya sadece bir birim (unit) testi yazmak bile kabusa dönüşebilir.
İşte tam bu noktada, yazılım dünyasının en popüler mimari yaklaşımlarından biri olan Heksagonal Yazılım Mimarisi (Hexagonal Architecture) devreye giriyor. 2005 yılında Alistair Cockburn tarafından ortaya atılan bu mimari, kodun dış dünyadan (veritabanı, arayüz, dış servisler) tamamen bağımsız olmasını hedefler.
Bu yazıda; Heksagonal mimarinin ne olduğunu, hangi durumlarda kullanılması gerektiğini, avantaj ve dezavantajlarını ve gerçek hayat senaryolarına uygun en iyi uygulama (best practice) örneklerini inceleyeceğiz.
1. Heksagonal Yazılım Mimarisi Nedir?
Heksagonal Mimari, diğer adıyla Ports and Adapters (Portlar ve Adaptörler) mimarisi, uygulamanın merkezinde yer alan iş mantığını (Domain/Business Logic), dış dünyadaki araçlardan (veritabanları, web framework'leri, API'ler, mail servisleri vb.) izole etmeyi amaçlayan bir mimari desendir.
"Heksagon" (Altıgen) terimi, geometrik olarak altı kenarı olmasından değil, uygulamanın dış dünya ile etkileşime girebileceği çok sayıda portu (giriş/çıkış noktasını) görselleştirmek için seçilmiştir.
Mimari temelde 3 ana katmandan oluşur:
+-----------------------------------+
| ADAPTERS |
| (Controller, CLI, Queue) |
+-----------------+-----------------+
|
v [Inbound Port]
+-----------------+-----------------+
| PORTS |
+-----------------+-----------------+
|
v
+-----------------+-----------------+
| DOMAIN |
| (Business Logic & Entities) |
+-----------------+-----------------+
^
|
+-----------------+-----------------+
| PORTS |
+-----------------+-----------------+
^ [Outbound Port]
|
+-----------------+-----------------+
| ADAPTERS |
| (Database, Mail, SMS API) |
+-----------------------------------+
A. Domain (Çekirdek / İş Mantığı)
Uygulamanın kalbidir. İş kuralları, varlıklar (entities) ve kullanım senaryoları (use cases) burada yaşar. En önemli kural şudur: Domain katmanı, dış dünyadaki hiçbir kütüphaneye, veritabanına veya framework'e bağımlı olmamalıdır. Tamamen saf (pure) koddan oluşmalıdır.
B. Ports (Portlar / Arayüzler)
Domain katmanının dış dünya ile iletişim kurmak için sunduğu arayüzlerdir (interfaces). İki tür port vardır: * Inbound Ports (Giriş Portları - Driving): Dış dünyanın uygulamayı tetiklemesini sağlar (Örn: Bir HTTP isteğinin uygulamayı çalıştırması için kullanılan kullanım senaryosu arayüzleri). * Outbound Ports (Çıkış Portları - Driven): Uygulamanın dış dünya ile konuşmasını sağlar (Örn: Veritabanına kaydetmek için kullanılan depo arayüzleri - repository interfaces).
C. Adapters (Adaptörler)
Portları implemente eden (gerçekleyen) somut sınıflardır. Dış dünya ile portlar arasındaki köprüdür. * Primary (Driving) Adapters: Uygulamayı tetikleyen araçlardır. Denetleyiciler (controllers), CLI komutları veya kuyruk (queue) dinleyicileri bu gruptadır. * Secondary (Driven) Adapters: Uygulamanın tetiklediği araçlardır. PostgreSQL veritabanı adaptörü, AWS S3 dosya yükleme adaptörü veya SMS gönderme adaptörü bu gruptadır.
2. Hangi Durumlarda Kullanılması Önerilir?
Heksagonal Mimari harika bir çözümdür ancak her proje için gerekli olmayabilir. Aşağıdaki durumlarda bu mimariyi kullanmanız şiddetle önerilir:
- Karmaşık İş Mantığı (Domain-Driven Design - DDD): Projenizin merkezinde yoğun ve sürekli değişen iş kuralları varsa.
- Teknoloji Değişim İhtimali: Veritabanının (Örn: PostgreSQL'den MongoDB'ye) veya harici bir servisin (Örn: Stripe ödeme altyapısından iyzico'ya) gelecekte değişme ihtimali yüksekse.
- Yüksek Test Edilebilirlik (Testability): Kodunuzu veritabanına veya gerçek API'lere ihtiyaç duymadan, hızlıca taklit ederek (mocking) birim testleri (unit tests) ile test etmek istiyorsanız.
- Uzun Ömürlü ve Büyüyen Projeler: Projenin yıllarca yaşayacağı ve sürekli yeni özellikler ekleneceği öngörülüyorsa.
3. Avantajları ve Dezavantajları Nelerdir?
Geleneksel katmanlı mimari (Layered Architecture) ile karşılaştırıldığında Heksagonal mimarinin artıları ve eksileri şunlardır:
Avantajları
- Sıfır Teknolojik Bağımlılık: Framework'ler (Spring, NestJS, .NET vb.) veya kütüphaneler değişse bile iş mantığınız bundan etkilenmez.
- Kolay Test Edilebilirlik: Gerçek bir veritabanı bağlantısı kurmadan, sadece portları taklit eden (mocking) testler yazabilirsiniz. Testler milisaniyeler içinde çalışır.
- Sürdürülebilirlik ve Esneklik: Yeni bir entegrasyon eklemek, sadece yeni bir adaptör yazmaktan ibarettir. Mevcut iş koduna dokunmanız gerekmez.
- Kodun Okunabilirliği: İş kuralları tek bir yerde (Domain) toplandığı için yeni katılan bir geliştirici sistemin nasıl çalıştığını çok daha hızlı anlar.
Dezavantajları
- Aşırı Mühendislik (Over-engineering) Riski: Basit bir CRUD (Ekle-Oku-Güncelle-Sil) uygulaması için bu mimariyi kullanmak gereksiz karmaşıklık yaratır.
- Kod Kalabalığı (Boilerplate Code): Çok fazla arayüz (interface), DTO (Data Transfer Object) ve eşleme (mapping) işlemi yapmanız gerekir. Bu da yazılan dosya sayısını artırır.
- Öğrenme Eğrisi: Ekibin mimariyi, bağımlılıkların yönünü (Dependency Inversion) ve port-adaptör ilişkisini iyi kavramış olması gerekir.
4. En İyi Uygulama (Best Practice) Örnekleri
Heksagonal mimariyi uygularken dikkat edilmesi gereken en önemli kurallardan biri Bağımlılıkların Yönü (Dependency Rule) prensibidir. Bağımlılıklar her zaman dışarıdan içeriye (Domain'e) doğru olmalıdır. Domain asla dışarıya bağımlı olmamalıdır.
Şimdi basit bir Üye Kayıt (User Registration) senaryosu üzerinden TypeScript ile en iyi uygulama (best practice) örneğini inceleyelim.
Adım 1: Domain Entity (Merkez)
Hiçbir kütüphaneye bağımlı olmayan saf iş sınıfımız.
// domain/user.entity.ts
export class User {
constructor(
public readonly id: string,
public readonly email: string,
public readonly name: string
) {
if (!email.includes('@')) {
throw new Error("Geçersiz e-posta adresi!");
}
}
}
Adım 2: Outbound Port (Çıkış Portu)
Domain'in veritabanına erişmek için kullanacağı arayüz (interface).
// domain/ports/user-repository.port.ts
import { User } from '../user.entity';
export interface UserRepositoryPort {
save(user: User): Promise<void>;
findByEmail(email: string): Promise<User | null>;
}
Adım 3: Inbound Port (Giriş Portu / Kullanım Senaryosu)
Dış dünyanın (Controller'ın) bu iş mantığını tetiklemek için kullanacağı arayüz (interface).
// domain/ports/create-user.usecase.ts
import { User } from '../user.entity';
export interface CreateUserUseCase {
execute(email: string, name: string): Promise<User>;
}
Adım 4: Domain Service (İş Mantığının Uygulanması)
İş kurallarının işletildiği ve portların kullanıldığı yer.
// domain/services/create-user.service.ts
import { CreateUserUseCase } from '../ports/create-user.usecase';
import { UserRepositoryPort } from '../ports/user-repository.port';
import { User } from '../user.entity';
export class CreateUserService implements CreateUserUseCase {
// Dependency Injection ile portu içeri alıyoruz
constructor(private readonly userRepository: UserRepositoryPort) {}
async execute(email: string, name: string): Promise<User> {
const existingUser = await this.userRepository.findByEmail(email);
if (existingUser) {
throw new Error("Bu e-posta adresi zaten kullanımda.");
}
// Basitlik açısından ID üretimi burada yapılmıştır.
const randomId = Math.random().toString(36).substring(2, 9);
const newUser = new User(randomId, email, name);
await this.userRepository.save(newUser);
return newUser;
}
}
Önemli Not: Yukarıdaki örnekte ID üretimi içinMath.random()fonksiyonu doğrudan kullanılmıştır. Ancak gerçek hayat senaryolarında (best practice olarak), ID üretme mekanizmasının da bir port (örneğinIdGeneratorPort) aracılığıyla soyutlanması önerilir. Böylece domain katmanı, rastgelelik üreten veya dış kütüphanelere bağımlı olan fonksiyonlardan da tamamen arındırılmış olur.
Adım 5: Secondary Adapter (Veritabanı Adaptörü)
Veritabanı teknolojisini (Örn: MongoDB) port ile eşleştirdiğimiz yer.
// adapters/database/mongo-user.repository.ts
import { UserRepositoryPort } from '../../domain/ports/user-repository.port';
import { User } from '../../domain/user.entity';
// Gerçek bir MongoDB modeli olduğunu varsayalım
export class MongoUserRepository implements UserRepositoryPort {
async save(user: User): Promise<void> {
console.log(`MongoDB'ye kaydedildi: ${user.name}`);
// db.users.insert({ id: user.id, email: user.email ... })
}
async findByEmail(email: string): Promise<User | null> {
console.log(`MongoDB'de arandı: ${email}`);
return null;
}
}
Adım 6: Primary Adapter (Giriş Adaptörü - Controller)
HTTP isteklerini karşılayan ve Kullanım Senaryosunu (Use Case) tetikleyen adaptör.
// adapters/web/user.controller.ts
import { CreateUserUseCase } from '../../domain/ports/create-user.usecase';
export class UserController {
constructor(private readonly createUserUseCase: CreateUserUseCase) {}
async handleRequest(req: any, res: any) {
try {
const { email, name } = req.body;
const user = await this.createUserUseCase.execute(email, name);
res.status(201).json(user);
} catch (error: any) {
res.status(400).json({ error: error.message });
}
}
}
Özet ve Sonuç
Heksagonal Yazılım Mimarisi, uygulamanızın gelecekteki değişimlere karşı esnek, kolay test edilebilir ve bakımı yapılabilir olmasını sağlar.
Eğer küçük ölçekli, sadece veritabanından veri çekip ekrana basacak bir proje geliştiriyorsanız, Heksagonal mimari sizin için gereksiz bir yük (over-engineering) olabilir. Ancak, iş kurallarının karmaşık olduğu, uzun ömürlü ve kurumsal bir proje üzerinde çalışıyorsanız, Heksagonal mimari projenizin can simidi olacaktır.
Unutmayın: En iyi mimari, projenizin ihtiyaçlarına en uygun olan mimaridir!