← Kursa Dön
📄 Text · 30 min

JavaScript/TypeScript Patterns

Tasarım Kalıpları Neden Önemli?

Bir şehir plancısını düşün: her yeni şehir farklıdır ama belirli kalıplar tekrar eder — kavşak düzeni, otopark yapısı, su tahliye sistemi. Bu kalıplar yüzyıllar içinde denenmiş, başarısız olanlar elenmiş, işe yarayanlar kalmış. Her şehir plancısı bu kalıpları bilir ve projesine uyarlar.

Yazılımda da durum aynı. Karşılaşacağın sorunların çoğu daha önce birisi tarafından çözülmüş. Bu çözümler design pattern (tasarım kalıbı) olarak belgelenmiş. Bu derste JavaScript/TypeScript dünyasına özgü, her gün karşılaşacağın pratik kalıpları inceleyeceğiz: hata yönetimi, repository pattern, dependency injection, event bus ve state management.


Error Handling Patterns: Hatayla Doğru Yaşamak

Hatalar kaçınılmaz — ağ kesilir, kullanıcı beklenmedik girdi verir, sunucu cevap vermez. Önemli olan hataları zarif bir şekilde yönetmek.

Result Pattern: Hata veya Sonuç

Go ve Rust dillerinden esinlenen bu pattern, fonksiyonun ya başarılı sonuç ya da hata döndürmesini garanti eder — exception fırlatmak yerine:

// Result tipi tanımla
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

// Yardımcı fonksiyonlar
function ok<T>(data: T): Result<T, never> {
  return { success: true, data };
}

function err<E>(error: E): Result<never, E> {
  return { success: false, error };
}

// Kullanım: exception fırlatmak yerine Result döndür
function parseJSON<T>(input: string): Result<T, string> {
  try {
    const data = JSON.parse(input);
    return ok(data);
  } catch {
    return err(`Geçersiz JSON: "${input.substring(0, 50)}..."`);
  }
}

function validateAge(age: unknown): Result<number, string> {
  if (typeof age !== "number") return err("Yaş sayı olmalı");
  if (age < 0 || age > 150) return err("Yaş 0-150 arasında olmalı");
  if (!Number.isInteger(age)) return err("Yaş tam sayı olmalı");
  return ok(age);
}

// Kullanım
const parsed = parseJSON<{ name: string }>('{"name": "Ali"}');
if (parsed.success) {
  console.log(parsed.data.name); // TypeScript data'nın tipini biliyor ✅
} else {
  console.error(parsed.error);    // TypeScript error'ın tipini biliyor ✅
}

// Zincirleme (chaining)
function processUserInput(input: string): Result<{ name: string; age: number }, string> {
  const parsed = parseJSON<{ name: string; age: unknown }>(input);
  if (!parsed.success) return parsed;

  const ageResult = validateAge(parsed.data.age);
  if (!ageResult.success) return ageResult;

  return ok({ name: parsed.data.name, age: ageResult.data });
}

Custom Error Classes

// Hata hiyerarşisi oluştur
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
    public readonly isOperational: boolean = true
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }
}

class ValidationError extends AppError {
  constructor(
    message: string,
    public readonly field: string
  ) {
    super(message, "VALIDATION_ERROR", 400);
  }
}

class NotFoundError extends AppError {
  constructor(resource: string, id: string | number) {
    super(`${resource} bulunamadı: ${id}`, "NOT_FOUND", 404);
  }
}

class AuthenticationError extends AppError {
  constructor(message: string = "Kimlik doğrulama başarısız") {
    super(message, "AUTH_ERROR", 401);
  }
}

// Kullanım
function getUser(id: number): User {
  const user = db.findById(id);
  if (!user) throw new NotFoundError("Kullanıcı", id);
  return user;
}

// Global error handler
function handleError(error: unknown) {
  if (error instanceof AppError) {
    if (error.isOperational) {
      // Beklenen hata — kullanıcıya mesaj göster
      console.warn(`[${error.code}] ${error.message}`);
      return { status: error.statusCode, message: error.message };
    }
  }
  // Beklenmeyen hata — logla, generic mesaj göster
  console.error("Beklenmeyen hata:", error);
  return { status: 500, message: "Bir hata oluştu" };
}

💡 İpucu: isOperational bayrağı önemli: true = beklenen hata (kullanıcı yanlış input girdi), false = programlama hatası (bug). İkincisi için alerting sistemi tetiklenmelidir.


Repository Pattern: Veri Erişimini Soyutlama

Repository pattern, veri erişim katmanını iş mantığından ayırır. Bir kütüphaneci gibi düşün: kitabın nerede olduğunu (hangi raf, hangi sıra) bilir, sen sadece "şu kitabı ver" dersin.

