Introduction
The Single Responsibility Principle (SRP) is a powerful concept that transcends programming paradigms. In functional programming, it leads to code that's more maintainable, testable, and composable. Let's explore how to apply SRP effectively through concrete examples.
Understanding SRP in Functional Terms
A function with single responsibility:
- Has one clear purpose
- Performs a single transformation
- Can be described without using "and"
- Changes for only one reason
Well-Designed Function
// Does one thing: calculates area
const calculateCircleArea = radius => Math.PI * radius ** 2;
Poorly Designed Function
// Does too much: validates AND calculates AND formats
function processCircle(radius) {
if (typeof radius !== 'number') throw new Error('Invalid radius');
const area = Math.PI * radius ** 2;
return `The area is ${area.toFixed(2)}`;
}
When to Apply SRP Strictly
1. Business Logic
// Pure functions for financial calculations
const calculateMonthlyPayment = (principal, rate, term) => {
const monthlyRate = rate / 12 / 100;
return (principal * monthlyRate) /
(1 - Math.pow(1 + monthlyRate, -term));
};
const calculateTotalInterest = (payment, term, principal) => {
return payment * term - principal;
};
2. Data Transformations
// Single-purpose data processors
const normalizeName = name => name.trim().toLowerCase();
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
const formatUserName = compose(capitalize, normalizeName);
3. Validation Logic
// Focused validation functions
const isEmailValid = email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const isPasswordStrong = password => password.length >= 8;
const validateCredentials = (email, password) =>
isEmailValid(email) && isPasswordStrong(password);
4. Pure Utility Functions
// Reusable utilities with single responsibility
const toDollars = amount => `$${amount.toFixed(2)}`;
const truncate = (str, max) =>
str.length > max ? `${str.slice(0, max)}...` : str;
When to Relax SRP
1. Performance Optimization
// Combined operation for efficiency
function analyzeArray(arr) {
return arr.reduce((stats, num) => {
stats.sum += num;
stats.min = Math.min(stats.min, num);
stats.max = Math.max(stats.max, num);
return stats;
}, { sum: 0, min: Infinity, max: -Infinity });
}
2. Simple Data Processing
// Reasonable combination for simple cases
function formatProduct(product) {
return {
id: product.id,
name: product.name.trim(),
price: `$${product.price.toFixed(2)}`,
inStock: product.quantity > 0
};
}
3. Domain Operations
// Combined domain-specific action
function placeOrder(cart, payment) {
const subtotal = calculateSubtotal(cart.items);
const tax = calculateTax(subtotal);
const order = createOrder(cart, payment, subtotal + tax);
return processPayment(order);
}
4. Prototyping
// Quick prototype version
function analyzeText(text) {
const words = text.split(/\s+/);
return {
wordCount: words.length,
avgLength: words.reduce((sum, w) => sum + w.length, 0) / words.length,
sentences: text.split(/[.!?]+/).length
};
}
Composing Single-Purpose Functions
The real power emerges when combining small, focused functions:
// Small building blocks
const filterActive = users => users.filter(u => u.isActive);
const sortByDate = users => [...users].sort((a, b) => b.joined - a.joined);
const limit = n => users => users.slice(0, n);
// Composed pipeline
const getRecentActiveUsers = compose(
limit(5),
sortByDate,
filterActive
);
Refactoring to SRP
Here's how to transform a multi-responsibility function:
// Before
function processUser(user) {
if (!user.name || !user.email) throw new Error('Invalid user');
const formattedName = user.name.trim().toUpperCase();
const formattedEmail = user.email.trim().toLowerCase();
return {
...user,
name: formattedName,
email: formattedEmail,
welcomeMessage: `Hello ${formattedName}!`
};
}
// After
const validateUser = user => {
if (!user.name || !user.email) throw new Error('Invalid user');
return user;
};
const formatName = name => name.trim().toUpperCase();
const formatEmail = email => email.trim().toLowerCase();
const createWelcome = name => `Hello ${name}!`;
const processUser = user => {
const validUser = validateUser(user);
const name = formatName(validUser.name);
const email = formatEmail(validUser.email);
return {
...validUser,
name,
email,
welcomeMessage: createWelcome(name)
};
};
Finding the Right Balance
Effective functional programming with SRP means:
- Start small: Create focused functions for complex logic
- Compose thoughtfully: Build larger operations from small pieces
- Be pragmatic: Combine simple operations when it improves clarity
- Refactor when needed: Split functions as responsibilities emerge
Remember that SRP serves the larger goals of maintainability and reliability. The best functional code balances separation of concerns with practical readability.