sazak@labs:~
oturum

etiketler

abstractioncohesionsoftware architecturemaintainabilitysoftware designcoupling

Software · Jun 3, 2025 · 10 dk okuma

Yazılım Mimarisi Nedir? İyi Bir Mimariyi Gerçekten İyi Yapan Şeyler

İyi yazılım mimarisi, karmaşık yapılar kurmak değil, önemli kararları doğru yerde vermektir. Architecture ve design farkından coupling, cohesion ve abstraction'a kadar mimari kalitesini belirleyen mekanizmalar.

Yazılım Mimarisi Nedir? İyi Bir Mimariyi Gerçekten İyi Yapan Şeyler

Yazılım mimarisi, bir sistemin temel yapısını belirleyen ve sonradan değiştirilmesi maliyetli olabilecek teknik kararların bütünüdür. Sistemin hangi parçalardan oluşacağı, bu parçaların hangi sorumlulukları taşıyacağı, birbirleriyle nasıl iletişim kuracağı ve hangi bağımlılıkların kabul edileceği mimarinin konusudur. İyi mimariyi belirleyen şey kullanılan pattern sayısı veya sistemin ne kadar katmanlı olduğu değildir. Asıl ölçüt, sistemin bugünkü gereksinimleri karşılayabilmesi ve makul ölçüde beklenen değişiklikleri kontrol edilebilir maliyetle taşıyabilmesidir.

Bu ayrım önemlidir çünkü mimari kararların bedeli çoğu zaman karar verildiği gün ortaya çıkmaz. Başlangıçta küçük görünen bir bağımlılık, altı ay sonra basit bir iş kuralı değişikliğinin beş farklı servise dokunmasını gerektirebilir. Tersine, gelecekte ihtiyaç duyulacağı varsayılan bir esneklik için kurulan karmaşık yapı, ekibin her gün daha fazla kod yazmasına ve daha fazla hata ayıklamasına neden olabilir. Yazılım mimarisinin gerçek problemi, değişimin nereden geleceğini kesin olarak bilmeden bugünkü karmaşıklığı yönetmektir.

Architecture ve design aynı karar düzeyinde değildir

Architecture ile design arasındaki farkı anlamak için kararların etkisine bakmak gerekir. Architecture, sistemin temel sınırlarını ve parçalar arasındaki ilişkileri belirler. Design ise bu sınırların içinde davranışların nasıl gerçekleştirileceğini şekillendirir. İki alan birbirinden tamamen bağımsız değildir; aralarındaki sınır, kararın kapsamına ve değiştirilme maliyetine göre değişir.

Bir e-ticaret uygulamasında sipariş yönetiminin ödeme sisteminden ayrı bir sorumluluk olarak ele alınması architectural bir karardır. Sipariş oluşturma işleminin hangi class veya function üzerinden gerçekleştirileceği çoğunlukla design kararıdır. Ancak belirli bir class'ın interface'i onlarca modülün doğrudan bağımlılığı haline gelirse, başlangıçta yerel görünen bu design kararı architectural önem kazanır.

Burada belirleyici olan dosyanın adı veya kullanılan pattern değildir. Bir kararın ne kadar geniş bir alanı etkilediği, kaç farklı parçanın ona bağımlı olduğu ve değiştirildiğinde ne kadar koordinasyon gerektirdiği önemlidir.

Örneğin monolith ile microservices arasında seçim yapmak genellikle architectural bir karardır. Çünkü deployment biçimini, veri tutarlılığını, hata yönetimini, operasyon maliyetini ve ekiplerin çalışma düzenini etkiler. Buna karşılık bir class içinde strategy pattern kullanmak çoğu durumda design düzeyinde kalır.

Yine de monolith seçimi tek başına iyi veya kötü mimari anlamına gelmez. Modülleri iyi ayrılmış bir monolith, bağımlılıkları kontrol edilmemiş bir microservices sisteminden daha kolay değiştirilebilir olabilir. Mimari kaliteyi deployment birimlerinin sayısından çıkarmak bu nedenle yanıltıcıdır.

