Reaktif Programlama Çağında Davranışsal Kalıplar
Observer, Strategy, Command, State ve Mediator kalıplarının TypeScript'te RxJS, Redux, XState ve modern reaktif programlama paradigmalarıyla nasıl evrildiğini keşfediyoruz.
Özet
Davranışsal (behavioral) kalıplar, nesnelerin nasıl iletişim kurduğunu ve sorumlulukların nasıl dağıtıldığını tanımlar. Gang of Four, Observer, Strategy, Command, State ve Mediator kalıplarını C++ ve Smalltalk için belgeledi - bu kalıpları uygulamanın önemli miktarda boilerplate gerektirdiği diller. Modern TypeScript ile RxJS, Redux ve React hooks bu kalıpları uygulama şeklimizi temelden değiştirdi. Bazıları framework convention'larına dönüştü, diğerleri reaktif paradigmalara evrildi, bazıları ise klasik formlarında şaşırtıcı derecede güncel kaldı.
Bu yazıda davranışsal kalıpların modern JavaScript/TypeScript uygulamalarında nasıl göründüğünü inceliyoruz. Klasik Observer'dan RxJS Observable'lara evrimi, Strategy pattern'in first-class function'larla nasıl basitleştiğini, Redux action'larının neden gizli Command pattern olduğunu, XState'in State pattern'i nasıl pratik hale getirdiğini ve Mediator pattern'in ne zaman tight coupling'i önlediğini keşfedeceğiz. Amaç: bu kalıpların ne zaman değer kattığını ve ne zaman gereksiz karmaşıklık olduğunu anlamak.
Observer Pattern: Callback'lerden Reaktif Stream'lere
Observer pattern, bir nesnedeki değişikliklerin bağımlı nesnelerde güncelleme tetiklediği one-to-many bağımlılık ilişkilerini sağlar. Modern web geliştirmede belki de en etkili pattern, her ne kadar tanımayabilsen de.
Klasik İmplementasyon
Ders kitabı Observer pattern'i açık subject-observer ilişkileri gerektirir:
Bu çalışır ama verbose ve modern uygulamaların ihtiyaç duyduğu özellikleri eksik: backpressure handling, error propagation, completion signal'ları, operator composition.
Node.js EventEmitter Evrimi
Node.js, EventEmitter ile daha esnek bir observer implementasyonu getirdi:
EventEmitter klasik Observer'ı şunlarla geliştirdi:
- Tek notification method yerine isimlendirilmiş event'ler
- Event başına birden fazla listener
- Error event desteği
- Tanıdık
.on()ve.emit()API'si
Ama hâlâ modern async senaryolar için gereken yetenekleri eksik: cancellation, debounce veya map gibi operator'ler, otomatik cleanup.
RxJS: Evrimleşmiş Observer Pattern
RxJS (Reactive Extensions for JavaScript), Observer pattern'in reaktif programlamaya tam evrimini temsil eder:
RxJS'i güçlü yapan:
- Composable operator'ler: Transformation'ları declarative olarak zincirle
- Error handling: Error'lar stream boyunca yayılır
- Completion signal'ları: Stream ne zaman bittiğini bil
- Backpressure: Hızlı producer'ları
throttlegibi operator'lerle handle et - Cancellation: Unsubscribe execution'ı durdurur
- Hot vs Cold: Execution'ın ne zaman başladığını kontrol et
Gerçek Dünya Senaryosu: WebSocket Dashboard
Gerçek zamanlı hisse senedi fiyat dashboard'u RxJS'in Observer evrimini gösterir:
Klasik Observer'a göre temel iyileştirmeler:
- Otomatik cleanup: Unsubscribe WebSocket'i kapatır
- Error handling: Bağlantı hataları retry mantığını tetikler
- Paylaşılan bağlantı: Birden fazla subscriber tek WebSocket kullanır
- Declarative filter'lar: Her observer sadece ilgili datayı işler
- Koordineli cleanup:
takeUntilmemory leak olmamasını sağlar
Redux: State için Observer Pattern
Redux, uygulama state yönetimi için Observer pattern uygular:
React-Redux component'leri observer olarak bağlar:
Note: Modern React-Redux,
connectHOC yerine hook'ları (useSelector/useDispatch) tercih eder. Aşağıdaki örnek eğitim amaçlı eski pattern'i gösterir.
Modern Alternatif: Zustand
Zustand, Observer pattern'i hook'lar ve minimal boilerplate ile basitleştirir:
Zustand'ın Observer faydaları:
- Otomatik subscription'lar: Component'ler hook'lar aracılığıyla subscribe olur
- Granular update'ler: Sadece seçilen state değiştiğinde re-render
- Boilerplate yok: Action, reducer ya da connect HOC yok
- TypeScript dostu: Tam type inference
- Middleware desteği: DevTools, persist, immer entegrasyonu
Hangi Observer Varyantını Kullanmalı
RxJS kullan:
- Karmaşık async koordinasyon (birden fazla stream birleştir)
- Gelişmiş operator'ler gerekli (debounce, throttle, retry)
- WebSocket veya SSE bağlantıları
- Event stream işleme
- Backpressure handling kritik
Redux kullan:
- Karmaşık state'li büyük uygulama
- Time-travel debugging gerekli
- Birçok component aynı state'e erişir
- Middleware ekosistemi değerli (saga'lar, thunk'lar)
- Ekip Redux pattern'lerine aşina
Zustand kullan:
- Orta ölçekli uygulama
- Özellikler yerine sadelik istiyorsun
- Redux DevTools gerekmez
- Connect HOC yerine hook'ları tercih et
- TypeScript desteği öncelik
Plain EventEmitter kullan:
- Node.js backend servisleri
- Modül içinde basit pub/sub
- Operator veya backpressure gerekmez
- Minimal dependency istiyorsun
Strategy Pattern: Class'lar Yerine Composition
Strategy pattern, algoritmaların runtime'da seçilmesini sağlar. First-class function'lara sahip dillerde, Strategy pattern genellikle class'lara ihtiyaç duymaz - function'lar daha iyi çalışır.
Klasik Strategy Pattern
Ders kitabı yaklaşımı interface'ler ve concrete implementation'lar kullanır:
Bu çalışır ama verbose. Her ödeme yöntemi için gerçekten class'lara ihtiyacımız var mı?
Strategy Olarak Function'lar
JavaScript/TypeScript'te first-class function'lar var. Strategy'ler basit function'lar olabilir:
Function strategy'lerinin faydaları:
- Daha az boilerplate (class tanımları yok)
- Daha esnek (closure'lar context yakalar)
- Test etmesi daha kolay (function mock'lamak class mock'lamaktan daha basit)
- Doğal composition (higher-order function'lar)
Closure'larla Strategy
Closure'lar private state'li strategy'leri sağlar:
React: Prop'lar Aracılığıyla Strategy
React'te strategy'ler genellikle render prop'lar veya component prop'lar olarak görünür:
Gerçek Senaryo: Form Validation
Form validation, composable strategy pattern'den faydalanır:
Bu yaklaşım sunar:
- Yeniden kullanılabilir validator'ler: Birden fazla formda kullan
- Composable: Strategy'leri kolayca birleştir
- Type-safe: TypeScript strategy imzalarını validate eder
- Test edilebilir: Validator'leri bağımsız test et
- Declarative: Konfigürasyon validation kurallarını açıklar
Strategy Pattern Ne Zaman Değer Katar
Strategy pattern kullan:
- Algoritma seçimi runtime'da olur
- Aynı interface'in birden fazla implementasyonu var
- Davranış konfigürasyona göre değişir
- Test için implementation değiştirmek gerekir
Strategy pattern kullanma:
- Sadece bir implementasyon var
- Mantık basit conditional (sadece
ifkullan) - Overhead faydayı aşar
- Function'lar kolayca abstract edilemez
Command Pattern: Redux Action'ları ve Undo/Redo
Command pattern, istekleri nesne olarak kapsüller, parameterization, queuing, logging ve undo operasyonlarını sağlar. Modern uygulamalarda Command pattern, Redux action'larında, undo/redo sistemlerinde ve task queue'lerinde görünür.
Klasik Command Pattern
Ders kitabı yaklaşımı execute() ve undo() methodlarıyla command nesneleri kullanır:
Command Olarak Redux Action'ları
Redux action'ları command nesneleridir. State değişikliklerini serializable data olarak kapsüllerler:
Redux command'ları sağlar:
- Serialization: Action'lar plain object'ler, log'lanabilir/store edilebilir
- Time-travel debugging: Action history'sini replay et
- Middleware: Command'ları intercept et ve transform et
- Undo/redo: Action history'sini store et, replay et veya tersine çevir
Redux Middleware: Command Pipeline
Middleware, reducer'lara ulaşmadan önce command'ları intercept eder:
Redux Thunk: Async Command'lar
Redux Thunk, side effect'li async command'ları sağlar:
Gerçek Senaryo: Undo/Redo'lu Rich Text Editor
Rich text editor, Command pattern'in undo/redo değerini gösterir:
Bu implementasyon sağlar:
- Detaylı undo: Her düzenleme operasyonu undo edilebilir
- Gruplu command'lar: Paste operasyonu tek action olarak undo edilir
- Command history: Tüm action'ların listesini görüntüle
- Redo desteği: Undo'daki hataları geri al
- Genişletilebilir: Yeni command type'ları kolayca ekle
Command Pattern Ne Zaman Değer Katar
Command pattern kullan:
- Undo/redo fonksiyonelliği gerekli
- Operasyonları queue'lemek veya schedule etmek gerekir
- Action'ları log'lamak/audit etmek gerekli
- Macro kaydetmek gerekli
- Transaction desteği gerekli
Command pattern kullanma:
- Undo olmadan basit CRUD operasyonları
- Action history gerekmez
- Overhead faydayı aşar
- Gerçek zamanlı collaboration (bunun yerine CRDT kullan)
State Pattern: Finite State Machine'ler
State pattern, nesnelerin internal state değiştiğinde davranışı değiştirmesini sağlar. UI geliştirmede State pattern, imkansız state kombinasyonlarını önler ve transition'ları açık hale getirir.
Problem: Boolean State Cehennemi
Karmaşık UI genellikle birden fazla boolean flag'e yol açar:
Boolean state'lerdeki problemler:
- İmkansız kombinasyonlar mümkün (
isSubmittingveisSuccessikisi de true) - Belirsiz transition'lar (validating'den submitting'e nasıl gideriz?)
- Test patlaması (tüm kombinasyonları test et)
- Davranış hakkında düşünmek zor
State Pattern Olarak Discriminated Union'lar
TypeScript discriminated union'ları imkansız state'leri önler:
Discriminated union'ların faydaları:
- İmkansız state'ler imkansız: Aynı anda
validatingvesuccessolamazsın - Type-safe: TypeScript tüm state'lerin handle edildiğini garanti eder
- Açık transition'lar: Explicit state değişiklikleri
- İlişkili data: Her state ilgili dataya sahip (sadece submitting'deyken
progress)
XState: Explicit State Machine'ler
XState declarative state machine tanımı sağlar:
State Machine'leri Görselleştirmek
XState machine'leri Mermaid diagram'larıyla görselleştirilebilir:
XState avantajları:
- Görsel tasarım: State machine'i görsel olarak tasarla, kod oluştur
- İmkansız transition'lar önlendi:
idle'dansuccess'e gidemezsin - Test oluşturma: State machine'den test'ler oluştur
- Actor model: State machine'ler spawn edebilir ve iletişim kurabilir
- History state'leri: Geri dönerken önceki state'i hatırla
Gerçek Senaryo: Multi-Step Wizard
Multi-step form'lar explicit state machine'lerden faydalanır:
State Machine'leri Ne Zaman Kullanmalı
State machine'leri kullan:
- Birçok state'li karmaşık UI workflow'ları
- İmkansız state'leri önlemek gerekli
- State transition'larının business rule'ları var
- Ekip iletişimi için görsel tasarım yararlı
- State transition'larını test etmek kritik
State machine'leri kullanma:
- 2-3 state'li basit form'lar
- Overhead faydayı aşar
- Ekip state machine'lere aşina değil
- Discriminated union'lar yeterli
Mediator Pattern: Component'leri Ayırma
Mediator pattern, nesneler arasındaki karmaşık iletişimi merkezileştirir, direkt referansları önler ve coupling'i azaltır. Modern uygulamalarda Mediator, event bus'ları, React Context ve state management library'leri olarak görünür.
Problem: Tight Coupling
Mediator olmadan component'ler birbirlerine direkt referans verir:
Mediator Olarak Event Bus
Event bus component'leri ayırır:
Mediator Olarak React Context
React Context, component ağaçları için mediator pattern sağlar:
Global Mediator Olarak Redux
Redux tüm uygulama iletişimini action'lar aracılığıyla merkezileştirir:
Mediator Pattern'i Ne Zaman Kullanmalı
Event bus kullan:
- Aynı modüldeki component'ler iletişim kurmalı
- Hafif pub/sub gerekli
- Global state gereksinimleri yok
- Hızlı prototip veya basit app
React Context kullan:
- Aynı subtree'deki component'ler iletişim kurar
- Prop drilling acı verici hale gelir
- Tema, auth veya locale paylaşımı
- Orta ölçekli component ağaçları
Redux kullan:
- Karmaşık state'li büyük uygulama
- Birçok component aynı state'e erişir
- DevTools ve time-travel gerekli
- Middleware değerli (saga'lar, thunk'lar)
- Ekip Redux ile deneyimli
Mediator'dan kaçın:
- Direkt iletişim daha basit (parent-child prop'lar)
- Sadece 2-3 component dahil
- Gereksiz indirection ekleniyor
- Debugging zorlaşır
Önemli Çıkarımlar
-
Observer reaktif programlamaya evrildi: RxJS, Redux ve React hook'ları, Observer pattern'in daha iyi composition, error handling ve backpressure yönetimiyle evrimini temsil eder.
-
Strategy pattern class'lar yerine function'lar kullanır: JavaScript'in first-class function'ları Strategy pattern'i basitleştirir - strategy class'ları oluşturmak yerine function'ları strategy olarak geçir.
-
Command Redux'u ve undo/redo'yu güçlendirir: Redux action'ları Command pattern'dir. Pattern undo/redo, action logging ve operation queuing için değerlidir.
-
State machine'ler imkansız state'leri önler: XState ve discriminated union'lar, boolean state cehennemini önlemek ve transition'ları açık hale getirmek için State pattern uygular.
-
Mediator coupling'i azaltır: Event bus'ları, React Context ve Redux, farklı ölçeklerde Mediator pattern uygular - uygulama boyutu ve iletişim karmaşıklığına göre seç.
-
Context önemli: Bu kalıplar evrensel olarak iyi değil. RxJS karmaşık async koordinasyon için değer katar ama basit event'ler için aşırı. State machine'ler karmaşık workflow'lara yardımcı olur ama basit form'ları karmaşıklaştırır. Gerçek ihtiyaçlara göre seç.
-
Modern implementasyonlar daha temiz: Bugünün davranışsal kalıpları, klasik GoF versiyonlarının C++/Smalltalk'ta gerektirdiğinden daha temiz implementasyonlar için dil özelliklerinden (first-class function'lar, closure'lar, modüller) yararlanır.
İlgili Yazılar
- Modern TypeScript'te Creational Pattern'ler - Bu serinin 1. bölümü
- Component Composition ile Structural Pattern'ler - Bu serinin 2. bölümü
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.