Skip to content
~/sph.sh

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'ı:

typescript
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ımconst 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:

typescript
// db-connection.tsclass DatabaseConnection {  constructor(private config: DatabaseConfig) {    // Setup logic  }
  query(sql: string): Promise<any> {    // Database operasyonları  }}
// Single instance export edilirexport const db = new DatabaseConnection({  host: process.env.DB_HOST,  port: parseInt(process.env.DB_PORT),});
// other-file.tsimport { db } from './db-connection';await db.query('SELECT * FROM users');
// another-file.tsimport { 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:

typescript
import { injectable, inject, container } from 'tsyringe';
@injectable()class DatabaseConnection {  constructor(    @inject('DatabaseConfig') private config: DatabaseConfig  ) {    // Setup logic  }}
// Singleton olarak kaydetcontainer.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:

typescript
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 etconst logger = Logger.getInstance();logger.addTransport(new ConsoleTransport());logger.addTransport(new FileTransport('/var/log/app.log'));
// Konfigürasyon olmadan her yerde kullanlogger.log('info', 'Application started');

Feature flag manager:

typescript
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 etawait FeatureFlags.getInstance().initialize();
// Her yerde kontrol etif (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:

typescript
// YAPMA: Test etmesi zor, implementation değiştiremezsinclass 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 alclass ApiClient {  constructor(    private config: ApiConfig,    private httpClient: HttpClient  ) {}
  async get(endpoint: string): Promise<any> {    return this.httpClient.get(`${this.config.baseUrl}${endpoint}`);  }}
// Productionconst apiClient = new ApiClient(  { baseUrl: 'https://api.prod.com' },  new HttpClient());
// Testingconst 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:

typescript
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-safeconst 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ı:

typescript
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ımconst 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:

typescript
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 ederconst 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:

typescript
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:

typescript
// GEREKSIZ: Basit nesne için factoryfunction createUser(name: string, email: string): User {  return { name, email };}
// DAHA İYİ: Object literal kullanconst 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:

typescript
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:

typescript
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österirconst 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:

typescript
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:

typescript
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 etconst 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:

typescript
// GEREKSIZnew 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:

typescript
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:

typescript
const original = {  name: 'John',  age: 30,  address: { city: 'NYC', zip: '10001' },};
// Shallow cloneconst clone = { ...original };
// Clone'u değiştir - original'in primitive property'lerini etkilemezclone.name = 'Jane';console.log(original.name); // Hala 'John'
// Ama nested object'ler paylaşılırclone.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):

typescript
const original = {  name: 'John',  age: 30,  address: { city: 'NYC', zip: '10001' },  metadata: {    tags: ['developer', 'typescript'],    preferences: { theme: 'dark' },  },};
// Deep cloneconst deepClone = structuredClone(original);
// Nested property'leri değiştir - original'i etkilemezdeepClone.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:

typescript
const [state, setState] = useState({  count: 0,  items: ['elma', 'muz'],  user: { name: 'John', role: 'admin' },});
// Değişiklikle shallow clonesetState(prev => ({ ...prev, count: prev.count + 1 }));
// Nested yapılar için deep clonesetState(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:

typescript
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şturconst adminTemplate = new UserBuilder().asAdmin().asVerified();
// Farklı testler için clone et ve customize etconst 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:

typescript
// Base Lambda configurationconst 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 etconst 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:

typescript
// YAPMA: Singleton serviceclass 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önetircontainer.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:

typescript
// GEREKSIZfunction 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:

typescript
// GEREKSIZnew 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:

typescript
// Discriminated union'lardan yararlantype 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:

typescript
// SSR için: Request başına instance yaratexport function createRequestContext(req: Request): RequestContext {  return new RequestContext(req);}
// Middleware request başına context yaratırapp.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ı:

typescript
// GEREKSIZ: Shallow işe yararken deep cloneconst user = structuredClone(originalUser);
// DAHA İYİ: Shallow cloneconst user = { ...originalUser };
// GEREKLİ: Nested object'leri değiştirirken deep cloneconst config = structuredClone(baseConfig);config.database.host = 'localhost'; // baseConfig'i etkilemez

Anahtar Çıkarımlar

  1. 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.

  2. Ö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ç.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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ç.

  8. 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.

İlerleme1/4 yazı tamamlandı

İlgili Yazılar