Pratikte architecture ile design arasında sert bir çizgi çizmek yerine şu soruya bakmak daha yararlıdır: Bu kararı değiştirmek istediğimizde yalnızca ilgili kodu mu düzenleyeceğiz, yoksa sistemin başka parçalarını ve çalışma biçimini de değiştirmek zorunda mı kalacağız?

Gereksinimler mimarinin sınırlarını belirler

Mimari seçimler teknik tercihlerden önce gereksinimlere dayanmalıdır. Aynı işlevleri sunan iki uygulamanın farklı mimarilere ihtiyaç duyması mümkündür. Çünkü sistemden beklenen yalnızca ne yaptığı değil, bunu hangi koşullar altında yaptığıdır.

Functional requirements, sistemin gerçekleştirmesi gereken davranışları tanımlar. Sipariş oluşturmak, ödeme almak, kullanıcı yetkisini kontrol etmek veya rapor üretmek bu gruba girer. Non-functional requirements ise performans, availability, security, scalability ve maintainability gibi sistemin davranışına ilişkin kalite beklentilerini ifade eder.

Bu ikinci grup architectural kararları özellikle güçlü biçimde etkiler. Örneğin bir API için p95 latency hedefinin 200 ms olması, isteklerin %95'inin 200 ms içinde tamamlanmasını gerektirir. Bu hedefin hangi yük altında, hangi endpoint için ve hangi sistem sınırında ölçüleceği de tanımlanmalıdır. Aksi halde 200 ms sayısı mimari karar vermeye yetmez.

Benzer şekilde saniyede birkaç sipariş işleyen bir iç uygulama ile yoğun trafik altında çalışan bir satış sistemi aynı decomposition stratejisini gerektirmeyebilir. İlkinde tek bir database ve modüler monolith yeterli olabilirken, ikincisinde belirli işlemleri asynchronous yürütmek gerekebilir. Ancak trafik miktarı tek başına microservices kullanımını zorunlu kılmaz.

Gereksinimler arasında çatışmalar da bulunur. Güçlü consistency isteyen bir işlem ile düşük latency hedefi aynı anda kolayca karşılanamayabilir. Her servisin bağımsız deployment yapabilmesi istenirken sistem genelinde tek transaction beklentisi korunmak istenebilir. Bu gerilimler mimarinin gerçek karar alanıdır.

Bu yüzden iyi bir mimari çalışması, gereksinimleri yalnızca listelemekle kalmaz. Hangilerinin architectural açıdan belirleyici olduğunu, hangilerinin ölçülebilir kabul kriterlerine sahip olduğunu ve hangilerinin diğer gereksinimlerle çatıştığını ortaya çıkarır.

Burada önemli bir ayrım vardır: Kesin gereksinim ile geleceğe ilişkin tahmin aynı şey değildir. Bugün iki farklı ödeme sağlayıcısının desteklenmesi gerekiyorsa bu somut bir gereksinimdir. Belki beş yıl sonra on farklı sağlayıcı kullanılabileceği düşüncesi ise bir varsayımdır. İkisine aynı mühendislik yatırımını yapmak çoğu zaman doğru değildir.

Coupling ve cohesion değişiklik maliyetini açıklar

Yazılım mimarisi hakkında en yaygın açıklamalardan biri, iyi mimarinin düşük coupling ve yüksek cohesion sağlaması gerektiğidir. Bu ifade genel olarak doğrudur fakat tek başına yeterince açıklayıcı değildir. Asıl mesele, bağımlılıkların değişiklikleri sistem içinde nasıl yaydığıdır.

Coupling, yazılım parçalarının birbirlerinin ayrıntılarına ne ölçüde bağımlı olduğunu ifade eder. Bir modül başka bir modülün internal data structure'ını, database schema'sını veya implementation ayrıntılarını biliyorsa aralarında güçlü bir coupling bulunabilir. Bu ayrıntılardan biri değiştiğinde diğer modülün de değiştirilmesi gerekebilir.

