2025-11-06
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’ı:
class DatabaseConnection {
private static instance: DatabaseConnection;
private constructor() {
// Private constructor direkt instantiation'ı önler
}
static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
query(sql: string): Promise<any> {
// Database operasyonları
}
}
// Kullanım
const db = DatabaseConnection.getInstance();
await db.query('SELECT * FROM users');
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:
// db-connection.ts
class DatabaseConnection {
constructor(private config: DatabaseConfig) {
// Setup logic
}
query(sql: string): Promise<any> {
// Database operasyonları
}
}
// Single instance export edilir
export const db = new DatabaseConnection({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
});
// other-file.ts
import { db } from './db-connection';
await db.query('SELECT * FROM users');
// another-file.ts
import { db } from './db-connection'; // Aynı instance
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:
import { injectable, inject, container } from 'tsyringe';
@injectable()
class DatabaseConnection {
constructor(
@inject('DatabaseConfig') private config: DatabaseConfig
) {
// Setup logic
}
}
// Singleton olarak kaydet
container.registerSingleton(DatabaseConnection);
// Herhangi bir class'ta kullanım
@injectable()
class UserRepository {
constructor(private db: DatabaseConnection) {
// Otomatik olarak singleton instance alır
}
}
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:
class Logger {
private static instance: Logger;
private transports: Transport[] = [];
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
addTransport(transport: Transport): void {
this.transports.push(transport);
}
log(level: string, message: string): void {
this.transports.forEach(t => t.write(level, message));
}
}
// App başlangıcında bir kez initialize et
const logger = Logger.getInstance();
logger.addTransport(new ConsoleTransport());
logger.addTransport(new FileTransport('/var/log/app.log'));
// Konfigürasyon olmadan her yerde kullan
logger.log('info', 'Application started');
Feature flag manager:
class FeatureFlags {
private static instance: FeatureFlags;
private flags = new Map<string, boolean>();
private constructor() {}
static getInstance(): FeatureFlags {
if (!FeatureFlags.instance) {
FeatureFlags.instance = new FeatureFlags();
}
return FeatureFlags.instance;
}
async initialize(): Promise<void> {
// Remote config'den flag'leri yükle
const response = await fetch('/api/feature-flags');
const data = await response.json();
data.forEach((flag: any) => this.flags.set(flag.name, flag.enabled));
}
isEnabled(flagName: string): boolean {
return this.flags.get(flagName) ?? false;
}
}
// Başlangıçta initialize et
await FeatureFlags.getInstance().initialize();
// Her yerde kontrol et
if (FeatureFlags.getInstance().isEnabled('new-dashboard')) {
// Yeni dashboard'u göster
}
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:
// YAPMA: Test etmesi zor, implementation değiştiremezsin
class ApiClient {
private static instance: ApiClient;
private baseUrl = 'https://api.prod.com';
private constructor() {}
static getInstance(): ApiClient {
if (!ApiClient.instance) {
ApiClient.instance = new ApiClient();
}
return ApiClient.instance;
}
}
// YAP: Dependency'leri constructor parametresi olarak al
class ApiClient {
constructor(
private config: ApiConfig,
private httpClient: HttpClient
) {}
async get(endpoint: string): Promise<any> {
return this.httpClient.get(`${this.config.baseUrl}${endpoint}`);
}
}
// Production
const apiClient = new ApiClient(
{ baseUrl: 'https://api.prod.com' },
new HttpClient()
);
// Testing
const apiClient = new ApiClient(
{ baseUrl: 'http://localhost:3000' },
new MockHttpClient()
);
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:
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
function createLogger(level: LogLevel): Logger {
switch (level) {
case 'debug':
return new DebugLogger();
case 'info':
return new InfoLogger();
case 'warn':
return new WarnLogger();
case 'error':
return new ErrorLogger();
}
}
// Exhaustive checking ile type-safe
const logger = createLogger('debug');
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ı:
import { Function, Runtime, Duration, ILayerVersion } from 'aws-cdk-lib/aws-lambda';
import { IVpc, ISecurityGroup, SubnetType } from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
class LambdaFunctionFactory {
constructor(
private vpc: IVpc,
private layers: ILayerVersion[],
private securityGroup: ISecurityGroup,
private scope: Construct
) {}
createApiHandler(config: ApiHandlerConfig): Function {
return new Function(this.scope, config.id, {
vpc: this.vpc,
vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS },
securityGroups: [this.securityGroup],
layers: this.layers,
runtime: Runtime.NODEJS_20_X,
timeout: Duration.seconds(30),
memorySize: 1024,
...config,
});
}
createWorkerHandler(config: WorkerConfig): Function {
return new Function(this.scope, config.id, {
vpc: this.vpc,
vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS },
securityGroups: [this.securityGroup],
layers: this.layers,
runtime: Runtime.NODEJS_20_X,
timeout: Duration.minutes(15),
memorySize: 2048, // Worker'lar daha fazla memory'ye ihtiyaç duyar
...config,
});
}
createScheduledHandler(config: ScheduledConfig): Function {
return new Function(this.scope, config.id, {
vpc: this.vpc,
vpcSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS },
securityGroups: [this.securityGroup],
layers: this.layers,
runtime: Runtime.NODEJS_20_X,
timeout: Duration.minutes(5),
memorySize: 512, // Scheduled task'lar genelde daha hafif
...config,
});
}
}
// CDK stack'te kullanım
const factory = new LambdaFunctionFactory(
vpc,
[commonLayer, vendorLayer],
lambdaSecurityGroup,
this
);
const getUserHandler = factory.createApiHandler({
id: 'GetUserHandler',
handler: 'dist/handlers/get-user.handler',
environment: { TABLE_NAME: usersTable.tableName },
});
const processJobWorker = factory.createWorkerHandler({
id: 'ProcessJobWorker',
handler: 'dist/workers/process-job.handler',
environment: { QUEUE_URL: jobQueue.queueUrl },
});
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:
type LoggerConfig =
| { type: 'console'; colorize: boolean }
| { type: 'file'; path: string; maxSize: number }
| { type: 'cloudwatch'; logGroup: string; region: string };
function createLogger(config: LoggerConfig): Logger {
switch (config.type) {
case 'console':
return new ConsoleLogger(config.colorize);
case 'file':
return new FileLogger(config.path, config.maxSize);
case 'cloudwatch':
return new CloudWatchLogger(config.logGroup, config.region);
}
}
// TypeScript her tip için doğru property'leri garanti eder
const logger = createLogger({
type: 'file',
path: '/var/log/app.log',
maxSize: 10485760, // 10MB
// Buraya 'colorize' eklersek TypeScript hata verir
});
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:
type DatabaseConfig = PostgresConfig | MySQLConfig | SQLiteConfig;
interface PostgresConfig {
type: 'postgres';
socketPath: string;
database: string;
}
interface MySQLConfig {
type: 'mysql';
host: string;
port: number;
connectionLimit: number;
}
interface SQLiteConfig {
type: 'sqlite';
filename: string;
}
function createDatabase(config: DatabaseConfig): Database {
if (config.type === 'postgres') {
return new PostgresDatabase(config.socketPath, config.database);
}
if (config.type === 'mysql') {
return new MySQLDatabase(config.host, config.port, config.connectionLimit);
}
return new SQLiteDatabase(config.filename);
}
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:
// GEREKSIZ: Basit nesne için factory
function createUser(name: string, email: string): User {
return { name, email };
}
// DAHA İYİ: Object literal kullan
const user: User = { name: 'John', email: '[email protected]' };
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:
interface LambdaOptions {
handler: string;
runtime?: Runtime;
timeout?: number;
memorySize?: number;
environment?: Record<string, string>;
}
const fn = new Lambda({
handler: 'index.handler',
runtime: Runtime.NODEJS_20_X,
timeout: 30,
memorySize: 1024,
environment: { TABLE_NAME: 'users' },
});
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:
class StepFunctionsWorkflow {
private states: State[] = [];
private errorHandler?: ErrorHandler;
addState(name: string, state: State): this {
this.states.push({ name, ...state });
return this;
}
addParallelStates(name: string, branches: State[][]): this {
this.states.push({
name,
type: 'Parallel',
branches,
});
return this;
}
onError(handler: (builder: ErrorPathBuilder) => ErrorPathBuilder): this {
const errorPath = new ErrorPathBuilder();
this.errorHandler = handler(errorPath).build();
return this;
}
build(): StateMachine {
if (this.states.length === 0) {
throw new Error('Workflow en az bir state içermeli');
}
return {
states: this.states,
errorHandler: this.errorHandler,
startAt: this.states[0].name,
};
}
}
// Kullanım progressive disclosure gösterir
const workflow = new StepFunctionsWorkflow()
.addState('ValidateInput', {
type: 'Task',
resource: validateLambda.functionArn,
})
.addParallelStates('ProcessData', [
[
{
type: 'Task',
resource: scanVirusLambda.functionArn,
retry: [{ errorEquals: ['States.TaskFailed'], maxAttempts: 3 }],
},
],
[
{
type: 'Task',
resource: extractMetadataLambda.functionArn,
timeout: 60,
},
],
[
{
type: 'Task',
resource: generateThumbnailLambda.functionArn,
resultPath: '$.thumbnail',
},
],
])
.addState('StoreResults', {
type: 'Task',
resource: storeLambda.functionArn,
})
.onError((errorPath) =>
errorPath
.addState('LogError', {
type: 'Task',
resource: logErrorLambda.functionArn,
})
.addState('SendAlert', {
type: 'Task',
resource: alertLambda.functionArn,
})
)
.build();
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:
type RequiredFields = 'url' | 'method';
class RequestBuilder<TSet extends string = never> {
private config: Partial<RequestConfig> = {};
url(url: string): RequestBuilder<TSet | 'url'> {
this.config.url = url;
return this as any;
}
method(method: HttpMethod): RequestBuilder<TSet | 'method'> {
this.config.method = method;
return this as any;
}
headers(headers: Record<string, string>): this {
this.config.headers = headers;
return this;
}
timeout(ms: number): this {
this.config.timeout = ms;
return this;
}
// build() sadece tüm required field'lar set edildiğinde kullanılabilir
build(this: RequestBuilder<RequiredFields>): Request {
return new Request(this.config as RequestConfig);
}
}
// Compile error - required field'lar eksik
// const req = new RequestBuilder().build();
// OK - tüm required field'lar sağlanmış
const req = new RequestBuilder()
.url('https://api.example.com/users')
.method('GET')
.headers({ 'Authorization': 'Bearer token' })
.timeout(5000)
.build();
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:
class ApiClientBuilder {
private config: Partial<ApiClientConfig> = {};
// Ortak konfigürasyonlar için factory method'lar
static forProduction(apiKey: string): ApiClientBuilder {
return new ApiClientBuilder()
.withApiKey(apiKey)
.withTimeout(30000)
.withRetries(3)
.enableCaching()
.withBaseUrl('https://api.prod.com');
}
static forDevelopment(apiKey: string): ApiClientBuilder {
return new ApiClientBuilder()
.withApiKey(apiKey)
.withTimeout(60000)
.disableCaching()
.withVerboseLogging()
.withBaseUrl('https://api.dev.com');
}
withApiKey(key: string): this {
this.config.apiKey = key;
return this;
}
withTimeout(ms: number): this {
this.config.timeout = ms;
return this;
}
withRetries(count: number): this {
this.config.retries = count;
return this;
}
enableCaching(): this {
this.config.cacheEnabled = true;
return this;
}
disableCaching(): this {
this.config.cacheEnabled = false;
return this;
}
withVerboseLogging(): this {
this.config.logLevel = 'debug';
return this;
}
withBaseUrl(url: string): this {
this.config.baseUrl = url;
return this;
}
build(): ApiClient {
if (!this.config.apiKey) {
throw new Error('API key gerekli');
}
if (!this.config.baseUrl) {
throw new Error('Base URL gerekli');
}
return new ApiClient(this.config as ApiClientConfig);
}
}
// Mantıklı default'larla hızlı başlangıç
const prodClient = ApiClientBuilder.forProduction(process.env.API_KEY);
// Ya da sıfırdan customize et
const customClient = new ApiClientBuilder()
.withApiKey(process.env.API_KEY)
.withBaseUrl('https://api.custom.com')
.withTimeout(45000)
.withRetries(5)
.build();
Builder’ı Ne Zaman Kullanmamak Gerekir
Basit nesneler için builder oluşturma:
// GEREKSIZ
new UserBuilder()
.withName('John')
.withEmail('[email protected]')
.build();
// DAHA İYİ
const user: User = { name: 'John', email: '[email protected]' };
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:
class Prototype {
clone(): this {
return Object.create(this);
}
}
class ConcretePrototype extends Prototype {
constructor(public data: string) {
super();
}
}
const original = new ConcretePrototype('data');
const clone = original.clone();
Modern Yaklaşım: Object Spread
Object spread shallow cloning’i temiz bir şekilde hallediyor:
const original = {
name: 'John',
age: 30,
address: { city: 'NYC', zip: '10001' },
};
// Shallow clone
const clone = { ...original };
// Clone'u değiştir - original'in primitive property'lerini etkilemez
clone.name = 'Jane';
console.log(original.name); // Hala 'John'
// Ama nested object'ler paylaşılır
clone.address.city = 'LA';
console.log(original.address.city); // Aynı zamanda 'LA'
Deep cloning için structuredClone() kullan (Node.js 17+ ve Node 18 LTS’den beri yaygın olarak mevcut, modern browser’larda mevcut):
const original = {
name: 'John',
age: 30,
address: { city: 'NYC', zip: '10001' },
metadata: {
tags: ['developer', 'typescript'],
preferences: { theme: 'dark' },
},
};
// Deep clone
const deepClone = structuredClone(original);
// Nested property'leri değiştir - original'i etkilemez
deepClone.address.city = 'LA';
deepClone.metadata.tags.push('react');
console.log(original.address.city); // Hala 'NYC'
console.log(original.metadata.tags); // Hala ['developer', 'typescript']
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:
const [state, setState] = useState({
count: 0,
items: ['elma', 'muz'],
user: { name: 'John', role: 'admin' },
});
// Değişiklikle shallow clone
setState(prev => ({ ...prev, count: prev.count + 1 }));
// Nested yapılar için deep clone
setState(prev => ({
...prev,
items: [...prev.items, 'kiraz'],
user: { ...prev.user, role: 'user' },
}));
Prototype Pattern Hala Ne Zaman İlgili
Test data builder’lar prototype benzeri cloning’den faydalanır:
class UserBuilder {
private template: Partial<User> = {
role: 'user',
verified: false,
createdAt: new Date(),
preferences: { theme: 'light', notifications: true },
};
fromTemplate(template: Partial<User>): this {
this.template = { ...this.template, ...template };
return this;
}
asAdmin(): this {
return this.fromTemplate({
role: 'admin',
permissions: ['read', 'write', 'delete'],
});
}
asVerified(): this {
return this.fromTemplate({ verified: true });
}
withEmail(email: string): this {
return this.fromTemplate({ email });
}
build(): User {
return {
id: crypto.randomUUID(),
email: `user-${Date.now()}@example.com`,
...this.template,
} as User;
}
}
// Base admin template oluştur
const adminTemplate = new UserBuilder().asAdmin().asVerified();
// Farklı testler için clone et ve customize et
const admin1 = adminTemplate.fromTemplate({ email: '[email protected]' }).build();
const admin2 = adminTemplate.fromTemplate({ email: '[email protected]' }).build();
// Her biri admin default'larına sahip ama unique email ve ID
Bu pattern benzer nesnelere ufak varyasyonlarla ihtiyaç duyduğun test suite’lerinde parlıyor.
Configuration Template’leri
Configuration object’leri cloning’den faydalanır:
// Base Lambda configuration
const baseLambdaConfig = {
runtime: Runtime.NODEJS_20_X,
timeout: Duration.seconds(30),
memorySize: 1024,
environment: {
LOG_LEVEL: 'info',
REGION: 'us-east-1',
},
vpc: sharedVpc,
securityGroups: [lambdaSecurityGroup],
layers: [commonLayer],
};
// Specific handler'lar için clone et ve customize et
const apiHandler = new Function(this, 'ApiHandler', {
...baseLambdaConfig,
handler: 'dist/api/handler.handler',
environment: {
...baseLambdaConfig.environment,
TABLE_NAME: usersTable.tableName,
},
});
const workerHandler = new Function(this, 'WorkerHandler', {
...baseLambdaConfig,
handler: 'dist/worker/handler.handler',
timeout: Duration.minutes(15),
memorySize: 2048,
environment: {
...baseLambdaConfig.environment,
QUEUE_URL: jobQueue.queueUrl,
},
});
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:
// YAPMA: Singleton service
class EmailService {
private static instance: EmailService;
static getInstance() { /* ... */ }
}
// YAP: Injectable service
@injectable()
class EmailService {
constructor(
@inject('MailProvider') private mail: MailProvider,
@inject('Config') private config: EmailConfig
) {}
}
// DI container lifecycle'ı yönetir
container.bind(EmailService).toSelf().inSingletonScope();
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:
// GEREKSIZ
function createUser(name: string, email: string): User {
return { name, email };
}
// DAHA İYİ
const user: User = { name: 'John', email: '[email protected]' };
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:
// GEREKSIZ
new UserBuilder()
.withName('John')
.withEmail('[email protected]')
.build();
// DAHA İYİ
const user: User = { name: 'John', email: '[email protected]' };
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:
// Discriminated union'lardan yararlan
type DatabaseConfig =
| { type: 'postgres'; connectionString: string }
| { type: 'mysql'; host: string; port: number }
| { type: 'sqlite'; filename: string };
function createDatabase(config: DatabaseConfig): Database {
// TypeScript her branch'te type'ları daraltır
switch (config.type) {
case 'postgres':
return new PostgresDatabase(config.connectionString);
case 'mysql':
return new MySQLDatabase(config.host, config.port);
case 'sqlite':
return new SQLiteDatabase(config.filename);
}
}
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:
// SSR için: Request başına instance yarat
export function createRequestContext(req: Request): RequestContext {
return new RequestContext(req);
}
// Middleware request başına context yaratır
app.use((req, res, next) => {
req.context = createRequestContext(req);
next();
});
// Her request isolated context'e sahip
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ı:
// GEREKSIZ: Shallow işe yararken deep clone
const user = structuredClone(originalUser);
// DAHA İYİ: Shallow clone
const user = { ...originalUser };
// GEREKLİ: Nested object'leri değiştirirken deep clone
const config = structuredClone(baseConfig);
config.database.host = 'localhost'; // baseConfig'i etkilemez
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.
Serideki tüm yazılar
İlgili yazılar
Decorator, Adapter, Facade, Composite ve Proxy patternlerinin React ve TypeScript'te nasıl evrildiğini keşfedin. HOC'ların ne zaman hook'lara yol verdiğini, adapterlerin third-party API'ları nasıl izole ettiğini ve facade'ların karmaşıklığı nasıl basitleştirdiğini öğrenin.
SOLID prensiplerininin modern JavaScript geliştirmede nasıl uygulanacağını öğrenin. TypeScript, React hooks ve fonksiyonel pattern'ler ile pratik örnekler - ayrıca ne zaman kullanmalı, ne zaman gereksiz.
AWS CDK projelerinde service-based, domain-based, feature-based veya layer-based organizasyon patternlerini ne zaman kullanacağını öğren. Karar çerçeveleri, çalışan örnekler ve sürdürülebilir infrastructure code için migration stratejileri.
Domain-Driven Design'a kapsamlı giriş - temel kavramlar, yapı taşları, stratejik desenler ve DDD'yi yazılım geliştirmede ne zaman ve nasıl uygulayacağınıza dair pratik rehber
Kural tabanlı chatbot'lardan otonom AI agent'larına mimari evrimi keşfet. ReAct, Plan-and-Execute ve çoklu-agent desenleri TypeScript implementasyonları ve pratik geçiş stratejileriyle öğren.