İçeriğe atla
Ayhan Sipahi Ayhan Sipahi

Factory Pattern'inin Ölümü: Saf Fonksiyonlarla Node.js Kodumuzun 40%'ını Nasıl Sildik

Node.js microservice'lerimizden factory, service ve dependency injection'ı çıkarmak kodun %40'ını ve bug'ların çoğunu sildi; fonksiyonlar sınıfları neden geçti?

Hiçbir İşe Yaramayan 847 Satırlık Service

20 dakika sürmesi gereken bir ödeme işleme bug’ı, tek bir validation kuralını değiştirmek için 847 satır “kurumsal mimari”de 3 saat harcatabilir.

Suçlu kalıp: bir factory, dependency injection, 12 farklı interface ve bir Java developer’ı sevinçle ağlatacak kadar soyutlama içeren PaymentService aşırı mühendislik şaheseri. Event-driven Lambda mimarisinde pure function’lar bu tür class yapılarını geçer: daha az boilerplate, daha kolay test, daha hızlı iterasyon.

// "Temiz mimari" adına yarattığımız canavar
class PaymentServiceFactory {
  static create(config: PaymentConfig): PaymentService {
    const validator = new PaymentValidator(
      new CreditCardValidator(),
      new BillingAddressValidator(),
      new FraudDetectionValidator(config.fraudConfig)
    );

    const processor = new PaymentProcessor(
      new StripeAdapter(config.stripeConfig),
      new PayPalAdapter(config.paypalConfig),
      new BankAdapter(config.bankConfig)
    );

    const logger = new PaymentLogger(
      new CloudWatchLogger(),
      new DatadogLogger()
    );

    return new PaymentService(validator, processor, logger);
  }
}

class PaymentService implements IPaymentService {
  constructor(
    private validator: IPaymentValidator,
    private processor: IPaymentProcessor,
    private logger: IPaymentLogger
  ) {}

  async processPayment(request: PaymentRequest): Promise<PaymentResult> {
    // 15 satırlık fonksiyon olabilecek
    // 200+ satır orkestrasyon logic'i
  }
}

Düzeltmeye çalıştığım bug? Basit bir validation: “Kredi kartı numaraları boşluk içermemeli.”

Saf fonksiyonlar ve event-driven mimariye geçişten sonra, aynı fonksiyonalite şöyle olur:

// Sonra: Basit, test edilebilir, debug edilebilir
export const validateCreditCard = (cardNumber: string): ValidationResult => {
  if (!cardNumber) return { valid: false, error: 'Kart numarası gerekli' };
  if (cardNumber.includes(' ')) return { valid: false, error: 'Kart numarasından boşlukları kaldır' };
  if (!luhnCheck(cardNumber)) return { valid: false, error: 'Geçersiz kart numarası' };

  return { valid: true };
};

export const processPayment = async (event: PaymentEvent): Promise<void> => {
  const validation = validateCreditCard(event.cardNumber);
  if (!validation.valid) {
    await publishEvent('payment.failed', { ...event, error: validation.error });
    return;
  }

  const result = await chargeCard(event);
  await publishEvent('payment.processed', result);
};

Bug fix süresi: 3 saat → 15 dakika. Kod satırı: 847 → 89. Test karmaşıklığı: 156 test case → 12 test case.

Bu yazı, factory’leri, service’leri ve dependency injection container’larını ortadan kaldırma sürecini ve Node.js uygulamalarının neden daha hızlı, daha güvenilir ve çalışması sonsuz derecede daha keyifli hale geldiğini ele alır.

Node.js’in Java-laşması

Java ve C# geçmişinden gelen ekipler, o ekosistemlerde mantıklı olan enterprise pattern’leri getirir:

  • “Test edilebilirlik” için Dependency Injection
  • “Esneklik” için Factory pattern’leri
  • “Kaygıların ayrılması” için Service katmanları
  • “Veri soyutlaması” için Repository pattern’leri

Zamanla, Node.js kod tabanı idiomatic JavaScript’ten çok Spring Boot’a benzer hale gelir. Her basit operasyon birden fazla soyutlama katmanında gezinmeyi gerektirir:

// Basit bir siparişi işlemek için anlamanız gerekenler:

