Node.js'te Factory ve DI Yerine Saf Fonksiyonlar
Lambda biçimli bir Node.js servisinde factory, service katmanı ve DI container'ı çoğu zaman fazladır. Yerlerine ne geçer, sınıflar nerede yerini korur?
Bir Node.js ödeme servisinde tek bir validation kuralını değiştirmek, kural bir factory’nin, bir dependency injection container’ının ve her biri tek implementasyonu olan bir yığın interface’in arkasında duruyorsa yarım günü yiyebilir. Maliyet, yapmak istediğiniz değişiklik ile o değişikliğin durduğu dosya arasındaki mesafedir. Xia ve arkadaşları bu zamanın nereye gittiğini IEEE Transactions on Software Engineering için yürüttükleri saha çalışmasında ölçtü: yedi proje, 79 profesyonel geliştirici, 3.244 çalışma saati. Kodu anlamak çalışma gününün %57,62’sini, dosyalar arasında gezinmek %23,96’sını, düzenleme ise %5,02’sini aldı.
Bu makinenin çoğu Java ve C#‘tan taşınmış bir alışkanlıktır. Lambda biçimli bir serviste saf fonksiyonlar ve event’ler aynı işi daha az dolaylılıkla yapar: daha az boilerplate, daha kolay test, daha hızlı değişiklik. Sınıflar connection pool’larda, lifecycle yönetiminde ve framework koduyla çalışırken yerini korur; bu durumları tek tek adlandırmak gerekir.
İlk emekliye ayrılması gereken kalıp aşağıdaki: bir factory tarafından kurulan, dependency injection ile bağlanan ve bir düzine interface’in arkasına saklanan PaymentService.
// "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
}
}
Değişmesi gereken kural tek satırlık bir validation: kredi kartı numaraları boşluk içermemeli.
Saf fonksiyonlarla ve event-driven akışla yazıldığında aynı işlevsellik şö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);
};
Kural artık tek girdisi ve tek çıktısı olan bir fonksiyon. Yanındaki handler ya bir hata event’i yayınlıyor ya da kartı çekiyor. İkisini kurmak için container, test etmek için mock grafiği gerekmiyor.
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 bu graftaki her düğümü mock etmek zorundaydı
// 5. Tek satırlık bir değişiklik dosya zincirinin tamamına yayılı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.
Gereksiz Dolaylılığın Maliyeti#
Kimsenin ihtiyaç duymadığı bir soyutlamanın bedeli ölçüldü. Prechelt, Unger, Tichy, Brössler ve Votta, 29 profesyonel yazılım mühendisiyle kontrollü bir bakım deneyi yürüttü; sonuçlar 2001’de IEEE Transactions on Software Engineering’de yayımlandı. Karşısındaki pattern’i önceden bilmeyen mühendisler, programlardan birinde pattern’li sürümü değiştirmek için basit alternatife göre %151 daha fazla zaman harcadı. Ölçüm 46,6 dakikaya karşı 18,5 dakika, p < 0,001. Tek satırlık bir kural değişikliğine giden yarım günün ölçülmüş hali bu.
Aynı deney, pattern’leri prensip gereği söküp atmaya karşı da en güçlü argüman. Dokuz bakım görevinin çoğunda sonuçlar pattern’li sürümün lehineydi. Yazarların kapanış tavsiyesi şu: basit çözümü tercih etmek için net bir neden yoksa pattern’in getirdiği esnekliği almak muhtemelen daha doğrudur. Beklenmedik gereksinimler gelmeye devam eder. Bütünüyle okununca bulgu, pattern’lere topyekûn karşı çıkmaktan çok daha dar: bedeli, tasarımı zaten kafasında taşımayan kişi ödüyor. Xia ve arkadaşları da aynı yöne işaret ediyor; kıdemli geliştiriciler zamanlarının daha küçük bir bölümünü kodu anlamaya harcıyor. Aynı çalışma, önceki araştırmalara dayanarak BT alanındaki personel devir oranını %20 ile %35 arasında veriyor. Ekipte grafiğe yeni olan biri her zaman vardır.
Kusur verisi ise sökme sırasını karmaşıklaştırıyor. Vokáč, IEEE Transactions on Software Engineering için 500.000 satırlık ticari bir C++ ürününün üç yıllık haftalık gelişim geçmişini izledi. Kusur oranları pattern’e göre belirgin biçimde değişiyordu: Factory ile ilişkili kod, kod tabanının ortalama kusur oranının %63’ünde kalırken Singleton %135, Observer %155 seviyesindeydi. Vokáč bu düşük oranı pattern’in kendisine değil, Factory’nin daha az karmaşık ve daha az merkezi kodu işaretlemesine bağlıyor ve pattern kullanmanın tek başına az kusur garantisi vermediği sonucuna varıyor. Bu, baştaki örnekteki factory’ye açılan davayı da daraltır. O factory tek bir gerekçeyle sökülmeyi hak ediyor: yalnızca nesne kuruyor, hiçbir zaman seçim yapmıyor; içinde karar olmayan bir grafiği kurmak için sınıfa gerek yok.
Dependency injection tarafında kanıt daha da zayıf, üstelik iki yönde birden. Razina ve Janzen, biri dependency injection kullanan diğeri kullanmayan 20 eşleştirilmiş SourceForge projesi çiftini karşılaştırdı. DI kullanımı ile coupling veya cohesion sayıları arasında korelasyon bulamadılar. Daha düşük coupling eğilimi ancak %10’un üzerinde DI kullanımında beliriyordu. Sun ve Kim 2022’de aynı soruya döndü ve önceki çalışmaların çıkarım sunduğunu, iki yönde de kesin kanıt vermediğini bildirdi. Bunların hiçbiri DI’ın kod tabanına zarar verdiğini göstermiyor. Gösterdikleri şey, vaat edilen bakım kolaylığının kanıtlanmamış olduğu; container’ı varsayılan yapmak için zayıf bir zemin.
Lambda ise bu çalışmaların göremediği bir maliyet ekliyor ve AWS bunu belgeliyor. On-demand concurrency’de Init fazı 10 saniyeyle sınırlı. Provisioned concurrency ve SnapStart bunun yerine daha uzun bir bütçe alıyor: 130 saniye ya da konfigüre edilmiş fonksiyon timeout’undan hangisi büyükse o. AWS, fonksiyon çalışmadan önceki gecikmenin en büyük kaynağı olarak initialization kodunu gösteriyor; sürücü olarak da paket boyutunu ve initialization işinin miktarını sayıyor. Bir factory’nin hangi fazda çalıştığı nereden çağrıldığına bağlı ve yukarıdaki örnek çağrı yerini hiç göstermiyor. Modül seviyesinde bir sabite atanırsa PaymentServiceFactory.create execution environment başına bir kez çalışır ve kurduğu nesne grafiği Init işidir. Handler’ın içinden çağrılırsa validator, adapter ve logger’ları her invocation’da, warm olanlar dahil, yeniden kurar. AWS’in kendi çözümü ilk yerleşimi hedefliyor ve burada savunulan biçimle aynı: çok sayıda nesne ve bağlantı taşıyan bir fonksiyonu, her biri daha az initialization kodu taşıyan daha küçük ve özelleşmiş fonksiyonlara bölmek. Yerleşimin ne kazandırdığı trafiğe bağlı. AWS, cold start’ların tipik olarak invocation’ların %1’inden azında görüldüğünü ve süresinin 100 ms’nin altından 1 saniyenin üzerine kadar değiştiğini söylüyor. Daha seyrek çağrılan fonksiyonlarda bunların daha sık görüldüğünü de ekliyor. İstikrarlı çağrılan bir fonksiyonda modül seviyesinde kurulan daha ince bir grafik yalnızca tail latency tarafında görünür. Seyrek ya da dalgalı çağrılan bir fonksiyonda cold start’lar invocation’ların çok daha büyük bir bölümünü kaplar ve aynı incelme çok daha fazla istekte karşılığını verir. Handler’ın içinde kurulursa grafik trafik ne olursa olsun her istekte yeniden kurulur; cold start rakamı bu maliyete bir sınır koymaz.
Event-Driven Bakış Açısı#
Çı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ı akış event’ler olarak modellenebilir:
// 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 olduğunda, etrafındaki sınıf tutacak hiçbir şeyi kalmayan bir sarmalayıcıya döner.
Refactoring: Sınıflardan Fonksiyonlara#
Aşama 1: Saf Operasyonların Belirlenmesi#
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ı#
Her Lambda fonksiyonu basit bir event handler’a dönüşür:
// 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ı#
Dependency inject etmek yerine konfigürasyon fonksiyonları ve environment-based switching kullanın:
// Ö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'
});
};
Kusurların Saklanacağı Daha Az Yer#
Saf fonksiyonlar ve açık event’ler bir hatanın hayatta kalabileceği yüzeyi daraltır:
- Saf fonksiyonlar öngörülebilir: Aynı input her zaman aynı output üretir
- Gizli state yok: Tutarsız duruma girebilecek instance variable’lar yok
- Kolay test: Karmaşık dependency graph’ları değil, sadece external çağrıları mock et
- Net veri akışı: Event’ler sistem davranışını açık hale getirir
Kusur verisinden gelen uyarı burada da geçerli. Biçim tek başına hiçbir şey garanti etmez; yalnızca bir hatanın fark edilmeden yaşayabileceği alanı daraltır.
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#
Business logic saf fonksiyonlara taşınır:
// 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 bağımlılıklar environment-based konfigürasyondan çözülür:
// config/database.ts
// Execution environment başına bir kez kurulur; warm invocation'lar aynı client'ı kullanır
let dbClient: DocumentClient | LocalDynamoDB | undefined;
export const getDatabaseClient = () => {
dbClient ??= process.env.NODE_ENV === 'production'
? new DocumentClient()
: new LocalDynamoDB();
return dbClient;
};
// config/events.ts
let eventBus: EventBridge | LocalEventBus | undefined;
export const getEventBridge = () => {
eventBus ??= process.env.NODE_ENV === 'production'
? new EventBridge()
: new LocalEventBus();
return eventBus;
};
// Handler'larda kullanım
const saveOrder = async (order: OrderData): Promise<void> => {
const db = getDatabaseClient();
await db.put({ TableName: 'Orders', Item: order }).promise();
};
Mock Grafiği Olmadan Test#
Önce: Tüm Grafiği Mock’lamak#
// Ö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: Düz Fonksiyonları Test Etmek#
// 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);
});
});
Tracing ve İş Metrikleri#
Saf fonksiyonlar ve event’lerle enstrümantasyon handler’ın kenarlarında kalır:
import AWSSDK from 'aws-sdk';
import { captureAWS } from 'aws-xray-sdk';
// SDK'yı modül başına bir kez sarmalayın. Segment'lerin kaydedilmesi
// için fonksiyonda active tracing açık olmalı.
const AWS = captureAWS(AWSSDK);
export const handler = async (event) => {
// Sarmalanmış SDK'dan üretilen client'larla X-Ray şunları kaydeder:
// - 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
});
Her adım bir event yayınladığı için, takılan bir sipariş ürettiği son event’e kadar izlenebilir. Arama beş servisin log’unu yan yana okumak yerine tek bir handler’a daralır.
Kendi Kod Tabanınızda Ne Ölçmeli#
Sınıfları saf fonksiyonlardan yalıtıp bir ekibin bu çizginin iki yanında ne ürettiğini ölçen yayımlanmış bir çalışma yok. Refactoring yazılarını süsleyen satır sayısı ve hız rakamları ya birinci elden ölçülür ya da hiçbir şey ifade etmez; doğru hamle onları ödünç almak yerine düzgün toplamak.
Nereye bakılacağını mekanizma söylüyor. Yukarıdaki saha çalışmasında düzenleme, çalışma gününün en küçük dilimiydi; dolayısıyla eklenen ya da silinen satırları saymak işin en ucuz kısmını ölçer. Bu değişikliğin asıl oynattığı şey, bir davranış ile onu tutan dosya arasındaki mesafedir. O mesafeyi üç gösterge izler: tek bir davranış değişikliğinin kaç dosyaya dokunduğu, bir değişikliğin ilk commit ile production arasında ne kadar beklediği ve davranış değiştiğinde handler’ın test kurulumunun ne kadarının yeniden yazılması gerektiği.
Adı konmuş tek bir yüzey seçin, refactoring öncesinde bu üçünü kaydedin, o yüzeyde birkaç hafta normal iş yaptıktan sonra tekrar kaydedin ve sonucu yöntemiyle birlikte paylaşın. DORA’nın teslimat metrikleri (deployment sıklığı, değişikliğin production’a ulaşma süresi, servisi geri getirme süresi, değişiklik başarısızlık oranı) önce-sonra karşılaştırması için tanıdık bir çerçeve verir. Ancak kapsamları bu sorunun gerisinde kalır. DORA’nın gevşek bağlı mimari tanımı, ekiplerin bağımlı oldukları servislerden bağımsız deploy edebilmesi üzerine kuruludur; bu da senkron çağrılardan event’lere geçişi kapsar, bir handler’ın sınıf mı fonksiyon mu olduğu hakkında bir şey söylemez.
Sınıfların Doğru Seçim Olduğu Durumlar#
Fonksiyonel, event-driven biçim bir varsayılandır, kural değil. Üç durum hâlâ sınıf ister:
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> {
let connection = this.connections.get(id);
if (!connection) {
connection = await createConnection(id);
this.connections.set(id, connection);
}
return connection;
}
}
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);
}
}
Değiştirmeye Değer Dört Varsayılan#
Java ve C#‘tan gelen pattern’ler kendi ekosistemlerinde yerini bulur. Node.js’te ise şu dört tercih daha iyi bir başlangıç noktasıdır:
- Stateless operasyonlar için sınıflar yerine fonksiyonlar
- Service iletişimi için method çağrıları yerine event’ler
- Dependency’ler için injection yerine konfigürasyon
- Business logic için karmaşık soyutlamalar yerine saf fonksiyonlar
Varsayılan, handler stateless kaldığı ve işi tek bir event’e sığdığı sürece geçerlidir: fonksiyonu yazın, event’e bağlayın, business logic’i saf tutun. Tek bir invocation’dan uzun yaşaması gereken bir şey varsa (connection pool, migration transaction’ı ya da sınıf bekleyen bir framework) sınıfa dönün. Bir ServiceFactory ya da tek implementasyonlu bir interface eklemeden önce, o soyutlamanın bu kod tabanındaki maliyetini karşılayıp karşılamadığını, yoksa başka bir dilden taşınmış bir alışkanlık mı olduğunu kontrol edin.
Kaynaklar#
- AWS Lambda nedir? (yeni sekmede açılır) - Saf fonksiyon yaklaşımının temelini oluşturan stateless fonksiyon tasarım prensipleri
- AWS Lambda fonksiyonları için en iyi uygulamalar (yeni sekmede açılır) - Tek amaçlı fonksiyonlar, handler yapısı ve paylaşılan mutable state’den kaçınma
- Node.js ile Lambda fonksiyonu oluşturma (yeni sekmede açılır) - Lambda için Node.js runtime yapılandırması, handler kalıpları ve modül başlatma
- Lambda’yı Amazon SQS ile kullanma (yeni sekmede açılır) - Servisler arası doğrudan method çağrılarına alternatif olarak SQS üzerinden olay güdümlü çağırma
- Amazon EventBridge nedir? (yeni sekmede açılır) - Serverless fonksiyonlar arasında gevşek bağlı, asenkron iletişim için event bus modeli
- Serverless Uygulamalar Lens - AWS Well-Architected (yeni sekmede açılır) - Tasarım prensipleri: hızlı, basit, tekil fonksiyonlar ve paylaşımsız stateless mimari
- Lambda execution environment yaşam döngüsü - AWS Lambda Operator Guide (yeni sekmede açılır) - On-demand concurrency için 10 saniyelik Init fazı sınırı, provisioned concurrency ile SnapStart’ın bunun yerine aldığı daha uzun bütçe, initialization gecikmesini neyin sürüklediği ve cold start’ların ne sıklıkta görüldüğü
- Measuring Program Comprehension: A Large-Scale Field Study with Professionals (IEEE TSE, 2018) (yeni sekmede açılır) - Yedi proje, 79 profesyonel geliştirici ve 3.244 çalışma saatlik ölçümlü çalışma; zamanın kodu anlama, gezinme ve düzenleme arasındaki dağılımı
- A Controlled Experiment in Maintenance Comparing Design Patterns to Simpler Solutions (IEEE TSE, 2001) (yeni sekmede açılır) - 29 profesyonel mühendisle yürütülen bakım deneyi; gereksiz bir pattern’in maliyetini de pattern’in işe yaradığı görevleri de ölçüyor
- Defect Frequency and Design Patterns: An Empirical Study of Industrial Code (IEEE TSE, 2004) (yeni sekmede açılır) - 500.000 satırlık ticari bir üründe üç yıl boyunca pattern başına kusur oranları; Factory, Singleton ve Observer ayrı ayrı ölçülmüş
- Effects of Dependency Injection on Maintainability (IASTED SEA, 2007) (yeni sekmede açılır) - Dependency injection kullanan ve kullanmayan 20 eşleştirilmiş açık kaynak proje çiftinin coupling ve cohesion karşılaştırması
- Analyzing Impact of Dependency Injection on Software Maintainability (arXiv, 2022) (yeni sekmede açılır) - Dependency injection ve bakım kolaylığı üzerine kanıtların neyi gösterip neyi göstermediği ve önerilen yeni metrik
- Loosely coupled architecture - DORA (yeni sekmede açılır) - DORA’nın mimari ayrışmayı bağımsız deploy edebilme üzerinden tanımlaması ve bunun teslimat metriklerinin kapsamına koyduğu sınır
İlgili yazılar
CloudEvents spesifikasyonu ve TypeScript SDK'sı için pratik rehber: AWS Lambda ve EventBridge'de standart eventler oluştur, parse et ve doğrula.
typescript · serverless · lambda +2
AWS Lambda'da Node.js'den Go'ya geçiş ne zaman kendini amorti eder, ne zaman etmez: karar çerçevesi, serverless Go pattern'ları ve maliyet matematiği.
go · nodejs · serverless +5
AWS Lambda, EventBridge ve DynamoDB ile pratik bir CQRS uygulaması; event sourcing, eventual consistency ve dağıtık sistemde debug yöntemleri.
architecture · dynamodb · event-driven +3
Node.js monolitlerini event-driven serverless fonksiyonlara ayrıştırma rehberi: migrasyon stratejileri, sınır kararları ve mimari kalıplar.
event-driven · architecture · nodejs +1
Legacy kodu kimin yazdığını sormayı bırakın. Sorumluluğu, hesap verebilirliği ve suçlamayı ayırın; miras kodu sahipsiz bırakmak yerine sahiplendirin.
engineering-culture · leadership · team-management +3