Portlar ve Adaptörler olarak da bilinen Hexagonal Mimari'nin uygulamalarınızın bakımını, test edilebilirliğini ve esnekliğini nasıl iyileştirebileceğini öğrenin.
Hexagonal Mimari: Portlar ve Adaptörler İçin Pratik Bir Rehber
Yazılım geliştirmenin sürekli gelişen ortamında, sağlam, bakımı yapılabilir ve test edilebilir uygulamalar oluşturmak çok önemlidir. Hexagonal Mimari, diğer adıyla Portlar ve Adaptörler, bir uygulamanın temel iş mantığını dış bağımlılıklarından ayırarak bu endişeleri ele alan bir mimari desendir. Bu rehber, Hexagonal Mimari'nin kapsamlı bir anlayışını, faydalarını ve dünya genelindeki geliştiriciler için pratik uygulama stratejilerini sunmayı amaçlamaktadır.
Hexagonal Mimari Nedir?
Alistair Cockburn tarafından ortaya atılan Hexagonal Mimari, uygulama çekirdeğinin iş mantığını dış dünyadan izole etme fikri etrafında döner. Bu izolasyon, portlar ve adaptörler kullanılarak gerçekleştirilir.
- Çekirdek (Uygulama): İş mantığını ve alan modellerini içeren uygulamanızın kalbini temsil eder. Herhangi bir özel teknolojiden veya çerçeveden bağımsız olmalıdır.
- Portlar: Çekirdek uygulamanın dış dünyayla etkileşim kurmak için kullandığı arayüzleri tanımlar. Bunlar, uygulamanın veritabanları, kullanıcı arayüzleri veya mesajlaşma kuyrukları gibi dış sistemlerle nasıl etkileşim kurduğunun soyut tanımlarıdır. Portlar iki türde olabilir:
- Yönlendiren (Birincil) Portlar: Dış aktörlerin (örn. kullanıcılar, diğer uygulamalar) çekirdek uygulama içinde eylemleri başlatabileceği arayüzleri tanımlar.
- Yönlendirilen (İkincil) Portlar: Çekirdek uygulamanın dış sistemlerle (örn. veritabanları, mesaj kuyrukları) etkileşim kurmak için kullandığı arayüzleri tanımlar.
- Adaptörler: Portlar tarafından tanımlanan arayüzleri uygular. Çekirdek uygulama ile dış sistemler arasında çevirmen görevi görürler. İki tür adaptör vardır:
- Yönlendiren (Birincil) Adaptörler: Yönlendiren portları uygular, dış istekleri çekirdek uygulamanın anlayabileceği komutlara veya sorgulara çevirir. Örnekler arasında kullanıcı arayüzü bileşenleri (örn. web denetleyicileri), komut satırı arabirimleri veya mesaj kuyruğu dinleyicileri bulunur.
- Yönlendirilen (İkincil) Adaptörler: Yönlendirilen portları uygular, çekirdek uygulamanın isteklerini dış sistemlerle özel etkileşimlere çevirir. Örnekler arasında veritabanı erişim nesneleri, mesaj kuyruğu üreticileri veya API istemcileri bulunur.
Bunu şu şekilde düşünün: çekirdek uygulama merkezde oturur, altıgen bir kabukla çevrilidir. Portlar bu kabuktaki giriş ve çıkış noktalarıdır ve adaptörler bu portlara takılarak çekirdeği dış dünyaya bağlar.
Hexagonal Mimari'nin Temel İlkeleri
Hexagonal Mimari'nin etkinliğini destekleyen birkaç temel ilke vardır:
- Bağımlılık Ters Çevirme: Çekirdek uygulama, somut uygulamalara (adaptörler) değil, soyutlamalara (portlar) bağlıdır. Bu, SOLID tasarımının temel bir ilkesidir.
- Açık Arayüzler: Portlar, çekirdek ile dış dünya arasındaki sınırları açıkça tanımlar, entegrasyon için sözleşme tabanlı bir yaklaşımı teşvik eder.
- Test Edilebilirlik: Çekirdeği dış bağımlılıklardan ayırarak, iş mantığını portların sahte uygulamalarını kullanarak izole bir şekilde test etmek daha kolay hale gelir.
- Esneklik: Adaptörler, çekirdek uygulamayı etkilemeden takılıp çıkarılabilir, bu da değişen teknolojilere veya gereksinimlere uyum sağlamayı kolaylaştırır. MySQL'den PostgreSQL'e geçmeniz gerektiğini hayal edin; yalnızca veritabanı adaptörünün değiştirilmesi gerekir.
Hexagonal Mimari Kullanmanın Faydaları
Hexagonal Mimari'yi benimsemek birçok avantaj sunar:
- Geliştirilmiş Test Edilebilirlik: Sorumlulukların ayrılması, çekirdek iş mantığı için birim testleri yazmayı önemli ölçüde kolaylaştırır. Portların sahtesini oluşturmak, çekirdeği izole etmenize ve harici sistemlere güvenmeden kapsamlı bir şekilde test etmenize olanak tanır. Örneğin, bir ödeme işleme modülü, gerçek ağ geçidine gerçek bağlantılar kurmadan başarılı ve başarısız işlemleri simüle eden ödeme ağ geçidi portunun sahtesini oluşturarak test edilebilir.
- Artan Sürdürülebilirlik: Harici sistemler veya teknolojilerdeki değişikliklerin çekirdek uygulamaya etkisi minimum düzeydedir. Adaptörler, çekirdeği harici değişkenlikten koruyan yalıtım katmanları görevi görür. SMS bildirimleri göndermek için kullanılan üçüncü taraf bir API'nin biçimini veya kimlik doğrulama yöntemini değiştirdiği bir senaryo düşünün. Yalnızca SMS adaptörünün güncellenmesi gerekir, çekirdek uygulama değişmeden kalır.
- Gelişmiş Esneklik: Adaptörler kolayca değiştirilebilir, bu da büyük yeniden düzenlemeler yapmadan yeni teknolojilere veya gereksinimlere uyum sağlamanıza olanak tanır. Bu, denemeyi ve yeniliği kolaylaştırır. Bir şirket, veri depolamasını geleneksel bir ilişkisel veritabanından bir NoSQL veritabanına taşımaya karar verebilir. Hexagonal Mimari ile yalnızca veritabanı adaptörünün değiştirilmesi gerekir, bu da çekirdek uygulamadaki kesintiyi en aza indirir.
- Azaltılmış Eşleşme: Çekirdek uygulama, daha modüler ve uyumlu bir tasarıma yol açan harici bağımlılıklardan ayrılmıştır. Bu, kod tabanının anlaşılmasını, değiştirilmesini ve genişletilmesini kolaylaştırır.
- Bağımsız Geliştirme: Farklı ekipler, paralel geliştirmeyi ve daha hızlı pazara sunma süresini teşvik ederek çekirdek uygulama ve adaptörler üzerinde bağımsız olarak çalışabilir. Örneğin, bir ekip çekirdek sipariş işleme mantığını geliştirmeye odaklanırken, başka bir ekip kullanıcı arayüzü ve veritabanı adaptörlerini oluşturabilir.
Hexagonal Mimari Uygulaması: Pratik Bir Örnek
Hexagonal Mimari'nin uygulamasını basitleştirilmiş bir kullanıcı kayıt sistemi örneğiyle açıklayalım. Netlik için varsayımsal bir programlama dili (Java veya C#'a benzer) kullanacağız.
1. Çekirdeği (Uygulamayı) Tanımlayın
Çekirdek uygulama, yeni bir kullanıcı kaydetmek için iş mantığını içerir.
// Core/UserService.java (veya UserService.cs)
public class UserService {
private final UserRepository userRepository;
private final PasswordHasher passwordHasher;
private final UserValidator userValidator;
public UserService(UserRepository userRepository, PasswordHasher passwordHasher, UserValidator userValidator) {
this.userRepository = userRepository;
this.passwordHasher = passwordHasher;
this.userValidator = userValidator;
}
public Result<User, String> registerUser(String username, String password, String email) {
// Kullanıcı girdisini doğrula
ValidationResult validationResult = userValidator.validate(username, password, email);
if (!validationResult.isValid()) {
return Result.failure(validationResult.getErrorMessage());
}
// Kullanıcının zaten var olup olmadığını kontrol edin
if (userRepository.findByUsername(username).isPresent()) {
return Result.failure("Kullanıcı adı zaten mevcut");
}
// Parolayı hash'le
String hashedPassword = passwordHasher.hash(password);
// Yeni bir kullanıcı oluştur
User user = new User(username, hashedPassword, email);
// Kullanıcıyı depoya kaydet
userRepository.save(user);
return Result.success(user);
}
}
2. Portları Tanımlayın
Çekirdek uygulamanın dış dünyayla etkileşim kurmak için kullandığı portları tanımlarız.
// Ports/UserRepository.java (veya UserRepository.cs)
public interface UserRepository {
Optional<User> findByUsername(String username);
void save(User user);
}
// Ports/PasswordHasher.java (veya PasswordHasher.cs)
public interface PasswordHasher {
String hash(String password);
}
//Ports/UserValidator.java (veya UserValidator.cs)
public interface UserValidator{
ValidationResult validate(String username, String password, String email);
}
//Ports/ValidationResult.java (veya ValidationResult.cs)
public interface ValidationResult{
boolean isValid();
String getErrorMessage();
}
3. Adaptörleri Tanımlayın
Çekirdek uygulamayı belirli teknolojilere bağlayan adaptörleri uygularız.
// Adapters/DatabaseUserRepository.java (veya DatabaseUserRepository.cs)
public class DatabaseUserRepository implements UserRepository {
private final DatabaseConnection databaseConnection;
public DatabaseUserRepository(DatabaseConnection databaseConnection) {
this.databaseConnection = databaseConnection;
}
@Override
public Optional<User> findByUsername(String username) {
// JDBC, JPA veya başka bir veritabanı erişim teknolojisi kullanarak uygulama
// ...
return Optional.empty(); // Yer tutucu
}
@Override
public void save(User user) {
// JDBC, JPA veya başka bir veritabanı erişim teknolojisi kullanarak uygulama
// ...
}
}
// Adapters/BCryptPasswordHasher.java (veya BCryptPasswordHasher.cs)
public class BCryptPasswordHasher implements PasswordHasher {
@Override
public String hash(String password) {
// BCrypt kütüphanesini kullanarak uygulama
// ...
return "hashedPassword"; //Yer tutucu
}
}
//Adapters/SimpleUserValidator.java (veya SimpleUserValidator.cs)
public class SimpleUserValidator implements UserValidator {
@Override
public ValidationResult validate(String username, String password, String email){
//Basit Doğrulama mantığı
if (username == null || username.isEmpty()) {
return new SimpleValidationResult(false, "Kullanıcı adı boş olamaz");
}
if (password == null || password.length() < 8) {
return new SimpleValidationResult(false, "Parola en az 8 karakter uzunluğunda olmalıdır");
}
if (email == null || !email.contains("@")) {
return new SimpleValidationResult(false, "Geçersiz e-posta formatı");
}
return new SimpleValidationResult(true, null);
}
}
//Adapters/SimpleValidationResult.java (veya SimpleValidationResult.cs)
public class SimpleValidationResult implements ValidationResult {
private final boolean valid;
private final String errorMessage;
public SimpleValidationResult(boolean valid, String errorMessage) {
this.valid = valid;
this.errorMessage = errorMessage;
}
@Override
public boolean isValid(){
return valid;
}
@Override
public String getErrorMessage(){
return errorMessage;
}
}
//Adapters/WebUserController.java (veya WebUserController.cs)
//Yönlendiren Adaptör - web'den gelen istekleri işler
public class WebUserController {
private final UserService userService;
public WebUserController(UserService userService) {
this.userService = userService;
}
public String registerUser(String username, String password, String email) {
Result<User, String> result = userService.registerUser(username, password, email);
if (result.isSuccess()) {
return "Kayıt başarılı!";
} else {
return "Kayıt başarısız: " + result.getFailure();
}
}
}
4. Bileşim
Her şeyi bir araya getirme. Bu bileşimin (bağımlılık enjeksiyonu) tipik olarak uygulamanın giriş noktasında veya bir bağımlılık enjeksiyonu kapsayıcısı içinde gerçekleştiğini unutmayın.
//Ana sınıf veya bağımlılık enjeksiyon yapılandırması
public class Main {
public static void main(String[] args) {
// Adaptörlerin örneklerini oluşturun
DatabaseConnection databaseConnection = new DatabaseConnection("jdbc:mydb://localhost:5432/users", "user", "password");
DatabaseUserRepository userRepository = new DatabaseUserRepository(databaseConnection);
BCryptPasswordHasher passwordHasher = new BCryptPasswordHasher();
SimpleUserValidator userValidator = new SimpleUserValidator();
// Çekirdek uygulamayı oluşturun, adaptörleri enjekte edin
UserService userService = new UserService(userRepository, passwordHasher, userValidator);
//Bir yönlendiren adaptör oluşturun ve servise bağlayın
WebUserController userController = new WebUserController(userService);
//Artık userController aracılığıyla kullanıcı kaydı isteklerini işleyebilirsiniz
String result = userController.registerUser("john.doe", "P@sswOrd123", "john.doe@example.com");
System.out.println(result);
}
}
//DatabaseConnection yalnızca gösterim amacıyla basit bir sınıftır
class DatabaseConnection {
private String url;
private String username;
private String password;
public DatabaseConnection(String url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
}
// ... veritabanına bağlanmak için yöntemler (kısa olması için uygulanmamıştır)
}
//Result sınıfı (işlevsel programlamadaki Either'a benzer)
class Result<T, E> {
private final T success;
private final E failure;
private final boolean isSuccess;
private Result(T success, E failure, boolean isSuccess) {
this.success = success;
this.failure = failure;
this.isSuccess = isSuccess;
}
public static <T, E> Result<T, E> success(T value) {
return new Result<>(value, null, true);
}
public static <T, E> Result<T, E> failure(E error) {
return new Result<>(null, error, false);
}
public boolean isSuccess() {
return isSuccess;
}
public T getSuccess() {
if (!isSuccess) {
throw new IllegalStateException("Sonuç bir hatadır");
}
return success;
}
public E getFailure() {
if (isSuccess) {
throw new IllegalStateException("Sonuç başarılıdır");
}
return failure;
}
}
class User {
private String username;
private String password;
private String email;
public User(String username, String password, String email) {
this.username = username;
this.password = password;
this.email = email;
}
// alıcılar ve ayarlayıcılar (kısa olması için atlanmıştır)
}
Açıklama:
UserService
, çekirdek iş mantığını temsil eder.UserRepository
,PasswordHasher
veUserValidator
arayüzlerine (portlara) bağlıdır.DatabaseUserRepository
,BCryptPasswordHasher
veSimpleUserValidator
, ilgili portları belirli teknolojileri (bir veritabanı, BCrypt ve temel doğrulama mantığı) kullanarak uygulayan adaptörlerdir.WebUserController
, web isteklerini işleyen veUserService
ile etkileşim kuran bir yönlendiren adaptördür.- Ana yöntem, uygulamanın bileşimini oluşturarak adaptörlerin örneklerini oluşturur ve bunları çekirdek uygulamaya enjekte eder.
Gelişmiş Hususlar ve En İyi Uygulamalar
Hexagonal Mimari'nin temel ilkeleri basit olsa da, akılda tutulması gereken bazı gelişmiş hususlar vardır:
- Doğru Granülerlik için Portları Seçme: Portlar için uygun soyutlama düzeyini belirlemek çok önemlidir. Çok ince taneli portlar gereksiz karmaşıklığa yol açabilirken, çok kaba taneli portlar esnekliği sınırlayabilir. Portlarınızı tanımlarken basitlik ve uyum arasındaki ödünleri göz önünde bulundurun.
- İşlem Yönetimi: Birden fazla harici sistemle uğraşırken işlemsel tutarlılığı sağlamak zor olabilir. Veri bütünlüğünü korumak için dağıtılmış işlem yönetimi tekniklerini kullanmayı veya telafi edici işlemleri uygulamayı düşünün. Örneğin, bir kullanıcı kaydı ayrı bir faturalandırma sisteminde bir hesap oluşturmayı içeriyorsa, her iki işlemin de birlikte başarılı olmasını veya başarısız olmasını sağlamanız gerekir.
- Hata İşleme: Harici sistemlerdeki hataları zarif bir şekilde işlemek için sağlam hata işleme mekanizmaları uygulayın. Kademeli hataları önlemek için devre kesicileri veya yeniden deneme mekanizmalarını kullanın. Bir adaptör veritabanına bağlanamazsa, uygulama hatayı zarif bir şekilde işlemeli ve potansiyel olarak bağlantıyı yeniden denemeli veya kullanıcıya bilgilendirici bir hata mesajı sağlamalıdır.
- Test Stratejileri: Uygulamanızın kalitesini sağlamak için birim testleri, entegrasyon testleri ve uçtan uca testlerin bir kombinasyonunu kullanın. Birim testleri çekirdek iş mantığına odaklanmalı, entegrasyon testleri ise çekirdek ve adaptörler arasındaki etkileşimleri doğrulamalıdır.
- Bağımlılık Enjeksiyonu Çerçeveleri: Bileşenler arasındaki bağımlılıkları yönetmek ve uygulamanın bileşimini basitleştirmek için bağımlılık enjeksiyonu çerçevelerinden (örn. Spring, Guice) yararlanın. Bu çerçeveler, bağımlılık oluşturma ve enjekte etme sürecini otomatikleştirerek, tekrarlayan kodları azaltır ve sürdürülebilirliği artırır.
- CQRS (Komut Sorgu Sorumluluk Ayrımı): Hexagonal Mimari, uygulamanızın okuma ve yazma modellerini ayırdığınız CQRS ile iyi uyum sağlar. Bu, özellikle karmaşık sistemlerde performansı ve ölçeklenebilirliği daha da iyileştirebilir.
Gerçek Dünya Hexagonal Mimari Örnekleri
Birçok başarılı şirket ve proje, sağlam ve bakımı yapılabilir sistemler oluşturmak için Hexagonal Mimari'yi benimsemiştir:
- E-ticaret Platformları: E-ticaret platformları, çekirdek sipariş işleme mantığını ödeme ağ geçitleri, nakliye sağlayıcıları ve envanter yönetimi sistemleri gibi çeşitli harici sistemlerden ayırmak için genellikle Hexagonal Mimari'yi kullanır. Bu, çekirdek işlevselliği kesintiye uğratmadan yeni ödeme yöntemlerini veya nakliye seçeneklerini kolayca entegre etmelerini sağlar.
- Finansal Uygulamalar: Bankacılık sistemleri ve ticaret platformları gibi finansal uygulamalar, Hexagonal Mimari'nin sunduğu test edilebilirliği ve bakım kolaylığından yararlanır. Çekirdek finansal mantık izole bir şekilde kapsamlı bir şekilde test edilebilir ve adaptörler, piyasa verileri sağlayıcıları ve takas odaları gibi çeşitli harici hizmetlere bağlanmak için kullanılabilir.
- Mikroservis Mimarileri: Hexagonal Mimari, her mikroservisin kendi çekirdek iş mantığı ve harici bağımlılıkları olan sınırlı bir bağlamı temsil ettiği mikroservis mimarileri için doğal bir uyumdur. Portlar ve adaptörler, mikroservisler arasındaki iletişim için net bir sözleşme sağlar, gevşek eşleşmeyi ve bağımsız dağıtımı teşvik eder.
- Eski Sistem Modernizasyonu: Hexagonal Mimari, mevcut kodu adaptörlere sararak ve portların arkasında yeni çekirdek mantığı tanıtarak eski sistemleri aşamalı olarak modernize etmek için kullanılabilir. Bu, tüm uygulamayı yeniden yazmadan eski sistemin parçalarını artımlı olarak değiştirmenize olanak tanır.
Zorluklar ve Ödünleşimler
Hexagonal Mimari önemli faydalar sunarken, ilgili zorlukları ve ödünleşimleri kabul etmek önemlidir:
- Artan Karmaşıklık: Hexagonal Mimari uygulamak, ek soyutlama katmanları getirebilir, bu da kod tabanının başlangıçtaki karmaşıklığını artırabilir.
- Öğrenme Eğrisi: Geliştiricilerin port ve adaptör kavramlarını ve bunları etkili bir şekilde nasıl uygulayacaklarını anlamaları için zamana ihtiyacı olabilir.
- Aşırı Mühendislik Potansiyeli: Gereksiz portlar ve adaptörler oluşturarak aşırı mühendislikten kaçınmak önemlidir. Basit bir tasarımla başlayın ve gerektiğinde karmaşıklığı aşamalı olarak ekleyin.
- Performans Hususları: Ek soyutlama katmanları potansiyel olarak bazı performans ek yükleri getirebilir, ancak bu çoğu uygulamada genellikle ihmal edilebilir düzeydedir.
Hexagonal Mimari'nin faydalarını ve zorluklarını projenizin özel gereksinimleri ve ekip yetenekleri bağlamında dikkatlice değerlendirmek çok önemlidir. Bu bir gümüş kurşun değildir ve her proje için en iyi seçim olmayabilir.
Sonuç
Portlar ve adaptörlere yaptığı vurguyla Hexagonal Mimari, bakımı yapılabilir, test edilebilir ve esnek uygulamalar oluşturmak için güçlü bir yaklaşım sağlar. Çekirdek iş mantığını harici bağımlılıklardan ayırarak, değişen teknolojilere ve gereksinimlere kolayca uyum sağlamanıza olanak tanır. Göz önünde bulundurulması gereken zorluklar ve ödünleşimler olsa da, Hexagonal Mimari'nin faydaları genellikle özellikle karmaşık ve uzun ömürlü uygulamalar için maliyetlerinden daha ağır basar. Bağımlılık ters çevirme ve açık arayüzler ilkelerini benimseyerek, daha dirençli, anlaşılması daha kolay ve modern yazılım ortamının taleplerini daha iyi karşılayabilecek sistemler oluşturabilirsiniz.
Bu rehber, Hexagonal Mimari'nin temel ilkelerinden pratik uygulama stratejilerine kadar kapsamlı bir genel bakış sunmuştur. Bu kavramları daha fazla araştırmanızı ve kendi projelerinizde uygulamanızı denemenizi teşvik ediyoruz. Hexagonal Mimari'yi öğrenme ve benimseme yatırımı, uzun vadede kesinlikle karşılığını verecek, daha yüksek kaliteli yazılımlara ve daha mutlu geliştirme ekiplerine yol açacaktır.
Sonuç olarak, doğru mimariyi seçmek projenizin özel ihtiyaçlarına bağlıdır. Kararınızı verirken karmaşıklık, uzun ömürlülük ve bakım gereksinimlerini göz önünde bulundurun. Hexagonal Mimari, sağlam ve uyarlanabilir uygulamalar oluşturmak için sağlam bir temel sağlar, ancak bu yalnızca bir yazılım mimarının araç kutusundaki bir araçtır.