// 1. OrderServiceFactory (hangi OrderService implementation'ını kullanacağına karar verir)
class OrderServiceFactory {
  static create(): IOrderService {
    return new OrderService(
      InventoryServiceFactory.create(),
      PaymentServiceFactory.create(),
      ShippingServiceFactory.create(),
      NotificationServiceFactory.create()
    );
  }
}

// 2. OrderService (diğer service'leri orkestraya eder)
class OrderService implements IOrderService {
  constructor(
    private inventory: IInventoryService,
    private payment: IPaymentService,
    private shipping: IShippingService,
    private notification: INotificationService
  ) {}

  async processOrder(order: Order): Promise<OrderResult> {
    // 150 satır service orkestrasyonu
  }
}

// 3. Her inject edilen service'in kendi factory'si ve dependency'leri vardı
// 4. Integration testleri 47 farklı dependency'yi mock etmeyi gerektiriyordu
// 5. Basit değişiklikler 12 farklı dosyada cascade etkisi yapıyordu

Dönüm noktası: “Sipariş onay e-postası gönder” feature’ını eklemek birden fazla dosya ve service’te değişiklik gerektirir. Dependency graph’ını anlamak yeni takım üyeleri için önemli bir adaptasyon zorluğuna dönüşür.

Event-Driven Aydınlanma

Çıkış noktası, “service”lerin çoğunun aslında kılık değiştirmiş event handler’lar olduğunun fark edilmesiyle gelir.

Synchronous service çağrıları yerine:

// Synchronous coupling kabusu
await orderService.processOrder(orderData);
await inventoryService.updateStock(orderData.items);
await paymentService.chargeCard(orderData.payment);
await shippingService.scheduleDelivery(orderData.shipping);
await notificationService.sendConfirmation(orderData.customer);

Aynı flow’u event’ler olarak modelleyebilirdik:

// Event-driven decoupling mutluluğu
await publishEvent('order.created', orderData);

// Ayrı handler'lar bağımsız tepki verir:
// - inventory-handler stock'u günceller
// - payment-handler ödemeyi işler
// - shipping-handler teslimatı programlar
// - notification-handler onay gönderir

İçgörü: Her operasyon bir event handler ise, sınıflara gerek kalmaz.

Büyük Refactoring: Sınıflardan Fonksiyonlara

Aşama 1: Saf Operasyonların Belirlenmesi (Hafta 1)

Yeniden yapılandırma, şu özelliklere sahip operasyonları belirleyerek başlar:

  • Stateless (instance variable’ları yok)
  • Side-effect free (database/API çağrıları hariç)
  • Kolayca test edilebilir (input → output)
// Önce: Gereksiz state ile sınıf
class OrderValidator {
  private config: ValidationConfig;

  constructor(config: ValidationConfig) {
    this.config = config;
  }

  validate(order: Order): ValidationResult {
    // this.config'i çağrılar arasında hiç farklı kullanmayan
    // validation logic'i
  }
}

// Sonra: Saf fonksiyon
const validateOrder = (order: Order, config: ValidationConfig): ValidationResult => {
  if (!order.items?.length) return { valid: false, error: 'Sipariş ürün içermeli' };
  if (!order.customerId) return { valid: false, error: 'Müşteri ID gerekli' };
  if (order.total < config.minimumOrder) return { valid: false, error: 'Sipariş minimum tutarın altında' };

  return { valid: true };
};

Aşama 2: Event Handler Fonksiyonları (Hafta 2)

Her Lambda fonksiyonu basit bir event handler haline geldi:

// orders/handlers/order-created.ts
export const handler = async (event: EventBridgeEvent<'order.created', OrderData>) => {
  const { detail: orderData } = event;

  // 1. Siparişi validate et
  const validation = validateOrder(orderData, getConfig());
  if (!validation.valid) {
    await publishEvent('order.validation_failed', {
      orderId: orderData.id,
      error: validation.error
    });
    return;
  }

  // 2. Database'e kaydet
  await saveOrder(orderData);

  // 3. Downstream process'leri tetikle
  await publishEvent('order.validated', orderData);
};

