Clean Code: Functions Should Do One Thing
Functions are the building blocks of our programs. Good functions are like well-designed tools: they do one thing, and they do it well. When you write functions that are small and focused, your code becomes easier to read, test, and debug.
Let's explore some examples in TypeScript to see how we can improve our functions.
One Function, One Responsibility
Imagine a function that tries to do everything. It's hard to understand, hard to test, and a nightmare to change. Let's look at an example that handles different types of orders.
// Bad: This function is doing too much. It handles both online and in-store orders.
function calculateOrderSummary(orders: any[]): any[] {
const summaries = [];
for (const order of orders) {
// ... logic for different order types
if ((order as any).type === 'online') {
// ...
} else if ((order as any).type === 'inStore') {
// ...
}
// ... more logic
}
return summaries;
}
// Good: We've split the logic into smaller, focused functions.
interface OnlineOrder {
id: string;
type: 'online';
total: number;
customerEmail: string;
}
interface InStoreOrder {
id: string;
type: 'inStore';
total: number;
storeLocation: string;
}
type Order = OnlineOrder | InStoreOrder;
interface OrderSummary {
id: string;
total: number;
channel: string;
}
function summarizeOnlineOrder(order: OnlineOrder): OrderSummary {
return {
id: order.id,
total: order.total,
channel: `Online (${order.customerEmail})`,
};
}
function summarizeInStoreOrder(order: InStoreOrder): OrderSummary {
return {
id: order.id,
total: order.total,
channel: `In-Store (${order.storeLocation})`,
};
}
function calculateOrderSummary(orders: Order[]): OrderSummary[] {
return orders.map(order => {
if (order.type === 'online') {
return summarizeOnlineOrder(order);
} else {
return summarizeInStoreOrder(order);
}
});
}
By breaking down the monolithic function, we now have smaller functions that are easy to understand and test individually. The main function, calculateOrderSummary
, is now just a coordinator, delegating the work to the appropriate specialist function.
Keep Them Small
A good rule of thumb is that a function should be small enough to fit on your screen without scrolling. This isn't a hard rule, but it's a good guideline. If a function is getting too long, it's probably doing too much.
// Bad: A long function with multiple logical steps.
function process(data: any[]): any[] {
const result = [];
for (const item of data) {
if (item.type === 'A') {
// ... 10 lines of logic for type A
} else if (item.type === 'B') {
// ... 10 lines of logic for type B
}
}
return [];
}
// Good: The logic is extracted into smaller, well-named functions.
function processTypeA(item: any): any {
// ... 10 lines of logic for type A
}
function processTypeB(item: any): any {
// ... 10 lines of logic for type B
}
function process(data: any[]): any[] {
const result = [];
for (const item of data) {
if (item.type === 'A') {
result.push(processTypeA(item));
} else if (item.type === 'B') {
result.push(processTypeB(item));
}
}
return result;
}
Tips for Clean Functions
- Do one thing: Each function should have a single, clear purpose.
- Use descriptive names: The name of a function should clearly state what it does.
- Keep arguments to a minimum: Ideally, a function should have zero, one, or two arguments. Any more than that, and you should consider passing an object instead.
- No side effects: A function should ideally not change the state of things outside of it.
Writing clean functions is a habit. The more you practice it, the more natural it becomes. Your future self will thank you for it.
// Bad
function process(data: any[]): any[] {
const result = [];
for (const item of data) {
if (item.type === 'A') {
// ... perform logic specific to type A
} else if (item.type === 'B') {
// ... perform logic specific to type B
}
// ... more logic
}
return result;
}
// Good
function processTypeA(item: any): any {
// ... perform logic specific to type A
}
function processTypeB(item: any): any {
// ... perform logic specific to type B
}
function process(data: any[]): any[] {
const result = [];
for (const item of data) {
if (item.type === 'A') {
result.push(processTypeA(item));
} else if (item.type === 'B') {
result.push(processTypeB(item));
}
}
return result;
}