6.3 KiB
id, title, category, priority, tags, related
| id | title | category | priority | tags | related | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| solid-srp-function | SOLID - Single Responsibility Principle (Function Level) | solid-principles | critical |
|
|
Single Responsibility Principle - Function Level
A function should do one thing, do it well, and do it only. It should have a single, clear purpose that can be described without using conjunctions like "and" or "or".
Bad Example
// Anti-pattern: Function does multiple things
async function processOrder(orderId: string): Promise<void> {
// Fetch order
const order = await db.query('SELECT * FROM orders WHERE id = ?', [orderId]);
// Validate order
if (!order) throw new Error('Order not found');
if (order.status !== 'pending') throw new Error('Order already processed');
if (order.items.length === 0) throw new Error('Order has no items');
// Calculate totals
let subtotal = 0;
for (const item of order.items) {
const product = await db.query('SELECT price FROM products WHERE id = ?', [item.productId]);
subtotal += product.price * item.quantity;
}
const tax = subtotal * 0.1;
const shipping = subtotal > 100 ? 0 : 10;
const total = subtotal + tax + shipping;
// Update inventory
for (const item of order.items) {
await db.query('UPDATE products SET stock = stock - ? WHERE id = ?',
[item.quantity, item.productId]);
}
// Process payment
const paymentResult = await stripe.charges.create({
amount: Math.round(total * 100),
currency: 'usd',
customer: order.customerId
});
// Update order status
await db.query('UPDATE orders SET status = ?, total = ?, payment_id = ? WHERE id = ?',
['completed', total, paymentResult.id, orderId]);
// Send confirmation email
const emailHtml = `<h1>Order Confirmed</h1><p>Total: $${total}</p>`;
await sendgrid.send({
to: order.customerEmail,
subject: 'Order Confirmation',
html: emailHtml
});
// Log analytics
await analytics.track('order_completed', { orderId, total, itemCount: order.items.length });
}
Good Example
// Correct approach: Each function has a single purpose
async function processOrder(orderId: string): Promise<ProcessedOrder> {
const order = await fetchAndValidateOrder(orderId);
const pricing = await calculateOrderPricing(order);
await reserveInventory(order.items);
try {
const payment = await processPayment(order.customerId, pricing.total);
const completedOrder = await finalizeOrder(order, pricing, payment);
await sendOrderConfirmation(completedOrder);
await trackOrderCompletion(completedOrder);
return completedOrder;
} catch (error) {
await releaseInventory(order.items);
throw error;
}
}
async function fetchAndValidateOrder(orderId: string): Promise<Order> {
const order = await orderRepository.findById(orderId);
if (!order) {
throw new OrderNotFoundError(orderId);
}
validateOrderStatus(order);
validateOrderItems(order);
return order;
}
function validateOrderStatus(order: Order): void {
if (order.status !== 'pending') {
throw new InvalidOrderStatusError(order.id, order.status);
}
}
function validateOrderItems(order: Order): void {
if (order.items.length === 0) {
throw new EmptyOrderError(order.id);
}
}
async function calculateOrderPricing(order: Order): Promise<OrderPricing> {
const subtotal = await calculateSubtotal(order.items);
const tax = calculateTax(subtotal);
const shipping = calculateShipping(subtotal);
return {
subtotal,
tax,
shipping,
total: subtotal + tax + shipping
};
}
async function calculateSubtotal(items: OrderItem[]): Promise<number> {
const itemTotals = await Promise.all(
items.map(item => calculateItemTotal(item))
);
return itemTotals.reduce((sum, total) => sum + total, 0);
}
async function calculateItemTotal(item: OrderItem): Promise<number> {
const product = await productRepository.findById(item.productId);
return product.price * item.quantity;
}
function calculateTax(subtotal: number): number {
const TAX_RATE = 0.1;
return subtotal * TAX_RATE;
}
function calculateShipping(subtotal: number): number {
const FREE_SHIPPING_THRESHOLD = 100;
const STANDARD_SHIPPING = 10;
return subtotal > FREE_SHIPPING_THRESHOLD ? 0 : STANDARD_SHIPPING;
}
async function reserveInventory(items: OrderItem[]): Promise<void> {
await Promise.all(
items.map(item => inventoryService.reserve(item.productId, item.quantity))
);
}
async function releaseInventory(items: OrderItem[]): Promise<void> {
await Promise.all(
items.map(item => inventoryService.release(item.productId, item.quantity))
);
}
async function processPayment(customerId: string, amount: number): Promise<Payment> {
return paymentService.charge(customerId, amount);
}
async function finalizeOrder(
order: Order,
pricing: OrderPricing,
payment: Payment
): Promise<ProcessedOrder> {
return orderRepository.updateStatus(order.id, {
status: 'completed',
total: pricing.total,
paymentId: payment.id
});
}
async function sendOrderConfirmation(order: ProcessedOrder): Promise<void> {
await emailService.sendOrderConfirmation(order);
}
async function trackOrderCompletion(order: ProcessedOrder): Promise<void> {
await analyticsService.track('order_completed', {
orderId: order.id,
total: order.total,
itemCount: order.items.length
});
}
Why
-
Readability: The main function reads like a story - fetch, validate, calculate, reserve, charge, finalize, notify.
-
Testability: Each function can be unit tested independently. Testing
calculateTaxdoesn't require mocking a database. -
Reusability:
calculateShippingcan be reused in a shipping estimate feature.validateOrderStatuscan be used in other order operations. -
Debugging: Stack traces point to specific functions. "Error in calculateSubtotal" is more helpful than "Error in processOrder at line 47".
-
Modification: Changing tax calculation only touches
calculateTax. Adding a discount feature can be inserted cleanly between subtotal and tax. -
Error Handling: Each function can have appropriate error handling. The main function can orchestrate rollback on failure.
-
Documentation: Function names serve as documentation. The code is self-explanatory without comments.