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

Node.js microservice'lerimizden tüm factory'leri, service'leri ve dependency injection'ları çıkardıktan sonra, 65% daha az bug ile 3x daha hızlı ship etmeye başladık. Event-driven mimariler için fonksiyonların sınıfları neden geçtiğini anlatıyorum.

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

Ekim 2022. 20 dakika sürmesi gereken bir ödeme işleme bug'ını debug ediyordum. Bunun yerine, tek bir validation kuralını değiştirmek için 847 satır "kurumsal mimari"de 3 saat geçirdim.

Suçlu? PaymentService sınıfımız - bir factory, dependency injection, 12 farklı interface ve bir Java developer'ı sevinçle ağlatacak kadar soyutlama içeren aşırı mühendislik şaheseri.

TypeScript
// "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şimizden sonra, aynı fonksiyonalite şöyle oldu:

TypeScript
// 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 → 3 dakika. Kod satırı: 847 → 23. Test karmaşıklığı: 156 test case → 8 test case.

Bu hikaye, factory'lerimizi, service'lerimizi ve dependency injection container'larımızı nasıl öldürdüğümüzü ve Node.js uygulamalarımızın neden daha hızlı, daha güvenilir ve çalışması sonsuz derecede daha keyifli hale geldiğini anlatıyor.

Node.js'in Java-laşması: Nasıl Bu Noktaya Geldik#

Takımımız 2020'de microservice'ler inşa etmeye başladığında, Java ve C# geçmişinden geliyorduk. O ekosistemlerde mantıklı olan enterprise pattern'leri getirdik:

  • "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

2022'ye gelindiğinde, Node.js kod tabanımız idiomatic JavaScript'ten çok Spring Boot'a benziyordu. Her basit operasyon birden fazla soyutlama katmanında gezinmeyi gerektiriyordu:

TypeScript
// 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

Uyanış çağrısı: "Sipariş onay e-postası gönder" feature'ını eklemek 4 farklı service boyunca 8 dosyada değişiklik gerektirdi. Junior bir developer sadece dependency graph'ını anlamak için 2 hafta harcadı.

Event-Driven Aydınlanma#

Çıkış noktası, "service"lerimizin çoğunun aslında kılık değiştirmiş event handler'lar olduğunu fark etmemizle geldi.

Synchronous service çağrıları yerine:

TypeScript
// 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:

TypeScript
// 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, neden hiç sınıfa ihtiyacımız olsun ki?

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

Aşama 1: Saf Operasyonları Belirle (Hafta 1)#

Şu özelliklere sahip operasyonları belirlemeye başladık:

  • Stateless (instance variable'ları yok)
  • Side-effect free (database/API çağrıları hariç)
  • Kolayca test edilebilir (input → output)
TypeScript
// Ö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:

TypeScript
// 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'ı Yok Et (Hafta 3)#

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

TypeScript
// Ö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#

3 haftalık refactoring sonrasında, metriklerimiz dikkat çekici bir hikaye anlattı:

Kod Azalması#

ServiceÖnce (Satır)Sonra (Satır)Azalma
Order Service1,24742366%
Payment Service84715682%
Inventory Service62320168%
Notification Service4458980%
Toplam3,16286972%

Geliştirme Hızı#

TypeScript
// Önce vs Sonra metrikleri (6 aylık ortalama)
const developmentMetrics = {
  once: {
    yeniFeatureSuresi: '2.3 hafta',
    bugFixSuresi: '4.7 saat',
    testYazmaSuresi: 'Geliştirmenin 40%\'ı',
    onboardingSuresi: '3 hafta',
    deploymentSikligi: 'Haftada 2x'
  },
  sonra: {
    yeniFeatureSuresi: '3.2 gün',
    bugFixSuresi: '22 dakika',
    testYazmaSuresi: 'Geliştirmenin 15%\'i',
    onboardingSuresi: '2 gün',
    deploymentSikligi: 'Günde 8x'
  }
};

Bug Azalması#

En şaşırtıcı sonuç production bug'larında 65% azalmaydı. 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:

TypeScript
// 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:

TypeScript
// 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:

TypeScript
// 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: Kabusdan Keyfе#

Önce: Mock Cehennemi#

TypeScript
// Ö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#

TypeScript
// 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 süresi azalması: 75% daha az test case, 90% daha az mock setup.

Monitoring Devrimi#

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

TypeScript
// 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: 4.7 saat → ortalama 12 dakika
  • Mean Time to Detection: 45 dakika → 3 dakika
  • Root cause tanımlama: 89% daha hızlı
  • Performance monitoring: X-Ray ile built-in

Bu Pattern'i NE ZAMAN Kullanmamalısınız#

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

1. Stateful Operasyonlar#

TypeScript
// 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#

TypeScript
// 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#

TypeScript
// 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#

18 aylık fonksiyonel, event-driven mimari sonrasında:

Takım Verimliliği#

  • Yeni developer onboarding: 3 hafta → 2 gün
  • Feature teslimat süresi: 2.3 hafta → 3.2 gün
  • Deployment sıklığı: Haftada 2x → günde 8x
  • Code review süresi: 67% azalma

Sistem Güvenilirliği#

  • Production incident'lar: 23/ay → 2/ay
  • Bug fix süresi: 4.7 saat → 22 dakika
  • System uptime: 99.2% → 99.89%
  • Performance: P95 response sürelerinde 34% iyileştirme

İş Etkisi#

TypeScript
const businessImpact = {
  developmentCosts: {
    once: 89000,    // Aylık developer maliyetleri
    sonra: 62000,   // Verimlilik sayesinde 30% azalma
    tasarruf: 27000
  },
  infrastructureCosts: {
    once: 19000,    // Önceki microservices migrasyonundan
    sonra: 8900,    // Serverless optimizasyonu
    tasarruf: 10100
  },
  toplamAylikTasarruf: 37100,  // Yılda 445K$
  kaliteMetrikleri: {
    bugAzalmasi: 0.65,              // 65% daha az production bug
    deploymentRiskAzalmasi: 0.78,   // 78% daha az deployment sorunu
    musteriMemnuniyeti: 0.23        // Response sürelerinde 23% iyileştirme
  }
};

Kilit İçgörü: Basitlik Ölçeklenir#

Bu yolculuktan çıkan en önemli ders teknik değil, felsefi bir dersdi. Karmaşıklık sofistikasyon değildir.

Java ve C#'ta öğrendiğimiz pattern'ler o bağlamlarda mantıklıydı, ama Node.js fonksiyonel doğasını benimsediğinizde 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

Sıradaki: 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 edebileceğinizi kanıtladık. Bazen en iyi mimari yolunuzdan çekilen mimaridir.

Bir dahaki sefer ServiceFactory yaratırken ya da tek implementation'ı olan bir interface yazarken kendinize sorun: "Bu karmaşıklığa ihtiyacım var mı, yoksa JavaScript'te Java'yı yeniden mi yaratıyorum?"

Cevap sizi şaşırtabilir.

Loading...

Yorumlar (0)

Sohbete katıl

Düşüncelerini paylaşmak ve toplulukla etkileşim kurmak için giriş yap

Henüz yorum yok

Bu yazı hakkında ilk düşüncelerini paylaşan sen ol!

Related Posts