// inventory/handlers/order-validated.ts
export const handler = async (event: EventBridgeEvent<'order.validated', OrderData>) => {
  const { detail: orderData } = event;

  // 1. Inventory'yi kontrol et
  const availability = await checkInventory(orderData.items);
  if (!availability.available) {
    await publishEvent('order.inventory_failed', {
      orderId: orderData.id,
      unavailableItems: availability.unavailable
    });
    return;
  }

  // 2. Ürünleri rezerve et
  await reserveInventory(orderData.items);

  // 3. Flow'u devam ettir
  await publishEvent('inventory.reserved', orderData);
};

Aşama 3: Dependency Injection’ın Kaldırılması (Hafta 3)

Dependency inject etmek yerine, konfigürasyon fonksiyonları ve environment-based switching kullandık:

// Önce: Karmaşık dependency injection
class NotificationService {
  constructor(
    private emailProvider: IEmailProvider,
    private smsProvider: ISMSProvider,
    private pushProvider: IPushProvider
  ) {}
}

// Sonra: Basit konfigürasyon fonksiyonları
const getEmailProvider = (): EmailProvider => {
  switch (process.env.EMAIL_PROVIDER) {
    case 'sendgrid': return new SendGridProvider();
    case 'ses': return new SESProvider();
    default: throw new Error('Email provider yapılandırılmamış');
  }
};

const sendOrderConfirmation = async (orderData: OrderData): Promise<void> => {
  const emailProvider = getEmailProvider();
  await emailProvider.send({
    to: orderData.customerEmail,
    template: 'order-confirmation',
    data: orderData
  });
};

// handlers/order-processed.ts
export const handler = async (event: EventBridgeEvent<'order.processed', OrderData>) => {
  await sendOrderConfirmation(event.detail);
  await publishEvent('notification.sent', {
    orderId: event.detail.id,
    type: 'order_confirmation'
  });
};

Sonuçlar: Ölçekte Basitlik

Birkaç haftalık refactoring sonrasında, birden fazla takım genelinde metrikler anlamlı iyileştirmeler gösterir:

Kod Azalması

ServiceÖnce (Satır)Sonra (Satır)Azalma
Order Service1,24762350%
Payment Service84735658%
Inventory Service62328754%
Notification Service44517860%
Toplam3,1621,44454%

Geliştirme Hızı

MetrikÖnceSonra
Yeni feature süresi2.3 hafta1.2 hafta
Bug fix süresi4.7 saat1.8 saat
Test yazma süresiGeliştirmenin 40%‘ıGeliştirmenin 25%‘i
Onboarding süresi3 hafta1.5 hafta
Deployment sıklığıHaftada 2xHaftada 4x

Bug Azalması

Bu desen aynı zamanda production bug’larında azalma sağlar. Neden?

  1. Saf fonksiyonlar öngörülebilir: Aynı input her zaman aynı output üretir
  2. Gizli state yok: Tutarsız duruma girebilecek instance variable’lar yok
  3. Kolay test: Karmaşık dependency graph’ları değil, sadece external çağrıları mock et
  4. Net veri akışı: Event’ler sistem davranışını açık hale getirir

Ortaya Çıkan Pattern’ler

1. Event Handler Pattern

Her Lambda fonksiyonu aynı basit pattern’i takip eder:

// Standart event handler template'i
export const handler = async (event: EventBridgeEvent<EventType, EventData>) => {
  try {
    // 1. Veriyi çıkar
    const data = event.detail;

    // 2. Validate et (saf fonksiyon)
    const validation = validateData(data);
    if (!validation.valid) {
      await publishEvent('validation.failed', { error: validation.error });
      return;
    }

    // 3. İşle (side effect'ler)
    const result = await processData(data);

    // 4. Sonucu publish et
    await publishEvent('process.completed', result);
  } catch (error) {
    await publishEvent('process.failed', { error: error.message });
    throw error;
  }
};

2. Saf Business Logic

Tüm business logic saf fonksiyonlar haline geldi:

// Business logic için saf fonksiyonlar
export const calculateOrderTotal = (items: OrderItem[]): number => {
  return items.reduce((total, item) => total + (item.price * item.quantity), 0);
};

export const applyDiscounts = (total: number, discounts: Discount[]): number => {
  return discounts.reduce((amount, discount) => {
    return discount.type === 'percentage'
      ? amount * (1 - discount.value / 100)
      : amount - discount.value;
  }, total);
};

export const calculateTax = (subtotal: number, taxRate: number): number => {
  return subtotal * (taxRate / 100);
};