// Soyut interface — veri kaynağı belirsiz
interface UserRepository {
  findById(id: number): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  findAll(filters?: UserFilters): Promise<User[]>;
  create(data: CreateUserDto): Promise<User>;
  update(id: number, data: UpdateUserDto): Promise<User>;
  delete(id: number): Promise<boolean>;
}

// Somut implementasyon 1: PostgreSQL
class PostgresUserRepository implements UserRepository {
  constructor(private db: Database) {}

  async findById(id: number): Promise<User | null> {
    const row = await this.db.query(
      "SELECT * FROM users WHERE id = $1",
      [id]
    );
    return row ? this.mapToUser(row) : null;
  }

  async findByEmail(email: string): Promise<User | null> {
    const row = await this.db.query(
      "SELECT * FROM users WHERE email = $1",
      [email]
    );
    return row ? this.mapToUser(row) : null;
  }

  async create(data: CreateUserDto): Promise<User> {
    const row = await this.db.query(
      "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *",
      [data.name, data.email]
    );
    return this.mapToUser(row);
  }

  // ... diğer metodlar

  private mapToUser(row: any): User {
    return { id: row.id, name: row.name, email: row.email };
  }
}

// Somut implementasyon 2: In-Memory (test için)
class InMemoryUserRepository implements UserRepository {
  private users: User[] = [];
  private nextId = 1;

  async findById(id: number): Promise<User | null> {
    return this.users.find(u => u.id === id) || null;
  }

  async findByEmail(email: string): Promise<User | null> {
    return this.users.find(u => u.email === email) || null;
  }

  async create(data: CreateUserDto): Promise<User> {
    const user = { ...data, id: this.nextId++ };
    this.users.push(user);
    return user;
  }

  // ... diğer metodlar
}

Service Katmanı

// UserService — veri kaynağından BAĞIMSIZ
class UserService {
  // Repository'yi dışarıdan alır (Dependency Injection)
  constructor(private userRepo: UserRepository) {}

  async registerUser(data: CreateUserDto): Promise<User> {
    // İş mantığı burada
    const existing = await this.userRepo.findByEmail(data.email);
    if (existing) throw new ValidationError("Email zaten kayıtlı", "email");

    const user = await this.userRepo.create(data);
    return user;
  }

  async getUser(id: number): Promise<User> {
    const user = await this.userRepo.findById(id);
    if (!user) throw new NotFoundError("Kullanıcı", id);
    return user;
  }
}

// Production'da
const prodService = new UserService(new PostgresUserRepository(db));

// Test'te
const testService = new UserService(new InMemoryUserRepository());

Bu pattern'ın gücü: UserService'in içinde hiçbir SQL, hiçbir veritabanı detayı yok. Yarın PostgreSQL'den MongoDB'ye geçsen, sadece yeni bir MongoUserRepository yazarsın — UserService hiç değişmez.


Dependency Injection: Bağımlılıkları Dışarıdan Ver

DI, bir sınıfın ihtiyaç duyduğu bağımlılıkları kendi oluşturmak yerine dışarıdan alması prensibi. Bir aşçının malzemeleri kendi yetiştirmek yerine tedarikçiden alması gibi.

DI Olmadan ve DI İle

// ❌ DI yok: Bağımlılıklar sınıf içinde oluşturuluyor
class OrderService {
  private db = new PostgresDatabase();      // Sıkı bağlı
  private email = new SendGridEmailService(); // Sıkı bağlı
  private logger = new FileLogger();          // Sıkı bağlı

  async createOrder(data: OrderDto) {
    const order = await this.db.insert("orders", data);
    await this.email.send(data.userEmail, "Sipariş onayı");
    this.logger.info(`Sipariş: ${order.id}`);
    return order;
  }
}
// Test etmek imkansız — gerçek veritabanı ve email servisi lazım!
// ✅ DI ile: Bağımlılıklar dışarıdan enjekte ediliyor
interface Database {
  insert(table: string, data: any): Promise<any>;
}
interface EmailService {
  send(to: string, subject: string): Promise<void>;
}
interface Logger {
  info(message: string): void;
}

class OrderService {
  constructor(
    private db: Database,
    private email: EmailService,
    private logger: Logger
  ) {}

  async createOrder(data: OrderDto) {
    const order = await this.db.insert("orders", data);
    await this.email.send(data.userEmail, "Sipariş onayı");
    this.logger.info(`Sipariş: ${order.id}`);
    return order;
  }
}

// Production
const orderService = new OrderService(
  new PostgresDatabase(),
  new SendGridEmailService(),
  new FileLogger()
);

