Modern TypeScript'te Creational Pattern'lerin Evrimi
Singleton, Factory, Builder ve Prototype pattern'lerinin TypeScript'te nasıl evrildiğini keşfet. ES modüllerinin singleton'ları ne zaman değiştirdiğini, factory function'ların ne zaman class'lardan daha iyi olduğunu ve TypeScript'in type sisteminin oyunu nasıl değiştirdiğini öğren.
Gang of Four'un design patterns kitabı 1994'te çıktı ve C++ ile Smalltalk geliştiricilerinin nesne yaratma sorunlarını hedef alıyordu. 30 yılı aşkın süre sonra TypeScript bize optional parametreler, default değerler, destructuring, ES modülleri ve gelişmiş bir type sistemi sunuyor. Creational pattern'ler kaybolmadı - evrildi.
Bu yazıda Singleton, Factory, Builder ve Prototype pattern'lerinin modern TypeScript kod tabanlarında nasıl ortaya çıktığını, ne zaman hala değer kattıklarını ve ne zaman dil özelliklerinin onları gereksiz kıldığını inceliyoruz.
Singleton Pattern: Anti-Pattern'den Context-Dependent Tool'a
Singleton pattern bir class'ın sadece bir instance'ının olmasını ve global erişimi garanti eder. 1994'te bu gerçek problemleri çözüyordu. 2025 TypeScript'te genellikle bir anti-pattern - ama her zaman değil.
Klasik Problem
Herkesin öğrendiği ders kitabı singleton'ı:
Bu çalışır ama test kabusları yaratır. Instance'ı kolayca mock'layamazsın, farklı konfigürasyonlar inject edemezsin ve her test global state'i paylaşır.
Modern Alternatif: Natural Singleton Olarak ES Modülleri
ES modülleri ilk import'tan sonra cache'lenir. Aynı modülü birden fazla kez import etmek aynı instance'ı döndürür:
Modül sistemi pattern'in karmaşıklığı olmadan singleton davranışı sağlar. Instance bir kez yaratılır, import'lar arasında paylaşılır ve modül mocking ile test edilebilir.
Dependency Injection Container'lar
Kompleks uygulamalar için dependency injection container'lar nesne lifecycle'larını yönetir:
DI container'lar dependency injection faydalarıyla singleton semantiği verir. Test kolay hale gelir - container'da implementation'ları değiştir.
Singleton Ne Zaman Hala Mantıklı
Bazı senaryolar explicit singleton pattern'den gerçekten faydalanır:
Configured transport'lar ile logger:
Feature flag manager:
Bu örnekler çalışır çünkü gerçekten uygulama çapında merkezileştirilmiş konfigürasyon gerektiren konuları temsil ederler.
Anti-Pattern Örnekleri
Inject edilmesi gereken dependency'ler için singleton kullanma:
Factory Pattern: Function'lar vs Class'lar
Factory pattern nesne yaratma mantığını encapsulate eder. TypeScript'te seçeneklerin var: factory function'lar, factory class'lar veya discriminated union'lar.
Factory Function'lar Ne Zaman Yeterli
Basit yaratma mantığı class'lara ihtiyaç duymaz:
TypeScript'in exhaustive checking'i tüm case'leri handle ettiğinden emin olur. Bir log level eklerseniz compiler eksik implementation'ları yakalar.
Factory Class'lar Ne Zaman Değer Katıyor
Factory class'lar yaratma mantığı paylaşılan konfigürasyon gerektirdiğinde mantıklı:
Bu factory ortak Lambda konfigürasyonunu (VPC, layer'lar, security group'lar) merkezileştirirken function tipi başına customization'a izin veriyor. Factory olmadan her Lambda tanımı aynı 10-15 satırı tekrarlardı.
Modern Alternatif: Discriminated Union'lar
TypeScript'in discriminated union'ları type-safe factory'ler sağlar:
Compiler her konfigürasyon branch'inin tam olarak doğru property'lere sahip olduğunu doğrular. Eksik veya yanlış option'lardan runtime hataları gelmez.
Type Guard'lar ile Factory Function'lar
Factory'leri type guard'larla runtime type checking için birleştir:
TypeScript her branch'te type'ları daraltır, branch-specific property'ler için autocomplete ve type safety verir.
Factory'leri Ne Zaman Kullanmamak Gerekir
Önemsiz nesne yaratımı için factory function yaratma:
Factory'leri conditional mantık, paylaşılan konfigürasyon veya kompleks initialization için ayır.
Builder Pattern: Fluent API'ler Ne Zaman Options Object'lerden Daha İyi
Builder pattern kompleks nesneleri adım adım oluşturur. TypeScript'te karar vermen gerekir: builder mi options object mi?
Options Object Ne Zaman Daha İyi
Az optional parametre içeren basit durumlar için options object'ler daha açık:
Bu temiz, type-safe ve self-documenting. Builder'a gerek yok.
Builder Ne Zaman Değer Katıyor
Builder pattern dependency'ler ve progressive konfigürasyon içeren kompleks nesneler için parlıyor:
Builder şunları sağlıyor:
- Progressive disclosure: Error handling sadece state'ler eklendikten sonra kullanılabilir
- Fluent API: Autocomplete ile method chaining
- Validation:
build()complete konfigürasyonu validate eder - Kompleks nesting: Parallel state'ler ve error handling temiz bir şekilde compose edilir
Generic'lerle Type-Safe Builder
Required field'ları compile time'da takip et:
Generic type parametresi hangi field'ların set edildiğini takip eder. build() methodu sadece TSet tüm required field'ları içerdiğinde çağrılabilir.
Factory Method'ları ile Builder
Pattern'leri ortak konfigürasyonlar için birleştir:
Builder'ı Ne Zaman Kullanmamak Gerekir
Basit nesneler için builder oluşturma:
Builder'ları şunlar içeren nesneler için ayır:
- 5+ optional parametre
- Kompleks validation dependency'leri
- Progressive konfigürasyon gereksinimleri
- Domain-specific language (DSL) faydaları
Prototype Pattern: Object Spread ile Değiştirildi
Prototype pattern mevcut instance'ları clone'layarak nesneler yaratır. JavaScript'in prototypal inheritance'ı bu pattern'i daha az ilgili yapıyor ve modern özellikler büyük ölçüde yerini aldı.
Eski Yaklaşım
Klasik prototype cloning:
Modern Yaklaşım: Object Spread
Object spread shallow cloning'i temiz bir şekilde hallediyor:
Deep cloning için structuredClone() kullan (Node.js 17+ ve Node 18 LTS'den beri yaygın olarak mevcut, modern browser'larda mevcut):
structuredClone() şunları handle eder:
- Nested object'ler ve array'ler
- Date, RegExp, Map, Set
- Typed array'ler
- Cyclic reference'lar
Şunları handle etmez:
- Function'lar
- DOM node'ları
- Symbol'ler
- Prototype'lar (plain object'ler oluşturur)
React State Update'leri: Immutability Pattern
React state update'leri pratik cloning'i gösteriyor:
Prototype Pattern Hala Ne Zaman İlgili
Test data builder'lar prototype benzeri cloning'den faydalanır:
Bu pattern benzer nesnelere ufak varyasyonlarla ihtiyaç duyduğun test suite'lerinde parlıyor.
Configuration Template'leri
Configuration object'leri cloning'den faydalanır:
Maliyet Analizi ve Trade-off'lar
Development Complexity
Singleton:
- ES modülleri: Düşük karmaşıklık, sıfır boilerplate
- DI container'lar: Orta karmaşıklık, framework bilgisi gerektirir
- Klasik singleton: Düşük karmaşıklık ama test overhead'i
Factory:
- Factory function'lar: Düşük karmaşıklık, basit
- Factory class'lar: Konfigürasyon paylaşıldığında orta karmaşıklık
- Discriminated union'lar: Güçlü type safety ile düşük karmaşıklık
Builder:
- Basit builder: Orta karmaşıklık, 5+ optional parametre için değer
- Type-safe builder: Yüksek karmaşıklık, public library API'leri için gerekçelendirilir
- Immutable builder: Daha yüksek memory kullanımı ama concurrent senaryolarda daha güvenli
Prototype:
- Object spread: Çok düşük karmaşıklık
structuredClone(): Çok düşük karmaşıklık, deep cloning handle eder- Custom cloning logic: Orta karmaşıklık, özel durumlar için gerekli
Runtime Performance
Singleton:
- İhmal edilebilir overhead
- Tek seferlik initialization maliyeti
- Lazy initialization her erişimde küçük check ekler
Factory:
- Minimal overhead - sadece function çağrısı
- Direct instantiation'a göre anlamlı performance farkı yok
Builder:
- Immutable builder'larda daha yüksek memory kullanımı (intermediate object'ler oluşturur)
- Mutable builder'lar minimal overhead'e sahip
- Method chaining ihmal edilebilir performance maliyetine sahip
Prototype:
- Object spread: Shallow cloning için hızlı
structuredClone(): Spread'den yavaş ama deep cloning'i doğru handle eder- Performance sadece büyük object'ler veya yüksek frekanslı cloning için önemli
Test Etkisi
Singleton:
- Klasik singleton ile büyük test zorluğu (testler arası paylaşılan state)
- Modül bazlı singleton'lar modül mock'lar ile mocklanabilir
- DI container'lar testi basit hale getirir
Factory:
- Test etmesi kolay - pure function'lar veya injectable dependency'ler
- Discriminated union'lar tüm branch'lerle test edilebilir
Builder:
- Test fixture'ları yaratmak için mükemmel
- Progressive konfigürasyon her build adımını test etmeyi sağlar
Prototype:
- Test etmesi kolay - pure data transformation
- Side effect'ler veya gizli state yok
Bundle Size
Singleton:
- ES modül yaklaşımı için minimal kod
- DI container'lar 5-10KB ekler (InversifyJS ~15.6KB gzipped)
Factory:
- Küçük - sadece function'lar veya hafif class'lar
Builder:
- Her builder methodu bundle'a eklenir
- Immutable builder'lar mutable'lardan daha büyük
- Type-safe builder'lar compile edilip kaybolur (sıfır runtime maliyeti)
Prototype:
- İhmal edilebilir - built-in language özellikleri kullanıyor
Pratik Kılavuzlar
ES Modüllerini Singleton Yerine Ne Zaman Kullanmalı:
- Basit stateful servis (logger, config manager)
- Lazy initialization gereği yok
- Node.js veya modern bundler'larda çalışıyorsun
Factory Function'ları Ne Zaman Kullanmalı:
- Basit conditional yaratma
- Paylaşılan initialization mantığı yok
- Stateless nesne yaratımı
Factory Class'ları Ne Zaman Kullanmalı:
- Kompleks paylaşılan initialization (AWS CDK construct'ları)
- Birden fazla ilişkili factory methodu
- Birçok instance'ta tekrar kullanılan konfigürasyon
Builder Pattern'i Ne Zaman Kullanmalı:
- 5+ optional parametre
- Progressive konfigürasyon gerekli
- Kompleks validation dependency'leri
- Domain-specific language (DSL) oluşturuyorsun
Builder Yerine Options Object Ne Zaman Kullanmalı:
- 5'ten az optional parametre
- Progressive konfigürasyon gerekmiyor
- Kompleks validation yok
Object Spread'i Ne Zaman Kullanmalı:
- Plain object'leri clone'luyorsun
- Shallow clone yeterli
- React state update'leri
structuredClone()'u Ne Zaman Kullanmalı:
- Deep cloning gerekli
- Function'sız nested object'ler
- Cyclic reference'ları handle ediyorsun
Yaygın Tuzaklar
Tuzak 1: Her Şey İçin Singleton
Problem: Her servisi singleton yapma çünkü "sadece bir instance'a ihtiyacımız var."
Çözüm: Dependency injection kullan. DI container lifecycle'ı kontrol etsin, class değil:
Tuzak 2: Değer Katmayan Factory Function'lar
Problem: Önemsiz nesne yaratımı için factory function yaratma.
Çözüm: Basit durumlar için constructor'ları veya object literal'leri kullan:
Tuzak 3: Basit Nesneler İçin Builder Pattern
Problem: 2-3 property'li nesneler için kompleks builder'lar.
Çözüm: Builder'ları gerçekten kompleks nesneler için ayır:
Tuzak 4: TypeScript'in Type Sistemini Görmezden Gelme
Problem: TypeScript'in özelliklerinden yararlanmadan pattern'leri implement etme.
Çözüm: Factory'ler için discriminated union'lar, builder'lar için conditional type'lar, singleton'lar için modül scope kullan:
Tuzak 5: Modül Singleton Yanlış Anlamaları
Problem: Modül singleton'larının her yerde çalıştığını varsaymak.
Ders: Modül singleton'ları şunlarla iyi çalışmaz:
- Hot module replacement (HMR) development'ta - modül reload'ları yeni instance'lar yaratır
- Server-side rendering (SSR) - her request isolated state'e sahip olmalı
- Testing - testler arası shared state sızıntıları
Çözüm: Bu senaryolar için factory'leri veya DI container'ları kullan:
Tuzak 6: Gereksiz Yere Deep Cloning
Problem: Her şey için structuredClone() veya deep clone kütüphaneleri kullanma.
Çözüm: Object spread ile shallow cloning genellikle yeterli ve çok daha hızlı:
Anahtar Çıkarımlar
-
Singleton Genellikle Bir Modül: ES modülleri doğal olarak singleton davranışı sağlıyor. Explicit Singleton pattern'i lazy initialization veya kompleks lifecycle yönetimi gerektiren durumlar için ayır.
-
Önce Factory Function'lar: Basit factory function'larla başla. Paylaşılan state veya kompleks initialization mantığı ek yapıyı gerekçelendirdiğinde factory class'lara geç.
-
Complexity İçin Builder, Convention İçin Değil: Refleks olarak builder oluşturma. Fluent API'ler discoverability'yi geliştirdiğinde veya kompleks validation progressive konfigürasyon gerektirdiğinde kullan.
-
Prototype ES6 ile Öldü: Object spread ve
structuredClone()çoğu kullanım için Prototype pattern'in yerini aldı. Prensip (instantiation yerine cloning) test data ve configuration template'leri için değerli olmaya devam ediyor. -
TypeScript Oyunu Değiştiriyor: Klasik pattern'ler sınırlı type sistemleri varsayıyordu. TypeScript'in discriminated union'ları, conditional type'ları ve type inference runtime overhead olmadan pattern faydalarını compile time'da encode ediyor.
-
Singleton Yerine Dependency Injection: Modern mimariler Singleton pattern yerine DI container'ları tercih ediyor. Bu single-instance semantiğinden taviz vermeden testability ve flexibility'yi geliştiriyor.
-
Implementation Yerine Pattern Prensipleri: Pattern'lerin çözdüğü problemler ilgili olmaya devam ediyor. 1994'teki spesifik implementation'lar olmayabilir. Altta yatan problemi anlamaya odaklan, sonra en idiomatic modern çözümü seç.
-
Context Önemli: Pattern'ler evrensel olarak iyi veya kötü değil. Singleton server request handler'larında problemli ama uygulama çapındaki logger'lar için iyi. Builder basit nesneler için gereksiz ama kompleks AWS CDK construct'ları için paha biçilmez.
Creational pattern'ler kaybolmadı - evrildi. Modern TypeScript sana 1994'te pattern'lerin ele aldığı birçok problemi çözen dil özellikleri veriyor. Pattern'lerin ne zaman değer kattığını ve ne zaman dil özelliklerinin yeterli olduğunu anla. Kod tabanın bunun için daha basit ve maintainable olacak.
Klasik Tasarım Kalıplarına Modern Bakış
Klasik Gang of Four tasarım kalıplarının modern TypeScript, React ve fonksiyonel programlama bağlamında nasıl evrildiğini inceleyen kapsamlı bir seri. Klasik kalıpların hala ne zaman geçerli olduğunu, ne zaman yerini yeni yaklaşımlara bıraktığını ve temel prensiplerin modern kod tabanlarında nasıl ortaya çıktığını öğren.