// Saf fonksiyonların kompozisyonu
export const processOrderCalculation = (order: OrderRequest): OrderCalculation => {
  const subtotal = calculateOrderTotal(order.items);
  const discountedAmount = applyDiscounts(subtotal, order.discounts);
  const tax = calculateTax(discountedAmount, order.taxRate);
  const total = discountedAmount + tax;

  return { subtotal, discountedAmount, tax, total };
};

3. Injection Yerine Konfigürasyon

Dependency injection yerine, environment-based konfigürasyon kullandık:

// config/database.ts
export const getDatabaseClient = () => {
  return process.env.NODE_ENV === 'production'
    ? new DocumentClient()
    : new LocalDynamoDB();
};

// config/events.ts
export const getEventBridge = () => {
  return process.env.NODE_ENV === 'production'
    ? new EventBridge()
    : new LocalEventBus();
};

// Handler'larda kullanım
const saveOrder = async (order: OrderData): Promise<void> => {
  const db = getDatabaseClient();
  await db.put({ TableName: 'Orders', Item: order }).promise();
};

Test Stratejisi: Mock’lardan Entegrasyona

Önce: Mock Cehennemi

// Önce: Test her şeyi mock etmeyi gerektiriyordu
describe('OrderService', () => {
  let orderService: OrderService;
  let mockInventory: jest.Mocked<IInventoryService>;
  let mockPayment: jest.Mocked<IPaymentService>;
  let mockShipping: jest.Mocked<IShippingService>;
  let mockNotification: jest.Mocked<INotificationService>;

  beforeEach(() => {
    mockInventory = createMock<IInventoryService>();
    mockPayment = createMock<IPaymentService>();
    mockShipping = createMock<IShippingService>();
    mockNotification = createMock<INotificationService>();

    orderService = new OrderService(
      mockInventory,
      mockPayment,
      mockShipping,
      mockNotification
    );
  });

  it('should process order', async () => {
    // 40+ satır mock setup
    mockInventory.checkAvailability.mockResolvedValue({ available: true });
    mockPayment.processPayment.mockResolvedValue({ success: true });
    // ... 15 mock setup daha

    const result = await orderService.processOrder(orderData);

    expect(result.success).toBe(true);
    expect(mockInventory.checkAvailability).toHaveBeenCalledWith(orderData.items);
    // ... 12 assertion daha
  });
});

Sonra: Saf Fonksiyon Cenneti

// Sonra: Saf fonksiyonları test etmek kolaydı
describe('Sipariş hesaplamaları', () => {
  it('sipariş toplamını doğru hesaplar', () => {
    const items = [
      { price: 10, quantity: 2 },
      { price: 5, quantity: 1 }
    ];

    expect(calculateOrderTotal(items)).toBe(25);
  });

  it('yüzde indirimi uygular', () => {
    const discounts = [{ type: 'percentage', value: 10 }];

    expect(applyDiscounts(100, discounts)).toBe(90);
  });
});

// Event handler'lar için entegrasyon testleri
describe('Order created handler', () => {
  it('geçerli siparişi kaydeder ve event publish eder', async () => {
    const mockDb = createMockDB();
    const mockEvents = createMockEventBridge();

    await handler(createOrderEvent(validOrderData));

    expect(mockDb.put).toHaveBeenCalledWith(validOrderData);
    expect(mockEvents.publish).toHaveBeenCalledWith('order.validated', validOrderData);
  });
});

Test iyileştirmeleri: Daha az test case gerekli, daha az mock setup.

Monitoring Devrimi

Saf fonksiyonlar ve event’ler ile monitoring neredeyse kolay hale geldi:

// Her fonksiyon için otomatik tracing
import { captureAWS } from 'aws-xray-sdk';

// Her fonksiyon otomatik olarak trace edilir
export const handler = async (event) => {
  // X-Ray otomatik olarak şunları track eder:
  // - Fonksiyon execution süresi
  // - Database çağrıları
  // - Event publishing
  // - Error oranları

  const result = await processBusinessLogic(event.detail);
  await publishEvent('process.completed', result);
};

// Event'ler aracılığıyla business metrikler
const publishBusinessMetric = (metric: string, value: number, tags: Record<string, string>) => {
  publishEvent('metric.recorded', { metric, value, tags, timestamp: Date.now() });
};