Cohesion ise bir modül içindeki sorumlulukların birbirleriyle ne kadar ilişkili olduğunu anlatır. Sipariş fiyatlandırmasıyla ilgili kuralları birlikte barındıran bir modül yüksek cohesion gösterebilir. Aynı modülün e-posta gönderimi, kullanıcı profil düzenleme ve log temizleme işlerini de üstlenmesi cohesion'ı zayıflatır.

Burada kaçırılan mekanizma değişikliğin yayılmasıdır. Mimari sınırlar doğru çizilmediğinde tek bir iş gereksinimi, aslında birbirinden bağımsız olması gereken birçok parçanın aynı anda değiştirilmesine yol açar. Böylece kod değişikliğinin maliyeti yalnızca düzenlenen satırların sayısıyla sınırlı kalmaz; test, review, deployment ve ekipler arası koordinasyon maliyetleri de artar.

Örneğin ödeme sağlayıcısının API response formatı değiştiğinde sipariş fiyatlandırma kodu da değişmek zorunda kalıyorsa bağımlılık yanlış yerde olabilir. Sipariş modülünün bilmesi gereken şey, ödemenin hangi durumda olduğu ve bu durumun sipariş yaşam döngüsünü nasıl etkilediğidir. Sağlayıcının kullandığı alan adları veya authentication ayrıntıları sipariş kurallarının parçası olmamalıdır.

Bu yaklaşım separation of concerns ilkesine dayanır. Farklı nedenlerle değişen sorumluluklar, mümkün olduğunca farklı sınırlar içinde tutulur. Buradaki amaç her sorumluluk için ayrı servis oluşturmak değildir. Amaç, bir değişikliğin ilgisiz kodu değiştirmesini önlemektir.

Abstraction da aynı probleme başka bir noktadan müdahale eder. Abstraction, bir bileşenin kullanım açısından gerekli davranışını görünür bırakırken implementation ayrıntılarını gizleyen sınırdır. Örneğin ödeme sağlayıcısına özgü HTTP çağrılarını doğrudan sipariş akışına yerleştirmek yerine, ödeme işleminin uygulama açısından anlamlı sonucunu sunan bir interface tanımlanabilir.

Fakat her interface yararlı bir abstraction değildir. Tek implementation'ı olan, herhangi bir değişkenliği gizlemeyen ve yalnızca çağrıları başka bir class'a aktaran interface çoğu zaman ek karmaşıklık üretir. Abstraction'ın değeri, gizlediği ayrıntının gerçekten değişebilmesi ve bu değişimin diğer kodlara yayılmasını engellemesidir.

Coupling'i sıfırlamak da gerçekçi bir hedef değildir. Çalışan parçalar arasında mutlaka bağımlılıklar bulunur. Önemli olan bağımlılığın yönü, türü ve değişiklik üzerindeki etkisidir. Özellikle sık değişen teknik ayrıntıların daha kararlı iş kurallarını değiştirmeye zorlamaması güçlü bir architectural ilkedir.

Bir ödeme entegrasyonu mimari sınırları görünür kılar

Somut bir örnek düşünelim. Bir backend geliştiricisi, sipariş alan bir uygulamaya ikinci bir ödeme sağlayıcısı eklemekle görevlendiriliyor. Mevcut sistemde sipariş oluşturma endpoint'i, ödeme sağlayıcısının SDK'sını doğrudan çağırıyor. Sağlayıcıdan dönen response içindeki status alanına göre siparişin durumu güncelleniyor.

İlk sağlayıcıyla çalışan bu yapı başlangıçta basit görünür. Ancak ikinci sağlayıcının farklı authentication yöntemi, hata kodları ve ödeme durumu modeli varsa sipariş oluşturma kodu sağlayıcıya özgü koşullarla dolmaya başlar. Sorun ikinci sağlayıcının eklenmesi değildir. Sorun, ödeme entegrasyonunun değişken ayrıntılarının sipariş davranışının içine yerleşmiş olmasıdır.

