Skip to content
~/sph.sh

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

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

typescript
// "Temiz mimari" adına yarattığımız canavarclass 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 edilebilirexport 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 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

Microservice'ler inşa etmeye başladığımızda, 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

Zamanla, 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 birden fazla dosya ve service'te değişiklik gerektirdi. Dependency graph'ını anlamak yeni takım üyeleri için önemli bir zorluk haline geldi.

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 kabusuawait 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ğuawait 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ıfclass 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 fonksiyonconst 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.tsexport 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.tsexport 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 injectionclass 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.tsexport 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, metriklerimiz anlamlı iyileştirmeler gösterdi:

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ı

Ayrıca production bug'larında azalma da gördük. 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'iexport 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 fonksiyonlarexport 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 kompozisyonuexport 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.tsexport const getDatabaseClient = () => {  return process.env.NODE_ENV === 'production'    ? new DocumentClient()    : new LocalDynamoDB();};
// config/events.tsexport const getEventBridge = () => {  return process.env.NODE_ENV === 'production'    ? new EventBridge()    : new LocalEventBus();};
// Handler'larda kullanımconst 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 gerektiriyordudescribe('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 testleridescribe('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:

typescript
// Her fonksiyon için otomatik tracingimport { captureAWS } from 'aws-xray-sdk';
// Her fonksiyon otomatik olarak trace edilirexport 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 metriklerconst publishBusinessMetric = (metric: string, value: number, tags: Record<string, string>) => {  publishEvent('metric.recorded', { metric, value, tags, timestamp: Date.now() });};
// Kullanımawait 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

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ğindeclass 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ğindeclass 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

Fonksiyonel, event-driven mimariyi benimsedikten sonra:

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ü: 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.

İlgili Yazılar