TypeScript'te Davranışsal Tasarım Kalıpları: Observer, Strategy, Command, State
Observer, Strategy, Command, State ve Mediator TypeScript'te: hangisini dilin kendisi karşılıyor, RxJS, Redux ve XState ne zaman yerini hak ediyor.
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. O dillerde her kalıp, işe yarar bir şey yapmadan önce bir sayfa boilerplate’e mal oluyordu. TypeScript bu hesabı değiştirdi: first-class function’lar, closure’lar ve discriminated union’lar bu kalıpların bir kısmını tek başına karşılıyor, kalanının çoğunu da RxJS, Redux ve XState framework convention’larına dönüştürdü.
Buradan çıkan varsayılan basit: kalıbı önce dilin kendi özellikleriyle kur, kütüphaneye ancak adı konmuş bir ihtiyaç zorladığında geç. Strategy için bir function, State için bir discriminated union, Observer için bir subscribe listesi yeter. Her yükseltmenin kendi tetikleyicisi var: operator composition, cancellation ve backpressure RxJS’i haklı çıkarır; serialize edilebilir, replay edilebilir action history Redux’ı haklı çıkarır; review edenlerin şema olarak okuması gereken guard’lı transition’lar XState’i haklı çıkarır, bu tetikleyiciler yoksa kütüphane dilin zaten verdiği bir şey için sadece ek maliyet demektir.
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ştirmenin muhtemelen en etkili kalıbı, ama adıyla pek karşımıza çıkmaz.
Subject-Observer Boilerplate’i#
Observer’ın orijinal hâli açık subject-observer ilişkileri gerektirir:
interface Observer {
update(data: any): void;
}
class Subject {
private observers: Observer[] = [];
subscribe(observer: Observer): void {
this.observers.push(observer);
}
unsubscribe(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
notify(data: any): void {
this.observers.forEach(o => o.update(data));
}
}
// Kullanım
class ConcreteObserver implements Observer {
update(data: any): void {
console.log('Güncelleme alındı:', data);
}
}
const subject = new Subject();
const observer = new ConcreteObserver();
subject.subscribe(observer);
subject.notify({ value: 42 });
Subject-observer çifti temel durumu karşılar, ama backpressure handling, error propagation, completion signal’ları ve operator composition tamamen eksiktir.
Node.js EventEmitter Evrimi#
Node.js, EventEmitter ile daha esnek bir observer implementasyonu getirdi:
import { EventEmitter } from 'events';
class DataStream extends EventEmitter {
start(): void {
setInterval(() => {
this.emit('data', { timestamp: Date.now() });
}, 1000);
}
}
const stream = new DataStream();
stream.on('data', (data) => {
console.log('Alındı:', data);
});
stream.on('error', (error) => {
console.error('Hata:', error);
});
stream.start();
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, Observer’ı Daha da İleri Götürür#
RxJS (Reactive Extensions for JavaScript), Observer pattern’in reaktif programlamaya tam evrimini temsil eder:
import { Observable, Subject, of } from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
map,
filter,
catchError
} from 'rxjs';
// Reaktif operator'lerle search input
const searchInput$ = new Subject<string>();
searchInput$.pipe(
debounceTime(300), // Yazmayı durdurduktan 300ms sonra bekle
distinctUntilChanged(), // Sadece değer değiştiyse
map(term => term.toLowerCase()),
filter(term => term.length >= 3), // Min 3 karakter
catchError(error => {
console.error('Arama hatası:', error);
return of([]); // Observable döndürmeli
})
).subscribe(term => {
console.log('Aranıyor:', term);
// Arama sonuçlarını getir
});
// Değer emit et
searchInput$.next('rea');
searchInput$.next('react');
searchInput$.next('reactive');
Aşağıdakilerin hiçbiri EventEmitter’da yok:
- 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
WebSocket Üzerinden Fiyat Akışı#
Canlı bir hisse fiyat akışı bu operator’lerin hepsini aynı anda çalıştırır:
import { Observable, merge, interval, Subject } from 'rxjs';
import {
filter,
map,
scan,
share,
retry,
takeUntil
} from 'rxjs';
interface StockPrice {
symbol: string;
value: number;
timestamp: number;
}
// Observable olarak WebSocket
function createStockStream(url: string): Observable<StockPrice> {
return new Observable<StockPrice>(subscriber => {
const ws = new WebSocket(url);
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
subscriber.next(data);
} catch (error) {
subscriber.error(error);
}
};
ws.onerror = (error) => {
subscriber.error(error);
};
ws.onclose = () => {
subscriber.complete();
};
// Cleanup fonksiyonu
return () => {
console.log('WebSocket bağlantısı kapatılıyor');
ws.close();
};
});
}
const stockPrices$ = createStockStream('wss://stocks.example.com').pipe(
retry(3), // Bağlantı hatasında tekrar dene
share() // Subscriber'lar arasında bağlantı paylaş
);
// Farklı işlemlerle birden fazla observer
// Observer 1: AAPL chart'ını güncelle
stockPrices$.pipe(
filter(price => price.symbol === 'AAPL'),
map(price => price.value)
).subscribe(value => {
updateChart('AAPL', value);
});
// Observer 2: Portfolio toplamını hesapla
stockPrices$.pipe(
scan((acc, price) => {
acc[price.symbol] = price.value;
return acc;
}, {} as Record<string, number>),
map(prices => calculatePortfolioValue(prices))
).subscribe(total => {
updatePortfolioDisplay(total);
});
// Observer 3: Önemli değişikliklerde alert
stockPrices$.pipe(
filter(price => Math.abs(price.value - getPreviousPrice(price.symbol)) > 10),
map(price => ({
symbol: price.symbol,
change: price.value - getPreviousPrice(price.symbol)
}))
).subscribe(alert => {
showNotification(`${alert.symbol} ${alert.change} hareket etti`);
});
// Component unmount'tan sonra cleanup
const unsubscribe$ = new Subject<void>();
stockPrices$.pipe(
takeUntil(unsubscribe$)
).subscribe();
// Sonra: tüm subscription'ları temizle
function cleanup() {
unsubscribe$.next();
unsubscribe$.complete();
}
Unsubscribe socket’i kapatır, share() de her observer’ı tek bağlantıda tutar.
State Gözlemcisi Olarak Redux ve Zustand#
Redux, uygulama state yönetimi için Observer pattern uygular:
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';
interface User {
id: string;
name: string;
}
interface AppState {
count: number;
user: User | null;
}
// Modern Redux Toolkit yaklaşımı (createStore deprecated)
const appSlice = createSlice({
name: 'app',
initialState: { count: 0, user: null } as AppState,
reducers: {
increment: (state) => {
state.count += 1;
},
decrement: (state) => {
state.count -= 1;
},
setUser: (state, action: PayloadAction<User>) => {
state.user = action.payload;
}
}
});
const { increment, decrement, setUser } = appSlice.actions;
const store = configureStore({
reducer: appSlice.reducer
});
// State değişikliklerini subscribe et (gözle)
const unsubscribe = store.subscribe(() => {
const state = store.getState();
console.log('State değişti:', state);
// Yeni state'e göre UI'ı güncelle
});
// Action'ları dispatch et
store.dispatch(increment());
store.dispatch(setUser({ id: '1', name: 'John' }));
// Sonra: cleanup
unsubscribe();
React-Redux, component’leri store’a observer olarak bağlar.
Note
Modern React-Redux, connect HOC yerine hook’ları (useSelector/useDispatch) tercih eder. Aşağıdaki örnek eğitim amaçlı eski pattern’i gösterir.
import { connect } from 'react-redux';
interface CounterProps {
count: number;
increment: () => void;
decrement: () => void;
}
function Counter({ count, increment, decrement }: CounterProps) {
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
// Component store'u gözler, değişikliklerde re-render olur
export default connect(
(state: AppState) => ({
count: state.count
}),
(dispatch) => ({
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' })
})
)(Counter);
Zustand aynı subscribe-bildir fikrini hook’larla ve çok daha az boilerplate ile kurar:
import { create } from 'zustand';
interface StoreState {
count: number;
user: User | null;
increment: () => void;
decrement: () => void;
setUser: (user: User) => void;
}
// Store oluştur (subject)
const useStore = create<StoreState>((set) => ({
count: 0,
user: null,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
setUser: (user) => set({ user })
}));
// Component otomatik olarak ilgili state'i gözler
function Counter() {
// Sadece count değiştiğinde re-render olur
const count = useStore(state => state.count);
const increment = useStore(state => state.increment);
const decrement = useStore(state => state.decrement);
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
Yukarıdaki store, React-Redux’ın sunduğu her özelliği daha az kodla korur. Component’ler hook’larla subscribe olur ve yalnızca seçtikleri state değiştiğinde yeniden render edilir. Ortada action creator, reducer ya da connect HOC yok; buna karşın type inference baştan sona çalışır ve middleware katmanı DevTools, persist ve immer’ı kapsamaya devam eder.
Observer Varyantı Seçimi#
| Araç | Ne zaman tercih edilir |
|---|---|
| RxJS | Birden fazla stream’i birleştiren karmaşık async koordinasyon, gelişmiş operatörler (debounce, throttle, retry), WebSocket veya SSE bağlantıları, event stream işleme ya da backpressure handling kritikse |
| Redux | Karmaşık state’li büyük uygulama, time-travel debugging gerekliyse, birçok component aynı state’e erişiyorsa ya da middleware ekosistemi (saga’lar, thunk’lar) önemliyse ve ekip Redux pattern’lerine aşinaysa |
| Zustand | Özellikler yerine sadelik isteyen orta ölçekli uygulama, Redux DevTools gerekmiyorsa, connect HOC yerine hook tercih ediliyorsa, TypeScript desteği öncelikliyse |
| Plain EventEmitter | Node.js backend servisleri, modül içinde basit pub/sub, operator veya backpressure gerekmiyorsa, minimal dependency isteniyorsa |
Class Hiyerarşisi Olmadan Strategy#
Strategy pattern, runtime’da hangi algoritmanın çalışacağını seçer. TypeScript’in first-class function’ları sayesinde bunun için genelde bir class hiyerarşisi gerekmez: düz bir function çoğunlukla daha iyi çalışır.
Interface Tabanlı Ödeme Strategy’leri#
Interface’ler ve concrete implementation’lar bunu standart yoldan halleder:
interface PaymentStrategy {
pay(amount: number): Promise<void>;
}
class CreditCardStrategy implements PaymentStrategy {
constructor(
private cardNumber: string,
private cvv: string
) {}
async pay(amount: number): Promise<void> {
console.log(`Kart ${this.cardNumber} ile $${amount} ödeniyor`);
// Kredi kartı API çağrısı
await this.processCreditCard(amount);
}
private async processCreditCard(amount: number): Promise<void> {
// İmplementasyon detayları
}
}
class PayPalStrategy implements PaymentStrategy {
constructor(private email: string) {}
async pay(amount: number): Promise<void> {
console.log(`PayPal ile ${this.email} adresine $${amount} ödeniyor`);
// PayPal API çağrısı
await this.processPayPal(amount);
}
private async processPayPal(amount: number): Promise<void> {
// İmplementasyon detayları
}
}
class CryptoStrategy implements PaymentStrategy {
constructor(private walletAddress: string) {}
async pay(amount: number): Promise<void> {
console.log(`Cüzdan ${this.walletAddress} ile $${amount} ödeniyor`);
// Crypto ödeme işleme
await this.processCrypto(amount);
}
private async processCrypto(amount: number): Promise<void> {
// İmplementasyon detayları
}
}
// Context class strategy kullanır
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
setStrategy(strategy: PaymentStrategy): void {
this.strategy = strategy;
}
async processPayment(amount: number): Promise<void> {
await this.strategy.pay(amount);
}
}
// Kullanım
const processor = new PaymentProcessor(
new CreditCardStrategy('4111-1111-1111-1111', '123')
);
await processor.processPayment(100);
processor.setStrategy(new PayPalStrategy('user@example.com'));
await processor.processPayment(50);
Üç ödeme yöntemi için üç class; hiçbiri bir constructor ve bir pay methodundan fazlasını taşımıyor. Bu, bir function’ın doğrudan yapabileceği bir iş için epey tören.
Strategy Olarak Function’lar#
JavaScript/TypeScript’te first-class function’lar var. Strategy’ler basit function’lar olabilir:
type PaymentStrategy = (amount: number) => Promise<void>;
const creditCardPayment: PaymentStrategy = async (amount) => {
console.log(`Kredi kartı ile $${amount} ödeniyor`);
// Kredi kartı işleme
};
const paypalPayment: PaymentStrategy = async (amount) => {
console.log(`PayPal ile $${amount} ödeniyor`);
// PayPal işleme
};
const cryptoPayment: PaymentStrategy = async (amount) => {
console.log(`Crypto ile $${amount} ödeniyor`);
// Crypto işleme
};
// Context function kabul eder
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
setStrategy(strategy: PaymentStrategy): void {
this.strategy = strategy;
}
async processPayment(amount: number): Promise<void> {
await this.strategy(amount);
}
}
// Daha da basit: class gerekmez
async function processPayment(
amount: number,
strategy: PaymentStrategy
): Promise<void> {
await strategy(amount);
}
// Kullanım
await processPayment(100, creditCardPayment);
await processPayment(50, paypalPayment);
Function strategy’ler class boilerplate’ini tamamen bırakır. Function mock’lamak class mock’lamaktan basittir, higher-order function’lar da bunları doğrudan compose eder.
Closure’lar ayrıca strategy’ye private state kazandırır:
type PaymentStrategy = (amount: number) => Promise<void>;
function createCreditCardPayment(
cardNumber: string,
cvv: string
): PaymentStrategy {
// Closure kart detaylarını yakalar
return async (amount: number) => {
console.log(`Son 4 hanesi ${cardNumber.slice(-4)} olan karta $${amount} ödeniyor`);
// Yakalanan credential'larla işle
await processCreditCard(cardNumber, cvv, amount);
};
}
function createPayPalPayment(email: string): PaymentStrategy {
return async (amount: number) => {
console.log(`PayPal hesabı ${email} ile $${amount} ödeniyor`);
await processPayPal(email, amount);
};
}
// Kullanım
const creditCard = createCreditCardPayment('4111-1111-1111-1111', '123');
const paypal = createPayPalPayment('user@example.com');
await processPayment(100, creditCard);
await processPayment(50, paypal);
React Prop’ları Aracılığıyla Strategy#
React’te strategy’ler genellikle render prop’lar veya component prop’lar olarak görünür:
interface DataGridProps<T> {
data: T[];
renderRow: (item: T, index: number) => JSX.Element; // Strategy
renderEmpty?: () => JSX.Element;
renderLoading?: () => JSX.Element;
}
function DataGrid<T>({
data,
renderRow,
renderEmpty,
renderLoading
}: DataGridProps<T>) {
if (loading && renderLoading) {
return renderLoading();
}
if (data.length === 0 && renderEmpty) {
return renderEmpty();
}
return (
<div className="data-grid">
{data.map((item, i) => (
<div key={i} className="grid-row">
{renderRow(item, i)}
</div>
))}
</div>
);
}
// Farklı rendering strategy'leri
interface User {
id: string;
name: string;
email: string;
avatar: string;
}
// Strategy 1: Kart düzeni
<DataGrid
data={users}
renderRow={(user) => (
<div className="user-card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)}
/>
// Strategy 2: Liste düzeni
<DataGrid
data={users}
renderRow={(user) => (
<div className="user-list-item">
<span>{user.name}</span>
<span>{user.email}</span>
</div>
)}
/>
// Strategy 3: Tablo satırı
<table>
<DataGrid
data={users}
renderRow={(user) => (
<>
<td>{user.name}</td>
<td>{user.email}</td>
</>
)}
/>
</table>
Compose Edilmiş Strategy’lerle Form Validation#
Form validation, composable strategy pattern’den faydalanır:
type ValidationStrategy = (value: string) => string | null;
// Tekil validation strategy'leri
const required: ValidationStrategy = (value) => {
return value.trim() ? null : 'Bu alan zorunludur';
};
const email: ValidationStrategy = (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value) ? null : 'Geçersiz email adresi';
};
const minLength = (min: number): ValidationStrategy => (value) => {
return value.length >= min
? null
: `En az ${min} karakter olmalı`;
};
const maxLength = (max: number): ValidationStrategy => (value) => {
return value.length <= max
? null
: `En fazla ${max} karakter olmalı`;
};
const phone: ValidationStrategy = (value) => {
const phoneRegex = /^\d{10}$/;
return phoneRegex.test(value) ? null : 'Geçersiz telefon numarası';
};
// Birden fazla strategy'yi compose et
function composeValidations(...strategies: ValidationStrategy[]): ValidationStrategy {
return (value: string) => {
for (const strategy of strategies) {
const error = strategy(value);
if (error) return error;
}
return null;
};
}
// Compose edilmiş strategy'lerle field konfigürasyonları
interface FieldConfig {
name: string;
label: string;
validate: ValidationStrategy;
}
const formConfig: FieldConfig[] = [
{
name: 'email',
label: 'Email Adresi',
validate: composeValidations(required, email)
},
{
name: 'password',
label: 'Şifre',
validate: composeValidations(
required,
minLength(8),
maxLength(128)
)
},
{
name: 'phone',
label: 'Telefon Numarası',
validate: composeValidations(required, phone)
}
];
// Form component strategy'leri kullanır
function RegistrationForm() {
const [values, setValues] = useState<Record<string, string>>({});
const [errors, setErrors] = useState<Record<string, string>>({});
const handleChange = (name: string, value: string) => {
setValues(prev => ({ ...prev, [name]: value }));
// Strategy kullanarak validate et
const field = formConfig.find(f => f.name === name);
if (field) {
const error = field.validate(value);
setErrors(prev => ({ ...prev, [name]: error || '' }));
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Tüm field'ları validate et
const newErrors: Record<string, string> = {};
formConfig.forEach(field => {
const error = field.validate(values[field.name] || '');
if (error) newErrors[field.name] = error;
});
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
// Formu submit et
console.log('Form geçerli:', values);
};
return (
<form onSubmit={handleSubmit}>
{formConfig.map(field => (
<div key={field.name}>
<label>{field.label}</label>
<input
value={values[field.name] || ''}
onChange={(e) => handleChange(field.name, e.target.value)}
/>
{errors[field.name] && (
<span className="error">{errors[field.name]}</span>
)}
</div>
))}
<button type="submit">Kayıt Ol</button>
</form>
);
}
Buradaki tek yeni soyutlama compose fonksiyonu. Validator’ler birden fazla formda yeniden kullanılıyor, her biri tek başına test ediliyor ve field konfigürasyonu declarative bir kural listesi gibi okunuyor.
Strategy pattern, algoritma seçimi runtime’da yapıldığında, aynı interface’i paylaşan birden fazla implementasyon olduğunda, davranış konfigürasyona göre değiştiğinde veya testler implementasyon değiştirmeyi gerektirdiğinde mantıklı olur. Sadece bir implementasyon varsa, mantık if ile rahatça okunan basit bir conditional’sa veya function’lar ortak bir şekle abstract edilmeye direniyorsa vazgeç.
Undo, Redo ve Redux Action’larını Kapsüllemek#
Bir isteği nesneye sarmak, Command pattern’in özü, parameterization, queuing, logging ve undo’yu mümkün kılar. Redux action’ları, undo/redo yığınları ve task queue’ler, adını koymasak da hep buna dayanır.
Execute ve Undo’lu Command Nesneleri#
execute() ve undo() methodlarına sahip command nesneleri standart şeklidir:
interface Command {
execute(): void;
undo(): void;
}
class TodoList {
private todos: string[] = [];
add(todo: string): void {
this.todos.push(todo);
}
remove(index: number): void {
this.todos.splice(index, 1);
}
getTodos(): string[] {
return [...this.todos];
}
}
class AddTodoCommand implements Command {
private index: number = -1;
constructor(
private todoList: TodoList,
private todo: string
) {}
execute(): void {
this.todoList.add(this.todo);
this.index = this.todoList.getTodos().length - 1;
}
undo(): void {
if (this.index !== -1) {
this.todoList.remove(this.index);
}
}
}
class RemoveTodoCommand implements Command {
private removedTodo: string = '';
private removedIndex: number = -1;
constructor(
private todoList: TodoList,
private index: number
) {}
execute(): void {
const todos = this.todoList.getTodos();
this.removedTodo = todos[this.index];
this.removedIndex = this.index;
this.todoList.remove(this.index);
}
undo(): void {
// Tam pozisyona geri yüklemek kolay değil
this.todoList.add(this.removedTodo);
}
}
// Undo/redo için command history
class CommandHistory {
private history: Command[] = [];
private current: number = -1;
execute(command: Command): void {
// Command'ı çalıştır
command.execute();
// Mevcut pozisyondan sonraki command'ları kaldır
this.history = this.history.slice(0, this.current + 1);
// Command'ı history'e ekle
this.history.push(command);
this.current++;
}
undo(): void {
if (this.canUndo()) {
this.history[this.current].undo();
this.current--;
}
}
redo(): void {
if (this.canRedo()) {
this.current++;
this.history[this.current].execute();
}
}
canUndo(): boolean {
return this.current >= 0;
}
canRedo(): boolean {
return this.current < this.history.length - 1;
}
}
// Kullanım
const todoList = new TodoList();
const history = new CommandHistory();
history.execute(new AddTodoCommand(todoList, 'Süt al'));
history.execute(new AddTodoCommand(todoList, 'Köpeği gezdir'));
console.log(todoList.getTodos()); // ['Süt al', 'Köpeği gezdir']
history.undo();
console.log(todoList.getTodos()); // ['Süt al']
history.redo();
console.log(todoList.getTodos()); // ['Süt al', 'Köpeği gezdir']
Command Olarak Redux Action’ları#
Redux action’ları command nesneleridir. State değişikliklerini serializable data olarak kapsüllerler:
// Action type'ları (command type'ları)
interface AddTodoAction {
type: 'ADD_TODO';
payload: {
id: string;
text: string;
};
}
interface RemoveTodoAction {
type: 'REMOVE_TODO';
payload: {
id: string;
};
}
interface ToggleTodoAction {
type: 'TOGGLE_TODO';
payload: {
id: string;
};
}
type TodoAction = AddTodoAction | RemoveTodoAction | ToggleTodoAction;
// Action creator'lar (command factory'leri)
function addTodo(text: string): AddTodoAction {
return {
type: 'ADD_TODO',
payload: {
id: crypto.randomUUID(), // Not: crypto.randomUUID() HTTPS veya localhost gerektirir
text
}
};
}
function removeTodo(id: string): RemoveTodoAction {
return {
type: 'REMOVE_TODO',
payload: { id }
};
}
function toggleTodo(id: string): ToggleTodoAction {
return {
type: 'TOGGLE_TODO',
payload: { id }
};
}
// Reducer (command handler)
interface TodoState {
todos: Array<{ id: string; text: string; completed: boolean }>;
}
function todoReducer(
state: TodoState = { todos: [] },
action: TodoAction
): TodoState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: action.payload.id,
text: action.payload.text,
completed: false
}
]
};
case 'REMOVE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload.id)
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
)
};
default:
return state;
}
}
// Store (command executor)
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: todoReducer
});
// Command'ları dispatch et
store.dispatch(addTodo('Süt al'));
store.dispatch(addTodo('Köpeği gezdir'));
store.dispatch(toggleTodo(/* id */));
Action’ları bu şekilde yapılandırmak birkaç şeyi aynı anda mümkün kılar: plain object olmaları log’lanabilmelerini ve store edilebilmelerini sağlar, time-travel debugging için replay edilebilirler, middleware tarafından intercept edilip transform edilebilirler ve undo/redo için history olarak store edilebilirler.
Middleware ile Command’ları Yakalamak#
Middleware, reducer’lara ulaşmadan önce command’ları intercept eder:
import { Middleware } from 'redux';
// Logging middleware
const logger: Middleware = store => next => action => {
console.log('Dispatch ediliyor:', action);
const result = next(action);
console.log('Sonraki state:', store.getState());
return result;
};
// Analytics middleware
const analytics: Middleware = store => next => action => {
// Kullanıcı action'larını track et
if (action.type.startsWith('USER_')) {
trackEvent(action.type, action.payload);
}
return next(action);
};
// Error handling middleware
const errorHandler: Middleware = store => next => action => {
try {
return next(action);
} catch (error) {
console.error('Action hatası:', error);
store.dispatch({
type: 'ERROR_OCCURRED',
payload: { error: error.message }
});
}
};
// Redux Toolkit ile middleware uygula
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(logger, analytics, errorHandler)
});
Redux Thunk de aynı zincirde durur ve side effect taşıyan command’ları üstlenir:
import { ThunkAction } from 'redux-thunk';
import { AnyAction } from 'redux';
type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
AppState,
unknown,
AnyAction
>;
// Async command
const fetchUser = (userId: string): AppThunk => async (dispatch) => {
// Request command dispatch et
dispatch({
type: 'FETCH_USER_REQUEST',
payload: { userId }
});
try {
// Side effect gerçekleştir
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
// Success command dispatch et
dispatch({
type: 'FETCH_USER_SUCCESS',
payload: { user }
});
} catch (error) {
// Error command dispatch et
dispatch({
type: 'FETCH_USER_FAILURE',
payload: { error: error.message }
});
}
};
// Async command dispatch et
store.dispatch(fetchUser('123'));
Rich Text Editor’ın Undo Stack’i#
Rich text editor, Command pattern’in undo/redo değerini gösterir:
interface EditorCommand {
execute(editor: Editor): void;
undo(editor: Editor): void;
description: string;
}
interface Editor {
content: string;
selectionStart: number;
selectionEnd: number;
insertText(position: number, text: string): void;
deleteText(position: number, length: number): string;
setSelection(start: number, end: number): void;
}
class InsertTextCommand implements EditorCommand {
description: string;
constructor(
private position: number,
private text: string
) {
this.description = `${position} pozisyonuna "${text}" ekle`;
}
execute(editor: Editor): void {
editor.insertText(this.position, this.text);
editor.setSelection(
this.position + this.text.length,
this.position + this.text.length
);
}
undo(editor: Editor): void {
editor.deleteText(this.position, this.text.length);
editor.setSelection(this.position, this.position);
}
}
class DeleteTextCommand implements EditorCommand {
private deletedText: string = '';
description: string;
constructor(
private position: number,
private length: number
) {
this.description = `${position} pozisyonundan ${length} karakter sil`;
}
execute(editor: Editor): void {
this.deletedText = editor.deleteText(this.position, this.length);
editor.setSelection(this.position, this.position);
}
undo(editor: Editor): void {
editor.insertText(this.position, this.deletedText);
editor.setSelection(
this.position + this.deletedText.length,
this.position + this.deletedText.length
);
}
}
class FormatTextCommand implements EditorCommand {
private previousFormat: string = '';
description: string;
constructor(
private start: number,
private end: number,
private format: 'bold' | 'italic' | 'underline'
) {
this.description = `${start}'dan ${end}'e ${format} uygula`;
}
execute(editor: Editor): void {
this.previousFormat = editor.getFormat(this.start, this.end);
editor.applyFormat(this.start, this.end, this.format);
}
undo(editor: Editor): void {
editor.applyFormat(this.start, this.end, this.previousFormat);
}
}
// Gruplama ile command history
class EditorHistory {
private history: EditorCommand[] = [];
private current: number = -1;
private groupedCommands: EditorCommand[] = [];
private isGrouping: boolean = false;
execute(command: EditorCommand, editor: Editor): void {
command.execute(editor);
if (this.isGrouping) {
this.groupedCommands.push(command);
return;
}
this.addToHistory(command);
}
private addToHistory(command: EditorCommand): void {
// Mevcut pozisyondan sonraki command'ları kaldır
this.history = this.history.slice(0, this.current + 1);
this.history.push(command);
this.current++;
}
beginGroup(): void {
this.isGrouping = true;
this.groupedCommands = [];
}
endGroup(): void {
this.isGrouping = false;
if (this.groupedCommands.length > 0) {
const group = new CompositeCommand(this.groupedCommands);
this.addToHistory(group);
}
}
undo(editor: Editor): void {
if (this.canUndo()) {
this.history[this.current].undo(editor);
this.current--;
}
}
redo(editor: Editor): void {
if (this.canRedo()) {
this.current++;
this.history[this.current].execute(editor);
}
}
canUndo(): boolean {
return this.current >= 0;
}
canRedo(): boolean {
return this.current < this.history.length - 1;
}
getHistory(): string[] {
return this.history.map(cmd => cmd.description);
}
}
// Gruplama için composite command
class CompositeCommand implements EditorCommand {
description: string;
constructor(private commands: EditorCommand[]) {
this.description = `${commands.length} command grubu`;
}
execute(editor: Editor): void {
this.commands.forEach(cmd => cmd.execute(editor));
}
undo(editor: Editor): void {
// Ters sırayla undo
for (let i = this.commands.length - 1; i >= 0; i--) {
this.commands[i].undo(editor);
}
}
}
// Editor component'inde kullanım
class RichTextEditor {
private history = new EditorHistory();
insertText(position: number, text: string): void {
const command = new InsertTextCommand(position, text);
this.history.execute(command, this.editor);
}
deleteText(position: number, length: number): void {
const command = new DeleteTextCommand(position, length);
this.history.execute(command, this.editor);
}
applyFormat(start: number, end: number, format: 'bold' | 'italic' | 'underline'): void {
const command = new FormatTextCommand(start, end, format);
this.history.execute(command, this.editor);
}
// Birden fazla command'ı tek undoable action olarak grupla
paste(text: string): void {
this.history.beginGroup();
// Varsa selection'ı sil
if (this.editor.selectionStart !== this.editor.selectionEnd) {
this.deleteText(
this.editor.selectionStart,
this.editor.selectionEnd - this.editor.selectionStart
);
}
// Yapıştırılan text'i ekle
this.insertText(this.editor.selectionStart, text);
this.history.endGroup();
}
undo(): void {
this.history.undo(this.editor);
}
redo(): void {
this.history.redo(this.editor);
}
}
Bu karmaşıklık karşılığında her düzenleme operasyonu tek tek undo edilebiliyor, paste tek action olarak geri alınıyor ve history class’ına dokunmadan yeni command type’ları eklenebiliyor.
Undo/redo, queue’lanan veya schedule edilen operasyonlar, logging ve audit, macro kaydı ve transaction desteği; bunların hepsi Command’ı gerektirir. Undo içermeyen basit CRUD bunlardan hiçbirine ihtiyaç duymaz; gerçek zamanlı collaboration ise CRDT ile daha iyi çözülür.
Boolean Flag Yerine State Machine#
State pattern, bir nesnenin davranışının internal state’iyle birlikte değişmesini sağlar; UI kodunda bu, imkansız state kombinasyonlarının ortadan kalkması ve transition’ların üstü kapalı olmaktan çıkması demektir.
Boolean State Cehennemi#
Karmaşık UI genellikle birden fazla boolean flag’e yol açar:
function Form() {
const [isValidating, setIsValidating] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [hasError, setHasError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// Ya isSubmitting ve hasError ikisi de true ise?
// Ya isSuccess ve isValidating ikisi de true ise?
// Birçok imkansız kombinasyon mümkün
}
Bu tür boolean state’ler imkansız kombinasyonları mümkün kılar: isSubmitting ve isSuccess aynı anda true olabilir. Transition’lar da belirsiz kalır, çünkü kodun hiçbir yeri validating’den submitting’e nasıl geçildiğini söylemez ve test kombinasyon patlamasına döner.
State Pattern Olarak Discriminated Union’lar#
TypeScript discriminated union’ları imkansız state’leri önler:
type FormState =
| { status: 'idle' }
| { status: 'validating' }
| { status: 'submitting'; progress: number }
| { status: 'success'; data: SubmitResult }
| { status: 'error'; error: Error };
function Form() {
const [state, setState] = useState<FormState>({ status: 'idle' });
const handleSubmit = async () => {
// Validating'e geç
setState({ status: 'validating' });
try {
await validateForm();
// Submitting'e geç
setState({ status: 'submitting', progress: 0 });
const result = await submitForm((progress) => {
setState({ status: 'submitting', progress });
});
// Success'e geç
setState({ status: 'success', data: result });
} catch (error) {
// Error'a geç
setState({ status: 'error', error });
}
};
// State'e göre render et
switch (state.status) {
case 'idle':
return <FormFields onSubmit={handleSubmit} />;
case 'validating':
return <Spinner message="Doğrulanıyor..." />;
case 'submitting':
return <ProgressBar progress={state.progress} />;
case 'success':
return <SuccessMessage data={state.data} />;
case 'error':
return (
<>
<ErrorMessage error={state.error} />
<button onClick={handleSubmit}>Tekrar Dene</button>
</>
);
}
}
Discriminated union’lar imkansız state’leri baştan eler (tip aynı anda validating ve success olamaz), TypeScript’in tüm state’lerin handle edildiğini garanti etmesini sağlar, transition’ları explicit tutar ve her state’e sadece ilgili datayı ekler (progress sadece submitting’deyken).
XState ile Explicit State Machine’ler#
XState declarative state machine tanımı sağlar:
import { setup, fromPromise, assign } from 'xstate';
// XState v5, actor logic'i ve tipleri setup() ile önden tanımlar
const formMachine = setup({
types: {
context: {} as {
formData: any;
error: any;
},
events: {} as
| { type: 'SUBMIT' }
| { type: 'RETRY' }
| { type: 'CANCEL' }
},
actors: {
validateForm: fromPromise(async () => {
// Validation mantığı
await new Promise(resolve => setTimeout(resolve, 1000));
}),
submitForm: fromPromise(async () => {
// Submission mantığı
await new Promise(resolve => setTimeout(resolve, 2000));
return { success: true };
})
}
}).createMachine({
id: 'form',
initial: 'idle',
context: {
formData: null,
error: null
},
states: {
idle: {
on: {
SUBMIT: 'validating'
}
},
validating: {
invoke: {
src: 'validateForm',
onDone: {
target: 'submitting'
},
onError: {
target: 'error',
actions: assign({ error: ({ event }) => event.error })
}
}
},
submitting: {
invoke: {
src: 'submitForm',
onDone: {
target: 'success',
actions: assign({ formData: ({ event }) => event.output })
},
onError: {
target: 'error',
actions: assign({ error: ({ event }) => event.error })
}
}
},
success: {
type: 'final'
},
error: {
on: {
RETRY: 'validating',
CANCEL: 'idle'
}
}
}
});
// React'te kullan
import { useMachine } from '@xstate/react';
function Form() {
const [state, send] = useMachine(formMachine);
return (
<form onSubmit={(e) => {
e.preventDefault();
send({ type: 'SUBMIT' });
}}>
{state.matches('idle') && (
<div>
<input name="email" />
<button type="submit">Gönder</button>
</div>
)}
{state.matches('validating') && (
<Spinner message="Form doğrulanıyor..." />
)}
{state.matches('submitting') && (
<Spinner message="Gönderiliyor..." />
)}
{state.matches('success') && (
<SuccessMessage data={state.context.formData} />
)}
{state.matches('error') && (
<div>
<ErrorMessage error={state.context.error} />
<button onClick={() => send({ type: 'RETRY' })}>Tekrar Dene</button>
<button onClick={() => send({ type: 'CANCEL' })}>İptal</button>
</div>
)}
</form>
);
}
State Machine’leri Görselleştirmek#
Yukarıdaki discriminated union’a kıyasla, explicit machine şunları ekler:
- 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
Makine State’leri Olarak Wizard Adımları#
Multi-step form’lar explicit state machine’lerden faydalanır:
import { setup, fromPromise, assign } from 'xstate';
interface WizardContext {
step1Data: any;
step2Data: any;
step3Data: any;
error: string | null;
}
type WizardEvent =
| { type: 'NEXT'; data: any }
| { type: 'PREVIOUS' }
| { type: 'SUBMIT' }
| { type: 'RESET' };
// XState v5, setup() ile
const wizardMachine = setup({
types: {
context: {} as WizardContext,
events: {} as WizardEvent
},
actors: {
// Actor logic verisini `input` üzerinden alır
submitWizard: fromPromise(async ({ input }: { input: WizardContext }) => {
const response = await fetch('/api/wizard', {
method: 'POST',
body: JSON.stringify({
step1: input.step1Data,
step2: input.step2Data,
step3: input.step3Data
})
});
if (!response.ok) {
throw new Error('Gönderim başarısız');
}
return response.json();
})
}
}).createMachine({
id: 'wizard',
initial: 'step1',
context: {
step1Data: null,
step2Data: null,
step3Data: null,
error: null
},
states: {
step1: {
on: {
NEXT: {
target: 'step2',
actions: assign({
step1Data: ({ event }) => event.data
})
}
}
},
step2: {
on: {
NEXT: {
target: 'step3',
actions: assign({
step2Data: ({ event }) => event.data
})
},
PREVIOUS: 'step1'
}
},
step3: {
on: {
PREVIOUS: 'step2',
SUBMIT: 'submitting'
}
},
submitting: {
invoke: {
src: 'submitWizard',
input: ({ context }) => context,
onDone: 'success',
onError: {
target: 'step3',
actions: assign({
error: ({ event }) => (event.error as Error).message
})
}
}
},
success: {
type: 'final'
}
}
});
// React component
function Wizard() {
const [state, send] = useMachine(wizardMachine);
const handleNext = (data: any) => {
send({ type: 'NEXT', data });
};
const handlePrevious = () => {
send({ type: 'PREVIOUS' });
};
const handleSubmit = () => {
send({ type: 'SUBMIT' });
};
return (
<div>
{state.matches('step1') && (
<Step1 onNext={handleNext} />
)}
{state.matches('step2') && (
<Step2
initialData={state.context.step1Data}
onNext={handleNext}
onPrevious={handlePrevious}
/>
)}
{state.matches('step3') && (
<Step3
data={{
step1: state.context.step1Data,
step2: state.context.step2Data
}}
error={state.context.error}
onSubmit={handleSubmit}
onPrevious={handlePrevious}
/>
)}
{state.matches('submitting') && (
<Spinner message="Wizard gönderiliyor..." />
)}
{state.matches('success') && (
<SuccessMessage message="Wizard tamamlandı!" />
)}
</div>
);
}
2-3 state’li basit form’larda, ekip state machine’lere aşina değilse veya discriminated union’lar aynı imkansız kombinasyonları zaten engelliyorsa vazgeç. State sayısı arttıkça ve transition’lar görsel olarak review edilmeye değer business rule’lar taşımaya başladığında, ya da transition’ları doğrudan test etmek kritik hale geldiğinde state machine’lere yönel.
Mediator ile Component’leri Ayırmak#
Mediator pattern iletişimi merkezileştirir, böylece nesneler birbirine direkt referans vermez; pratikte bu, event bus, React Context ve state management library’leri şeklinde karşımıza çıkar.
Mediator Olmadan Tight Coupling#
Mediator olmadan component’ler birbirlerine direkt referans verir:
class UserList {
constructor(private userDetails: UserDetails) {}
selectUser(user: User): void {
// UserDetails'e direkt bağımlılık
this.userDetails.display(user);
}
}
class UserDetails {
display(user: User): void {
// Kullanıcı detaylarını göster
}
}
// UserList'i UserDetails olmadan test etmek zor
// UserList'i farklı detail component ile yeniden kullanmak zor
Ayrıştırma için Event Bus#
Event bus component’leri ayırır:
type EventCallback = (data: any) => void;
class EventBus {
private events = new Map<string, EventCallback[]>();
subscribe(event: string, callback: EventCallback): () => void {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(callback);
// Unsubscribe fonksiyonunu döndür
return () => {
const callbacks = this.events.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
};
}
publish(event: string, data?: any): void {
const callbacks = this.events.get(event);
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
clear(): void {
this.events.clear();
}
}
// Component'ler mediator aracılığıyla iletişim kurar
class UserList {
constructor(private eventBus: EventBus) {}
selectUser(user: User): void {
// Direkt çağrı yerine event publish et
this.eventBus.publish('user:selected', user);
}
}
class UserDetails {
constructor(private eventBus: EventBus) {
// Event'lere subscribe ol
this.eventBus.subscribe('user:selected', this.display.bind(this));
}
display(user: User): void {
console.log('Kullanıcı gösteriliyor:', user);
}
}
class UserStats {
constructor(private eventBus: EventBus) {
this.eventBus.subscribe('user:selected', this.loadStats.bind(this));
}
loadStats(user: User): void {
console.log('İstatistikler yükleniyor:', user);
}
}
// Kullanım
const eventBus = new EventBus();
const userList = new UserList(eventBus);
const userDetails = new UserDetails(eventBus);
const userStats = new UserStats(eventBus);
// Component'ler birbirlerini bilmez
userList.selectUser({ id: '1', name: 'John' });
React Context ile State Paylaşımı#
React Context, component ağaçları için mediator pattern sağlar:
interface AppContextValue {
selectedUser: User | null;
setSelectedUser: (user: User | null) => void;
notifications: Notification[];
addNotification: (notification: Notification) => void;
}
const AppContext = createContext<AppContextValue>(null!);
function AppProvider({ children }: { children: ReactNode }) {
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [notifications, setNotifications] = useState<Notification[]>([]);
const addNotification = (notification: Notification) => {
setNotifications(prev => [...prev, notification]);
};
return (
<AppContext.Provider value={{
selectedUser,
setSelectedUser,
notifications,
addNotification
}}>
{children}
</AppContext.Provider>
);
}
// Component'ler context aracılığıyla iletişim kurar
function UserList() {
const { setSelectedUser } = useContext(AppContext);
const users = useUsers();
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => setSelectedUser(user)}>
{user.name}
</li>
))}
</ul>
);
}
function UserDetails() {
const { selectedUser } = useContext(AppContext);
if (!selectedUser) {
return <div>Bir kullanıcı seç</div>;
}
return (
<div>
<h2>{selectedUser.name}</h2>
<p>{selectedUser.email}</p>
</div>
);
}
function UserStats() {
const { selectedUser } = useContext(AppContext);
if (!selectedUser) return null;
return <div>{selectedUser.name} için istatistikler</div>;
}
// App yapısı
function App() {
return (
<AppProvider>
<UserList />
<UserDetails />
<UserStats />
</AppProvider>
);
}
Redux’ta İletişimi Merkezileştirmek#
Redux tüm uygulama iletişimini action’lar aracılığıyla merkezileştirir:
// Tüm component'ler store aracılığıyla iletişim kurar
function UserList() {
const dispatch = useDispatch();
const users = useSelector(state => state.users);
const handleSelect = (user: User) => {
// Component'ler birbirlerini direkt çağırmaz
dispatch(selectUser(user));
dispatch(fetchUserDetails(user.id));
dispatch(loadUserStats(user.id));
};
return (
<ul>
{users.map(user => (
<li key={user.id} onClick={() => handleSelect(user)}>
{user.name}
</li>
))}
</ul>
);
}
function UserDetails() {
const selectedUser = useSelector(state => state.selectedUser);
const userDetails = useSelector(state => state.userDetails);
if (!selectedUser) return null;
return (
<div>
<h2>{selectedUser.name}</h2>
{userDetails && <DetailedInfo details={userDetails} />}
</div>
);
}
function UserStats() {
const userStats = useSelector(state => state.userStats);
if (!userStats) return null;
return <StatsDisplay stats={userStats} />;
}
Sadece 2-3 component söz konusuysa direkt iletişim en basit çözümdür: parent-child prop’lar zaten çalışıyorsa Mediator’dan vazgeç, çünkü eklenen indirection hiçbir şey kazandırmadan debugging’i zorlaştırır. Bu ölçeğin ötesinde doğru araç kapsama göre değişir. Event bus, global state gereksinimi olmayan bir prototipte aynı modüldeki pub/sub’ı karşılar; React Context ise tema, auth veya locale için prop drilling acı vermeye başladığında tek bir subtree’yi karşılar. Redux, birçok component’in aynı state’e eriştiği, DevTools ve time-travel debugging’in önem taşıdığı ve ekibin middleware ekosistemine zaten aşina olduğu büyük uygulamalarda ağırlığını hak eder.
Dil Özelliği mi, Kütüphane mi#
Önce dile bakan varsayılan, uygulama kodunun çoğunda geçerli: ortada ek bir dependency yok ve koda yeni katılan birine önce bir kütüphane anlatmak gerekmiyor. Yükseltmenin, elle yazsan kötü yazacağın bir yeteneği satın aldığı yerde varsayılanı bırak.
İ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ü
Kaynaklar#
- Davranışsal Tasarım Kalıpları (yeni sekmede açılır) - Observer, Strategy, Command, State ve Mediator kalıplarının refactoring.guru kataloğu
- RxJS Dokümantasyonu (yeni sekmede açılır) - JavaScript için resmi reaktif programlama kütüphanesi
- ReactiveX - Observable (yeni sekmede açılır) - Diller arası Observable spesifikasyonu ve operatör referansı
- XState Dokümantasyonu (yeni sekmede açılır) - State machine örneklerinde kullanılan
setup(), actor logic veassigniçin XState v5 referansı - Redux Toolkit (yeni sekmede açılır) -
configureStore,createSliceve middleware kurulumunu kapsayan resmi Redux araç seti - Tasarım Kalıpları: Yeniden Kullanılabilir Nesne Yönelimli Yazılımın Unsurları (yeni sekmede açılır) - 23 klasik kalıbı tanımlayan orijinal GoF kitabı
- Tasarım Kalıpları Kataloğu (yeni sekmede açılır) - TypeScript örnekleriyle refactoring.guru tam kalıp kataloğu
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.
Bu serideki tüm yazılar
İlgili yazılar
Mimari ağırlığını runtime'ın init-amortismanına göre seç: single-purpose Lambda'da yalın handler, Lambdalith'te orta, tam OOP/DI yalnızca uzun ömürlü runtime'da.
architecture · lambda · serverless +3
Effect'i adım adım öğrenmek ve AWS Lambda ile entegre etmek için pratik bir rehber: gerçek kod örnekleri, yaygın hatalar ve üretim desenleri.
typescript · functional-programming · lambda +4
AWS Lambda ve DynamoDB üzerinde Effect ve SST v4 ile küçük, eksiksiz bir URL kısaltıcı; sınırda şema, layer'lar ve tagged error eşlemesini gösterir.
functional-programming · lambda · typescript +2
Async backend'le çalışan tasarımcılar için pragmatik rehber: üç etkileşim şekli, hangisi ne zaman, ve karşı durmanız gereken dört anti-pattern.
event-driven · state-management · design-patterns +2
SOLID prensiplerinin modern JavaScript'te uygulanışı: TypeScript, React hooks ve fonksiyonel pattern'lerle pratik örnekler, ayrıca ne zaman gereksiz.
typescript · javascript · react +4