// Test — her bağımlılık mock
const testService = new OrderService(
  { insert: jest.fn().mockResolvedValue({ id: "1" }) },
  { send: jest.fn().mockResolvedValue(undefined) },
  { info: jest.fn() }
);

Basit DI Container

// Minimal DI container
class Container {
  private services = new Map<string, any>();

  register<T>(key: string, factory: () => T): void {
    this.services.set(key, factory);
  }

  resolve<T>(key: string): T {
    const factory = this.services.get(key);
    if (!factory) throw new Error(`Service bulunamadı: ${key}`);
    return factory();
  }
}

// Kayıt
const container = new Container();
container.register("db", () => new PostgresDatabase());
container.register("email", () => new SendGridEmailService());
container.register("logger", () => new ConsoleLogger());
container.register("orderService", () =>
  new OrderService(
    container.resolve("db"),
    container.resolve("email"),
    container.resolve("logger")
  )
);

// Kullanım
const orderService = container.resolve<OrderService>("orderService");

Event Bus: Bileşenler Arası İletişim

Event Bus, farklı modüllerin birbirini doğrudan tanımadan iletişim kurmasını sağlar. Bir radyo istasyonu gibi düşün: herkes aynı frekansı dinler, mesaj gönderen kimin dinlediğini bilmez.

type EventHandler<T = any> = (data: T) => void;

class EventBus {
  private handlers = new Map<string, Set<EventHandler>>();

  // Event dinle
  on<T>(event: string, handler: EventHandler<T>): () => void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler);

    // Cleanup fonksiyonu döndür
    return () => {
      this.handlers.get(event)?.delete(handler);
    };
  }

  // Tek seferlik dinle
  once<T>(event: string, handler: EventHandler<T>): () => void {
    const wrappedHandler: EventHandler<T> = (data) => {
      handler(data);
      this.handlers.get(event)?.delete(wrappedHandler);
    };
    return this.on(event, wrappedHandler);
  }

  // Event yayınla
  emit<T>(event: string, data: T): void {
    const eventHandlers = this.handlers.get(event);
    if (eventHandlers) {
      eventHandlers.forEach(handler => handler(data));
    }
  }

  // Tüm listener'ları temizle
  clear(event?: string): void {
    if (event) {
      this.handlers.delete(event);
    } else {
      this.handlers.clear();
    }
  }
}

// Kullanım
const bus = new EventBus();

// Farklı modüller birbirinden habersiz
// Auth modülü
bus.on("user:login", (user: { id: number; name: string }) => {
  console.log(`Hoş geldin, ${user.name}`);
});

// Analytics modülü
bus.on("user:login", (user: { id: number }) => {
  analytics.track("login", { userId: user.id });
});

// Notification modülü
bus.on("order:created", (order: { id: string; total: number }) => {
  sendPushNotification(`Sipariş #${order.id} oluşturuldu!`);
});

// Event yayınla — kim dinliyor bilmek zorunda değilsin
bus.emit("user:login", { id: 1, name: "Ali" });
bus.emit("order:created", { id: "ORD-123", total: 250 });

// Cleanup — memory leak önleme
const unsubscribe = bus.on("notification", handleNotification);
// Component unmount olduğunda:
unsubscribe();

Type-Safe Event Bus

// Event tiplerini merkezi olarak tanımla
interface AppEvents {
  "user:login": { id: number; name: string };
  "user:logout": { id: number };
  "order:created": { orderId: string; total: number };
  "order:cancelled": { orderId: string; reason: string };
  "theme:changed": { theme: "light" | "dark" };
}

class TypedEventBus<TEvents extends Record<string, any>> {
  private handlers = new Map<string, Set<Function>>();

  on<K extends keyof TEvents>(
    event: K,
    handler: (data: TEvents[K]) => void
  ): () => void {
    const key = event as string;
    if (!this.handlers.has(key)) {
      this.handlers.set(key, new Set());
    }
    this.handlers.get(key)!.add(handler);
    return () => { this.handlers.get(key)?.delete(handler); };
  }

  emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
    const handlers = this.handlers.get(event as string);
    handlers?.forEach(handler => handler(data));
  }
}

const bus = new TypedEventBus<AppEvents>();

bus.on("user:login", (data) => {
  // data otomatik { id: number; name: string } ✅
  console.log(data.name);
});

bus.emit("user:login", { id: 1, name: "Ali" });  // ✅
bus.emit("user:login", { id: 1 });                 // ❌ name eksik
bus.emit("unknown:event", {});                       // ❌ event tanımlı değil

State Management: Durum Yönetimi