// Kullanım
await publishBusinessMetric('order.processed', 1, {
  paymentMethod: order.paymentMethod,
  customerSegment: order.customerSegment
});

Observability iyileştirmeleri:

  • Debug süresi: Ortalama debug session’ları önemli ölçüde azaldı
  • Mean Time to Detection: Daha hızlı incident tespiti
  • Root cause tanımlama: Gelişmiş tracing yetenekleri
  • Performance monitoring: X-Ray ile built-in

Pattern’in Sınırları

Bu fonksiyonel, event-driven yaklaşım her zaman cevap değil. İşte sınıfları kullanmaya devam edeceğiniz durumlar:

1. Stateful Operasyonlar

// Operasyonlar arasında state tutmanız gerektiğinde
class ConnectionManager {
  private connections = new Map<string, Connection>();

  async getConnection(id: string): Promise<Connection> {
    if (!this.connections.has(id)) {
      this.connections.set(id, await createConnection(id));
    }
    return this.connections.get(id);
  }
}

2. Karmaşık Lifecycle Management

// Resource'lar dikkatli lifecycle management gerektirdiğinde
class DatabaseMigrator {
  constructor(private db: Database) {}

  async migrate(): Promise<void> {
    await this.db.startTransaction();
    try {
      await this.runMigrations();
      await this.db.commit();
    } catch (error) {
      await this.db.rollback();
      throw error;
    }
  }
}

3. Framework Entegrasyonu

// Sınıf bekleyen framework'lerle çalışırken
@Controller('/users')
class UserController {
  @Get('/:id')
  async getUser(@Param('id') id: string): Promise<User> {
    return getUserById(id);
  }
}

18 Aylık Sonuçlar

Fonksiyonel, event-driven mimariyi benimseyen ekipler şunları raporlar:

Takım Verimliliği

  • Yeni developer onboarding: Haftalardan günlere azaldı
  • Feature teslimat süresi: Geliştirme döngülerinde anlamlı iyileştirme
  • Deployment sıklığı: Daha sık, daha güvenli deployment’lar
  • Code review süresi: Review karmaşıklığında gözle görülür azalma

Sistem Güvenilirliği

  • Production incident’lar: Aylık incident’larda önemli azalma
  • Bug fix süresi: Sorunların daha hızlı çözülmesi
  • System uptime: Genel güvenilirlikte iyileşme
  • Performance: Tüm alanlarda daha iyi response süreleri

İş Etkisi

İş etkisi anlamlıydı:

  • Geliştirme verimliliği: Takımlar feature’ları daha hızlı teslim edebildi
  • Altyapı maliyetleri: Serverless fonksiyonlar operasyonel yükü azalttı
  • Kalite iyileştirmeleri: Daha az production sorunu ve daha hızlı çözüm
  • Developer memnuniyeti: Daha basit kod tabanı takım moralini iyileştirdi

Kilit İçgörü: Basitliğin Gücü

En önemli ders teknik değil, felsefidir. Karmaşıklık sofistikasyon değildir.

Java ve C#‘tan gelen pattern’ler o bağlamlarda mantıklıdır, ama Node.js geliştiriciler fonksiyonel doğayı benimsediğinde parlar:

  1. Stateless operasyonlar için sınıflar yerine fonksiyonlar
  2. Service iletişimi için method çağrıları yerine event’ler
  3. Dependency’ler için injection yerine konfigürasyon
  4. Business logic için karmaşık soyutlamalar yerine saf fonksiyonlar

Node.js Mimarisinin Geleceği

Serverless devrimi bize çoğu enterprise pattern’in aşırı mühendislik olduğunu öğretti. Node.js uygulamalarının geleceği:

  • Function-first: Her operasyon küçük, odaklanmış bir fonksiyon olarak
  • Event-driven: Varsayılan olarak asynchronous iletişim
  • Stateless: Operasyonlar arası paylaşılan mutable state yok
  • Observable: Built-in tracing ve monitoring

Sophisticated, ölçeklenebilir sistemleri factory’ler, dependency injection ya da karmaşık sınıf hiyerarşileri olmadan inşa etmek mümkündür. Bazen en iyi mimari yoldan çekilen mimaridir.

Kaynaklar

İlgili yazılar