Burada önerilecek ilk adım yeni bir microservice oluşturmak değildir. Önce mevcut kodda ödeme sağlayıcısına ait sorumluluklarla sipariş kurallarını ayırmak gerekir. Sipariş tarafı ödeme talebinin sonucunu uygulamanın kendi kavramlarıyla değerlendirmelidir. Sağlayıcıya özgü HTTP request, authentication, timeout ve response mapping işlemleri adapter içinde kalabilir.

Örneğin uygulama tarafında PaymentGateway adlı bir interface düşünülebilir. charge işlemi tutar, para birimi, sipariş referansı ve idempotency key alır; sonuç olarak uygulamanın anlayacağı bir ödeme durumu döndürür. Her sağlayıcı için ayrı adapter bu sözleşmeyi uygular.

Buradaki kritik ayrıntı interface'in varlığı değil, sözleşmenin doğru tasarlanmasıdır. Ödeme işlemi her zaman anında başarılı veya başarısız sonuçlanmaz. Bazı işlemler beklemede kalabilir, dış doğrulama gerektirebilir veya daha sonra webhook ile tamamlanabilir. Bu gerçekliği yalnızca true veya false döndüren bir abstraction arkasına saklamak, yanlış bir model üretir.

Ayrıca ödeme gibi dış sistemlerle çalışan işlemlerde timeout, başarısızlık anlamına gelmez. İstek karşı tarafa ulaşmış ve ödeme alınmış olabilir; yalnızca response uygulamaya dönmemiştir. Bu nedenle ödeme denemelerinin kalıcı bir kimlikle takip edilmesi ve tekrar denemelerin idempotent davranması gerekir. Idempotency, aynı mantıksal işlemin tekrarlanmasının ikinci bir bağımsız ödeme oluşturmasını engelleyen davranıştır.

Sipariş kaydı ile dış ödeme işlemini tek bir database transaction içine koymak da sorunu otomatik çözmez. Yerel transaction, uzak ödeme sağlayıcısının işlemini geri alamaz. Bu nedenle sipariş durumları, ödeme denemeleri ve sonradan gelen callback veya webhook olayları açıkça modellenmelidir.

Bu örnekte iyi mimari, birçok pattern'ın bir araya getirilmesi değildir. Siparişin iş kurallarıyla ödeme sağlayıcısının değişken teknik ayrıntıları arasında doğru sınırın kurulmasıdır.

Aynı örnek önemli bir karşı durumu da gösterir. Uygulama yalnızca tek bir sağlayıcı kullanacaksa, entegrasyon çok küçükse ve değişiklik olasılığı düşükse kapsamlı bir plugin sistemi kurmak gereksiz olabilir. Küçük bir adapter bile yeterli olabilir; bazen daha fazla abstraction eklemeden açık bir modül sınırı korumak en iyi seçenektir.

İyi mimari değişimi ucuzlatır, her değişimi değil

İyi mimari için sık kullanılan tanım, değişime dayanıklı mimari olmasıdır. Bu yaklaşımı doğru buluyorum, ancak önemli bir koşulla: Mimari her türlü değişime eşit derecede dayanıklı olmak zorunda değildir. Böyle bir hedef hem gerçekçi değildir hem de aşırı mühendisliğe davetiye çıkarır.

Bir sistemin hangi yönde değişeceği tamamen bilinemez. Bugün değişmeyeceği düşünülen bir iş kuralı yarın yeniden yazılabilir. Gelecekte kritik hale geleceği varsayılan bir entegrasyon ise hiçbir zaman kullanılmayabilir. Dolayısıyla mimari, olası bütün senaryolar için önceden hazırlanmış esneklik katmanları oluşturmak yerine, değişikliklerin etkisini sınırlayabilecek anlaşılır sınırlar kurmalıdır.

Aşırı mühendislik genellikle iyi niyetli bir varsayımla başlar: Gelecekte değişiklik olabilir, o halde bugünden bir abstraction daha ekleyelim. Zamanla interface'ler, factory'ler, generic repository'ler, event bus'lar ve configuration mekanizmaları birikir. Sistem henüz bu esnekliğe ihtiyaç duymadığı halde geliştiriciler basit bir davranışı anlamak için birçok dosya arasında dolaşmak zorunda kalır.