Uygulama durumunu (state) yönetmek karmaşık uygulamaların en büyük zorluklarından biri. Redux'un popülerleştirdiği unidirectional data flow (tek yönlü veri akışı) pattern'ı:

// Minimal Store — Redux benzeri
type Reducer<S, A> = (state: S, action: A) => S;
type Listener = () => void;

class Store<S, A> {
  private state: S;
  private listeners: Set<Listener> = new Set();

  constructor(
    private reducer: Reducer<S, A>,
    initialState: S
  ) {
    this.state = initialState;
  }

  getState(): S {
    return this.state;
  }

  dispatch(action: A): void {
    this.state = this.reducer(this.state, action);
    this.listeners.forEach(listener => listener());
  }

  subscribe(listener: Listener): () => void {
    this.listeners.add(listener);
    return () => { this.listeners.delete(listener); };
  }
}

// Kullanım: Todo uygulaması
interface TodoState {
  todos: Array<{ id: number; text: string; done: boolean }>;
  filter: "all" | "active" | "completed";
}

type TodoAction =
  | { type: "ADD_TODO"; text: string }
  | { type: "TOGGLE_TODO"; id: number }
  | { type: "DELETE_TODO"; id: number }
  | { type: "SET_FILTER"; filter: TodoState["filter"] };

let nextId = 1;

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case "ADD_TODO":
      return {
        ...state,
        todos: [...state.todos, { id: nextId++, text: action.text, done: false }],
      };

    case "TOGGLE_TODO":
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.id ? { ...todo, done: !todo.done } : todo
        ),
      };

    case "DELETE_TODO":
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.id),
      };

    case "SET_FILTER":
      return { ...state, filter: action.filter };

    default:
      return state;
  }
}

// Store oluştur
const store = new Store(todoReducer, { todos: [], filter: "all" as const });

// State değişikliklerini dinle
store.subscribe(() => {
  const state = store.getState();
  console.log(`${state.todos.length} todo, filtre: ${state.filter}`);
});

// Action'lar gönder
store.dispatch({ type: "ADD_TODO", text: "TypeScript öğren" });
store.dispatch({ type: "ADD_TODO", text: "Test yaz" });
store.dispatch({ type: "TOGGLE_TODO", id: 1 });
store.dispatch({ type: "SET_FILTER", filter: "active" });

Bu pattern'ın gücü:

  • Tek kaynak: Tüm state tek bir yerde

  • Öngörülebilir: Aynı action → aynı state değişikliği

  • Debug: Her action kaydedilebilir (time-travel debugging)

  • Test: Reducer saf fonksiyon — giriş/çıkış testi kolay


Yaygın Hatalar

1. Error Swallowing — Hatayı Yutmak

// ❌ Hata yakalanıyor ama bir şey yapılmıyor
try {
  await saveData();
} catch (e) {
  // Sessizce yutuldu — hata olduğunu KIMSE bilmiyor
}

// ✅ En azından logla
try {
  await saveData();
} catch (error) {
  console.error("Veri kaydetme hatası:", error);
  throw error; // Veya Result pattern ile döndür
}

2. Event Bus Memory Leak

// ❌ Listener temizlenmedi — component her mount'ta yeni listener ekler
function setupComponent() {
  bus.on("update", handleUpdate);
  // Component unmount olunca listener kalıyor!
}

// ✅ Cleanup
function setupComponent() {
  const unsubscribe = bus.on("update", handleUpdate);
  // Cleanup
  return () => unsubscribe();
}

3. Doğrudan State Mutasyonu

// ❌ Reducer'da state doğrudan değiştirildi
function badReducer(state, action) {
  state.todos.push(newTodo); // MUTASYON! ❌
  return state;
}

// ✅ Yeni nesne döndür (immutable update)
function goodReducer(state, action) {
  return {
    ...state,
    todos: [...state.todos, newTodo],
  };
}

Özet

  • Result Pattern: Exception fırlatmak yerine { success, data/error } döndür. Hata yönetimini tip güvenli ve açık yap.

  • Custom Error Classes: Hata hiyerarşisi oluştur — AppError, ValidationError, NotFoundError. isOperational ile beklenen/beklenmeyen hataları ayır.

  • Repository Pattern: Veri erişimini soyutla. İş mantığı veritabanı detaylarından bağımsız olmalı. Test için in-memory implementasyon.

  • Dependency Injection: Bağımlılıkları dışarıdan ver — test edilebilirlik, esneklik, gevşek bağlantı.

  • Event Bus: Modüller arası iletişim — publish/subscribe. Tip güvenli versiyonunu kullan, cleanup'ı unutma.

  • State Management: Tek yönlü veri akışı — action → reducer → yeni state. Öngörülebilir, test edilebilir, debug edilebilir.