Software · Sep 1, 2025 · 11 dk okuma
Design Patterns Neden Var? Yazılım Tasarımında Tekrar Eden Problemler
Design Pattern, tekrar eden tasarım problemlerine verilen, bağlama göre uyarlanabilen bir çözümdür. Factory, Strategy, Observer ve diğer pattern'ların değerini, hangi değişikliği kolaylaştırdıkları belirler.

Design Pattern'lar, yazılım geliştirmede belirli bir class yapısını kullanmak ya da kodu daha profesyonel göstermek için ortaya çıkmadı. Aynı tür tasarım problemleri farklı projelerde tekrarlandığı için varlar. Bir nesnenin nasıl oluşturulacağı, değişen davranışların nasıl ayrılacağı, bağımlılıkların nasıl yönetileceği veya bir değişikliğin sistemin geri kalanına nasıl aktarılacağı gibi problemler, uygulamanın alanı değişse bile benzer yapılar üretir. Design Pattern, bu problemlere karşı geliştirilmiş, belirli koşullarda işe yarayan ve farklı sistemlere uyarlanabilen bir çözüm düzenidir.
Bu ayrım önemlidir çünkü pattern kullanmak ile iyi tasarım yapmak aynı şey değildir. Factory eklemek, Strategy interface'i oluşturmak veya Observer kullanmak tek başına kod kalitesini artırmaz. Bir pattern'ın değeri, sistemin hangi değişikliğe daha az maliyetle cevap verebildiğinde ortaya çıkar. Yanlış yerde kullanılan pattern ise gereksiz abstraction, dolaylı çağrılar ve anlaşılması güç bağımlılıklar oluşturabilir. Bu yüzden asıl mesele hangi pattern'ları bildiğimiz değil, karşılaştığımız problemin gerçekten o pattern'ı gerektirip gerektirmediğidir.
Pattern isimleri değil, değişim problemleri kalıcıdır
Bir sipariş sisteminde ödeme işlemini düşünelim. Başlangıçta yalnızca kredi kartı destekleniyorsa PaymentService içinde doğrudan CreditCardPayment oluşturmak makul görünebilir. Daha sonra banka transferi, dijital cüzdan veya farklı ödeme sağlayıcıları eklendiğinde aynı class giderek daha fazla koşul taşımaya başlar. Her yeni ödeme yöntemi, mevcut karar kodunun değiştirilmesini gerektirir.
Buradaki problem kredi kartı değildir. Problem, ödeme yöntemini seçme sorumluluğu ile ödeme işlemini yürütme sorumluluğunun aynı yerde birleşmesidir. İki farklı değişim nedeni tek class içinde toplandığında, birinin değişmesi diğerini de etkileyebilir.
Design Pattern'ların temel amacı bu tür tekrar eden yapısal problemleri tanımlamak ve sorumlulukların nasıl dağıtılabileceğini göstermektir. Pattern bir kod parçası değildir. Bir problemi, o problemin ortaya çıktığı koşulları, çözümdeki katılımcıları ve çözümün getirdiği sonuçları birlikte ele alır.
Örneğin Strategy, birbirinin yerine geçebilen davranışların ortak bir contract üzerinden kullanılmasını önerir. Factory, nesne oluşturma kararını o nesneyi kullanan koddan ayırabilir. Observer ise bir nesnedeki değişikliğin, ona bağımlı birçok bileşene doğrudan referans verilmeden duyurulmasını sağlar.
Bu çözümlerin hiçbiri ücretsiz değildir. Strategy yeni interface'ler ve implementation'lar getirebilir. Factory nesne oluşturma akışını dolaylı hale getirebilir. Observer ise olayların hangi sırayla işlendiğini takip etmeyi zorlaştırabilir.
Dolayısıyla pattern'ın çözmeye çalıştığı problem kadar, sisteme getirdiği maliyet de tasarım kararının parçasıdır.
Pattern'ların ikinci önemli işlevi ortak bir tasarım dili oluşturmalarıdır. Bir code review sırasında 'bu davranışı Strategy olarak ayıralım' demek, belirli bir sorumluluk dağılımını hızlıca anlatabilir. Ancak bu dil yalnızca ekip aynı mekanizmayı anlıyorsa işe yarar. Pattern adını kullanmak, bağımlılık yönünü veya değişimin gerçekten nerede gerçekleşeceğini açıklamanın yerine geçmez.
Üç kategori, üç farklı tasarım sorusu
Design Pattern'lar geleneksel olarak Creational, Structural ve Behavioral olmak üzere üç kategoriye ayrılır. Bu ayrım bir kalite sıralaması değildir. Pattern'ın öncelikle hangi tasarım sorunuyla ilgilendiğini gösterir.
Creational pattern'lar nesnelerin nasıl oluşturulduğuna odaklanır. Kodun belirli bir concrete class'a doğrudan bağımlı olması sorun yaratıyorsa oluşturma sürecini ayrı bir sorumluluk olarak düzenlemek gerekebilir.
Factory bu kategorinin en tanınan örneklerindendir. Bir PaymentFactory, ödeme yöntemine göre uygun PaymentProcessor implementation'ını döndürebilir. Böylece ödeme işlemini başlatan kod, her sağlayıcının constructor ayrıntılarını bilmek zorunda kalmaz.
Factory adının birkaç farklı yapıyı ifade edebildiğini bilmek gerekir. Factory Method, nesne oluşturma kararının bir method üzerinden alt class'lara bırakılabildiği belirli bir pattern'dır. Abstract Factory ise ilişkili nesne ailelerinin birlikte oluşturulmasına yöneliktir. Bir factory function veya basit factory class da pratikte faydalı olabilir; ancak bunların tamamı aynı mekanizma değildir.
Structural pattern'lar nesnelerin ve class'ların nasıl bir araya getirileceğini düzenler. Buradaki mesele çoğunlukla farklı interface'leri uyumlu hale getirmek veya karmaşık bir yapıya daha kullanışlı bir erişim sağlamaktır.
Adapter, bir interface'i başka bir interface'e dönüştürür. Örneğin ödeme uygulaması charge(amount) contract'ını kullanırken harici sağlayıcının SDK'sı createTransaction(request) bekliyorsa Adapter bu iki taraf arasındaki dönüşümü üstlenebilir. Uygulamanın geri kalanı sağlayıcının request modelini öğrenmek zorunda kalmaz.
Decorator, mevcut bir nesnenin davranışını aynı interface'i koruyarak genişletir. Bir PaymentProcessor etrafına logging veya ölçümleme davranışı ekleyen wrapper buna örnektir. Decorator'ın önemli özelliği, ek davranışın nesnenin yerine kullanılabilirliğini bozmamasıdır.
Facade ise karmaşık bir alt sisteme daha sade bir giriş noktası sunar. CheckoutFacade, sipariş doğrulama, ödeme ve sipariş durumunu güncelleme gibi işlemlerin koordinasyonunu dışarıya tek bir operasyon olarak açabilir. Facade karmaşıklığı ortadan kaldırmaz; onu belirli bir sınırın arkasında toplar.
Behavioral pattern'lar nesneler arasındaki iletişimi ve davranışların dağıtımını düzenler.
Strategy, aynı işi farklı algoritmalarla gerçekleştiren implementation'ların değiştirilebilir olmasını sağlar. Ödeme yöntemi seçimi, fiyatlandırma politikası veya gönderim ücreti hesaplaması buna uygun olabilir. Buradaki temel ayrım, hangi algoritmanın çalışacağı ile algoritmanın nasıl çalıştığının birbirinden ayrılmasıdır.
Observer, bir kaynağın durum değişikliklerini ona abone olmuş bileşenlere bildirmesini sağlar. Sipariş ödendiğinde bildirim hazırlamak veya analitik olay üretmek bu modele uyabilir. Ancak Observer tek başına güvenilir mesaj teslimatı sağlamaz. Process kapandığında kaybolmaması gereken olaylar için kalıcı mesajlaşma veya transactional outbox gibi ek mekanizmalar gerekebilir.
Mediator ise birbirleriyle doğrudan konuşan çok sayıda bileşenin etkileşimini merkezi bir koordinasyon nesnesine taşır. Özellikle arayüz bileşenlerinin birbirlerinin durumunu sürekli değiştirdiği yapılarda bağımlılıkları azaltabilir. Buna karşılık bütün kararları bilen devasa bir Mediator, başka bir karmaşıklık merkezi oluşturur.
Bu üç kategori birbirini dışlamaz. Gerçek bir sistemde Factory nesneleri oluşturabilir, Adapter dış servisi uyumlu hale getirebilir ve Strategy çalışma sırasında davranışı seçebilir. Kategoriler birlikte kullanım yasağı değil, problem türlerini ayıran bir sınıflandırmadır.
Popüler açıklamanın kaçırdığı mekanizma: bağımlılık yönü
Design Pattern'lar sıklıkla 'tekrar kullanılabilir kod çözümleri' olarak anlatılır. Bu açıklama kısmen doğrudur ama kritik mekanizmayı eksik bırakır. Pattern'ın esas katkısı çoğu zaman kod tekrarını azaltmak değil, değişen bir kararın sistemin hangi bölümlerini değiştirmek zorunda bırakacağını kontrol etmektir.
Bunu anlamak için iki farklı tekrar türünü ayırmak gerekir. Aynı hesaplamanın beş yerde yazılması implementation tekrarına örnektir. Beş farklı bileşenin aynı concrete class hakkında bilgi sahibi olması ise bağımlılıkların yayılmasıdır. İlk problem ortak bir function ile çözülebilir. İkincisi çoğu zaman sorumlulukların ve bağımlılık yönünün yeniden düzenlenmesini gerektirir.
Dependency, bir bileşenin başka bir bileşene ihtiyaç duymasıdır. Dependency inversion ise yüksek seviyeli iş kurallarının düşük seviyeli teknik ayrıntılara doğrudan bağlanmak yerine abstraction'lara dayanmasını amaçlar. Her Design Pattern bu ilkenin doğrudan uygulaması değildir; fakat birçok pattern'ın sağladığı değişim esnekliği, bağımlılıkların belirli sınırlarda tutulmasına dayanır.
Örneğin CheckoutService, doğrudan StripeClient oluşturuyorsa ödeme sağlayıcısına ilişkin teknik karar checkout akışına taşınmıştır. Bunun yerine PaymentGateway contract'ına bağımlıysa, sağlayıcıya özgü kod ayrı bir implementation içinde kalabilir. Adapter bu implementation'ın dış SDK ile konuşmasını sağlayabilir; Factory veya dependency injection ise uygun implementation'ı uygulama kurulurken seçebilir.
Burada üç farklı sorumluluk vardır: contract'ın ne vaat ettiği, implementation'ın bunu nasıl yaptığı ve implementation'ın nerede seçildiği. Bu sorumluluklar birbirinden ayrılabildiğinde değişiklik daha yerel kalır.
Fakat abstraction oluşturmak otomatik olarak bağımsızlık sağlamaz. PaymentGateway interface'i belirli bir sağlayıcının request ve response tiplerini kullanıyorsa teknik bağımlılık yalnızca farklı bir dosyaya taşınmıştır. Interface'in adı genel olsa bile contract sağlayıcıya özgüdür.
Bu nedenle iyi bir abstraction, implementation ayrıntılarını tekrar etmek yerine uygulamanın gerçekten ihtiyaç duyduğu davranışı ifade etmelidir. Ödeme sisteminin ihtiyacı createProviderSpecificIntent değil, iş modeline uygun biçimde bir ödeme girişimini başlatmak olabilir.
Sıklıkla önerilen 'her class için interface oluşturun' yaklaşımı da aynı nedenle sorunludur. Tek implementation'ı bulunan ve değişim sınırı oluşturmayan bir interface yalnızca ek dosya üretir. Bir interface'in değeri, sayısından değil, bağımlılıkları hangi kararlı contract etrafında topladığından gelir.
Benim tercih ettiğim tasarım ölçütü şudur: Bir pattern eklemeden önce, beklenen değişikliğin hangi dosyalarda gerçekleşeceğini söyleyebilmeliyiz. Bu soruya cevap verilemiyorsa pattern seçmek için henüz yeterli gerekçe yoktur.
Checkout akışı üzerinde gerçek bir tasarım kararı
Bir backend geliştiricisinin checkout akışını düzenlediğini düşünelim. Repository içinde CheckoutService.ts, PaymentGateway.ts, CardPayment.ts ve OrderRepository.ts bulunuyor. İlk sürümde ödeme yalnızca kartla yapılıyor. Şimdi banka transferi eklenmesi gerekiyor; ayrıca ödeme tamamlandıktan sonra e-posta gönderilecek ve muhasebe kaydı hazırlanacak.
İlk tasarımda CheckoutService, ödeme türüne göre if blokları kullanıyor, ilgili SDK'yı çağırıyor, sipariş durumunu değiştiriyor ve ardından e-posta gönderiyor olabilir. Böyle bir dosyada üç ayrı değişim nedeni birleşmiştir: ödeme yöntemleri, sipariş yaşam döngüsü ve ödeme sonrasındaki işlemler.
İlk karar, ödeme yöntemlerinin gerçekten farklı davranışlar içerip içermediğidir. Kart ödemesi anında sonuçlanırken banka transferi daha sonra doğrulanıyorsa bu fark yalnızca farklı bir API çağrısı değildir. Siparişin durum modeli de değişir. pending, paid ve failed gibi durumların hangi olaylarla değiştiği açıkça tanımlanmalıdır.
Bunun ardından bir PaymentProcessor contract'ı oluşturulabilir. Kart ve banka transferi implementation'ları, ortak iş sonucunu döndürür. Strategy bu noktada uygundur çünkü checkout işlemi hangi ödeme yönteminin seçildiğini bilir, fakat yöntemin teknik ayrıntılarını yürütmek zorunda değildir.
Ödeme türünden uygun implementation'a geçiş için basit bir Factory kullanılabilir. PaymentProcessorFactory, kayıtlı processor'lar arasından seçim yapar. Eğer uygulamanın dependency injection container'ı zaten bu seçimi merkezi olarak yapıyorsa ikinci bir Factory class oluşturmak gereksiz olabilir.
Kart sağlayıcısının SDK'sı uygulamanın contract'ına uymuyorsa CardGatewayAdapter kullanılabilir. Adapter tutar formatını, request alanlarını ve hata tiplerini dönüştürür. Özellikle para tutarlarında birim dönüşümü açık olmalıdır: uygulama minor unit kullanırken SDK decimal tutar bekliyorsa dönüşüm tek yerde yapılmalı ve test edilmelidir.
Ödeme işlemlerinin süresini ölçmek için MetricsPaymentProcessor adlı bir Decorator eklenebilir. Bu wrapper aynı PaymentProcessor contract'ını uygular, çağrıyı gerçek processor'a iletir ve süresini kaydeder. Ancak başarısız bir ödemeyi başarılı gösteremez veya domain sonucunu sessizce değiştiremez. Böyle yaparsa artık yalnızca teknik davranış eklemiyor, iş kuralını değiştiriyordur.
Checkout akışının dışarıya tek operasyon sunması gerekiyorsa CheckoutFacade uygun olabilir. HTTP controller bu Facade'ı çağırır; Facade doğrulama, ödeme başlatma ve sipariş güncelleme adımlarını koordine eder. Fakat mevcut CheckoutService zaten bu görevi anlaşılır biçimde yerine getiriyorsa aynı işi yapan yeni bir Facade oluşturmak tasarımı iyileştirmez.
Ödeme tamamlandığında e-posta ve muhasebe işlemlerini başlatmak için Observer düşünülebilir. Aynı process içinde çalışan basit event subscriber'lar küçük bir uygulamada yeterli olabilir. Fakat ödeme kaydının transaction'ı tamamlandıktan hemen sonra process kapanırsa, yalnızca memory içindeki event'e güvenmek bildirim kaybına yol açabilir.
Bu noktada transactional outbox daha uygun bir seçenek olabilir. Ödeme durumuyla birlikte gönderilecek event, aynı database transaction'ı içinde outbox tablosuna yazılır. Ayrı bir worker bu kaydı okuyarak mesajı yayınlar. Böylece sipariş durumunun kalıcı hale gelmesi ile yayınlanması gereken olayın kaydedilmesi aynı transaction sınırına girer.
Yine de mesajın birden fazla kez teslim edilmesi mümkündür. Bu nedenle muhasebe tarafı aynı payment ID için işlemi tekrar çalıştırdığında çift kayıt oluşturmamalıdır. Buna idempotency denir: aynı isteğin tekrar uygulanması, tanımlanan iş sonucu açısından ek etki üretmez.
Mediator ise bu checkout akışında zorunlu değildir. Ödeme, sipariş ve bildirim bileşenleri birbirleriyle karmaşık biçimde doğrudan konuşuyorsa koordinasyonu merkezileştirmek yararlı olabilir. Ancak yalnızca üç method çağrısını düzenlemek için Mediator eklemek, zaten anlaşılır olan akışı gereksiz yere dolaylı hale getirebilir.
Bu tasarımın doğruluğu pattern sayısıyla ölçülmez. Daha anlamlı test, yeni bir ödeme sağlayıcısı eklendiğinde CheckoutService içindeki iş kurallarının değişmek zorunda kalıp kalmadığıdır. Aynı şekilde e-posta servisi değiştiğinde ödeme implementation'larının etkilenmemesi beklenir.
Pattern, maliyetini karşıladığı yerde kullanılmalı
Design Pattern kullanmanın önemli faydaları vardır. Sorumlulukları daha açık sınırlara ayırabilir, implementation değişikliklerini belirli bölgelerde tutabilir, testlerde bağımlılıkların değiştirilmesini kolaylaştırabilir ve ekip içinde ortak bir tasarım dili sağlayabilir. Özellikle uzun ömürlü sistemlerde, değişimin sık gerçekleştiği sınırların doğru kurulması bakım maliyetini azaltabilir.
Ancak bunların hiçbiri her pattern kullanımında otomatik gerçekleşmez. Pattern, çözmeye çalıştığı problemden daha fazla karmaşıklık üretiyorsa anti-pattern'a dönüşebilir. Anti-pattern, ilk bakışta makul görünen fakat tekrarlandığında sistem üzerinde olumsuz sonuçlar doğuran tasarım yaklaşımıdır.
Örneğin yalnızca tek bir ödeme yöntemi bulunan ve ikinci yöntemin geleceğine dair somut bir gereksinimi olmayan uygulamada AbstractPaymentFactory, PaymentStrategyResolver, PaymentMediator ve birkaç Decorator oluşturmak muhtemelen gereksizdir. Kod artık daha fazla dosyaya dağılmıştır, ancak çözülen gerçek bir değişim problemi yoktur.
Bunun karşısındaki hata ise bütün davranışları tek class'a yığmak ve ancak sistem büyüdüğünde tasarıma bakmaktır. Her iki uç da maliyetlidir. Doğru karar, bugünkü karmaşıklık ile makul biçimde öngörülebilen değişim arasında verilir.
Pattern seçmeden önce uygulanabilecek somut bir kontrol vardır: Değişmesi beklenen kararı belirleyin, bu kararın şu anda kaç bileşene yayıldığını bulun ve pattern sonrasında hangi bağımlılıkların ortadan kalkacağını açıklayın. Ardından yeni interface'lerin, wrapper'ların ve dolaylı çağrıların maliyetini değerlendirin.
Burada faydalı bir eşik şudur: Pattern kaldırıldığında sistemin davranışı aynı kalıyor ve beklenen değişikliklerin maliyeti de anlamlı biçimde artmıyorsa pattern'ın varlığı sorgulanmalıdır. Bu matematiksel bir kural değildir; tasarımın gerçek ihtiyaca hizmet edip etmediğini anlamaya yarayan bir değerlendirmedir.
Basit bir koşul bloğu bazen Strategy'den daha iyi olabilir. Tek SDK çağrısı yapan küçük bir wrapper için Adapter gerekli olmayabilir. İki nesne arasındaki doğrudan method çağrısı, Observer ile kurulmuş dolaylı bir iletişimden daha anlaşılır olabilir.
Buna karşılık sistemin birçok bölümü aynı değişken davranışa bağımlı hale geldiyse veya dış servis ayrıntıları domain koduna yayılmaya başladıysa pattern kullanmamak daha pahalı hale gelebilir. O noktada abstraction, varsayımsal bir gelecek için değil, mevcut bir değişim baskısına cevap verir.
Benim önerim, pattern'ları başlangıç şablonu olarak değil, tasarım kararı olarak kullanmak. Önce çalışan ve anlaşılır bir çözüm kurun; değişim sınırları belirginleştiğinde Factory, Strategy, Adapter veya başka bir pattern ile sorumlulukları yeniden düzenleyin. Eğer gereksinim baştan açıkça birden fazla implementation, bağımsız değişen algoritmalar veya zorunlu entegrasyon sınırları içeriyorsa pattern'ı ilk tasarımdan itibaren kullanmak da doğrudur.
Bu yaklaşımın sınırı, değişiklik maliyetinin geç fark edildiğinde çok yüksek olduğu sistemlerdir. Public API contract'ları, uzun ömürlü veri formatları veya değiştirilmesi zor entegrasyon protokolleri söz konusuysa abstraction sınırlarını daha erken kurmak gerekir. Yine de kararın gerekçesi pattern'ın popülerliği değildir. Karar, hangi bağımlılığın korunacağı ve hangi değişikliğin güvenli yapılacağı üzerinden verilmelidir.
İyi yazılım tasarımının hedefi mümkün olduğunca çok pattern kullanmak değildir. Hedef, değişmesi gereken kodun değişebilmesi ve değişmemesi gereken kodun bu süreçte korunmasıdır.
yorumlar
0Henü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.