Basit mimari ise katmansız veya kuralsız mimari demek değildir. Basitlik, mevcut gereksinimleri karşılamak için gereken en az kavramsal yükle çalışabilmektir. Bazen bu tek bir uygulama içinde açık modül sınırları kurmak anlamına gelir. Bazen de farklı ölçeklenme veya güvenilirlik gereksinimleri nedeniyle belirli bir bileşeni ayrı deploy etmek gerekir.

Burada yararlı bir karar ilkesi vardır: Geri alınması pahalı kararları erken görünür hale getir, ucuz kararları ise ihtiyaç ortaya çıkana kadar ertele. Database teknolojisini değiştirmek, servisler arasındaki transaction modelini yeniden kurmak veya dış API sözleşmesini kırmak yüksek maliyetli olabilir. Yerel bir class'ın implementation'ını refactor etmek ise iyi sınırlar içinde tutulmuşsa daha ucuzdur.

Bu ayrım architectural decision record, yani ADR kullanımıyla somutlaştırılabilir. Kısa bir ADR; kararın bağlamını, değerlendirilen seçenekleri, seçilen yaklaşımı ve kabul edilen sonuçları kaydeder. Amaç belge üretmek değil, ileride bir karar yeniden değerlendirildiğinde hangi varsayımlarla verildiğini görebilmektir.

Mimari kalitenin ölçülmesinde de aynı yaklaşım kullanılmalıdır. Class sayısı, servis sayısı veya kullanılan design pattern miktarı doğrudan kalite göstergesi değildir. Bunun yerine belirli değişikliklerin kaç modülü etkilediğine, bağımsız testlerin ne kadar kolay yazıldığına, deployment sırasında ne kadar koordinasyon gerektiğine ve gereksinimlerin ölçülen kalite hedeflerini karşılayıp karşılamadığına bakılabilir.

Değişiklik maliyetini değerlendirirken yalnızca kod düzenleme süresini ölçmek eksik kalır. Bir değişiklik tek satırdan oluşsa bile üç ekibin onayını, birden fazla deployment'ı ve geniş kapsamlı regression test gerektiriyorsa architectural maliyeti yüksek olabilir. Buna karşılık yüzlerce satırlık ama tek modül içinde kalan ve bağımsız test edilebilen bir değişiklik operasyonel açıdan daha güvenli olabilir.

Benim tercih edeceğim başlangıç noktası, açık sınırları olan modüler bir yapıdır. Gereksinimler bağımsız deployment, ayrı ölçeklenme, farklı güvenilirlik sınırları veya ekipler arasında gerçek operasyonel bağımsızlık gerektirmedikçe microservices ile başlamam. Bunun nedeni monolith'in her zaman üstün olması değil, dağıtık sistemlerin network failure, observability, veri tutarlılığı ve deployment koordinasyonu gibi maliyetleri en baştan getirmesidir.

Bu önerinin geçerliliği de sınırlıdır. Bir bileşenin diğerlerinden bağımsız ölçeklenmesi zorunluysa, farklı security sınırlarına ihtiyaç varsa veya ayrı ekiplerin birbirini beklemeden release yapabilmesi ölçülebilir bir gereksinimse servis ayrımı daha doğru olabilir. Benzer şekilde mevcut monolith'in modül sınırları sürekli ihlal ediliyorsa, yalnızca aynı deployment içinde kalmak basitliği garanti etmez.

Son karar için şu ölçütü kullanırım: Bir architectural sınır, gerçek bir değişiklik nedenini izole ediyor veya açık bir kalite gereksinimini karşılıyorsa değer üretir. Yalnızca gelecekte işe yarayabileceği düşüncesiyle eklenmişse maliyetinin kanıtlanması gerekir. İyi mimari, değişimi tamamen ortadan kaldıran yapı değildir; hangi değişikliklerin nerede duracağını bilinçli biçimde belirleyen yapıdır.

yorumlar

0

Henüz yorum yok. İlk notu sen bırak.

Yorum yazmak için giriş yap.

Gmail ile saniyeler içinde, ya da e-posta ile kaydol.

bağlı 26109:33:01