Single Responsibility Principle
Single Responsibility Principle (SRP) "A class should have one, and only one, reason to change." The Single Responsibility Principle states that a class should …
Single Responsibility Principle (SRP)
"A class should have one, and only one, reason to change."
The Single Responsibility Principle states that a class should have only one job or responsibility. If a class has multiple responsibilities, it becomes coupled, making it harder to change and maintain.
The Problem: Violating SRP
// ❌ BAD: This class has multiple responsibilities
class Employee {
constructor(
public name: string,
public email: string,
public salary: number
) {}
// Responsibility 1: Business logic
calculateBonus(): number {
return this.salary * 0.10;
}
// Responsibility 2: Database operations
save(): void {
const connection = openDatabaseConnection();
connection.execute(
`INSERT INTO employees VALUES ('${this.name}', '${this.email}', ${this.salary})`
);
connection.close();
}
// Responsibility 3: Email operations
sendWelcomeEmail(): void {
const smtp = new SMTPClient();
smtp.connect();
smtp.send(this.email, `Welcome ${this.name}!`);
smtp.disconnect();
}
// Responsibility 4: Report generation
generatePayslip(): string {
return `
PAYSLIP
Name: ${this.name}
Salary: $${this.salary}
Bonus: $${this.calculateBonus()}
Total: $${this.salary + this.calculateBonus()}
`;
}
}
// Why is this bad?
// 1. If database changes, we modify Employee class
// 2. If email system changes, we modify Employee class
// 3. If payslip format changes, we modify Employee class
// 4. Hard to test - need database and email setup for every test
// 5. Can't reuse email logic or database logic elsewhereThe Solution: Applying SRP
// ✅ GOOD: Each class has a single, well-defined responsibility
// Responsibility 1: Employee data and business logic
class Employee {
constructor(
public readonly id: string,
public name: string,
public email: string,
public salary: number
) {}
calculateBonus(): number {
return this.salary * 0.10;
}
getTotalCompensation(): number {
return this.salary + this.calculateBonus();
}
}
// Responsibility 2: Database operations
class EmployeeRepository {
private db: Database;
constructor(database: Database) {
this.db = database;
}
save(employee: Employee): void {
this.db.execute(
`INSERT INTO employees (id, name, email, salary)
VALUES (?, ?, ?, ?)`,
[employee.id, employee.name, employee.email, employee.salary]
);
}
find(id: string): Employee | null {
const result = this.db.query(
`SELECT * FROM employees WHERE id = ?`,
[id]
);
if (result.length === 0) return null;
return new Employee(
result[0].id,
result[0].name,
result[0].email,
result[0].salary
);
}
delete(id: string): void {
this.db.execute(`DELETE FROM employees WHERE id = ?`, [id]);
}
}
// Responsibility 3: Email operations
class EmailService {
private smtp: SMTPClient;
constructor(smtpConfig: SMTPConfig) {
this.smtp = new SMTPClient(smtpConfig);
}
sendWelcomeEmail(employee: Employee): void {
this.smtp.send(
employee.email,
'Welcome!',
`Welcome to the company, ${employee.name}!`
);
}
sendPayslipEmail(employee: Employee, payslip: string): void {
this.smtp.send(
employee.email,
'Monthly Payslip',
payslip
);
}
}
// Responsibility 4: Report generation
class PayslipGenerator {
generate(employee: Employee): string {
return `
═══════════════════════════════
MONTHLY PAYSLIP
═══════════════════════════════
Employee: ${employee.name}
ID: ${employee.id}
Base Salary: $${employee.salary.toFixed(2)}
Bonus: $${employee.calculateBonus().toFixed(2)}
───────────────────────────────
Total: $${employee.getTotalCompensation().toFixed(2)}
═══════════════════════════════
`;
}
}
// Usage: Each class is focused and easy to test
const employee = new Employee('1', 'John Doe', 'john@example.com', 50000);
const repository = new EmployeeRepository(database);
repository.save(employee);
const emailService = new EmailService(smtpConfig);
emailService.sendWelcomeEmail(employee);
const payslipGen = new PayslipGenerator();
const payslip = payslipGen.generate(employee);
emailService.sendPayslipEmail(employee, payslip);Benefits of SRP
Easier to understand: Each class does one thing, making code more readable
Easier to test: Can test each responsibility in isolation
Lower coupling: Classes are less dependent on each other
Easier to modify: Changes to one responsibility don't affect others
Better reusability: Focused classes can be reused in different contexts
Real-World Example: Order Processing
// ❌ BAD: Order class doing everything
class Order {
processOrder() {
// Validate order
// Calculate total
// Process payment
// Update inventory
// Send confirmation email
// Generate invoice
// Update analytics
}
}
// ✅ GOOD: Separate responsibilities
class Order {
constructor(
public id: string,
public items: OrderItem[],
public customerId: string
) {}
calculateTotal(): number {
return this.items.reduce((sum, item) => sum + item.getPrice(), 0);
}
}
class OrderValidator {
validate(order: Order): ValidationResult {
// Check if items are in stock
// Validate customer exists
// Check for minimum order amount
return { isValid: true, errors: [] };
}
}
class PaymentProcessor {
process(order: Order, paymentMethod: PaymentMethod): PaymentResult {
// Process payment through gateway
return { success: true, transactionId: 'TX123' };
}
}
class InventoryManager {
updateStock(order: Order): void {
order.items.forEach(item => {
// Decrease stock for each item
});
}
}
class OrderNotificationService {
sendConfirmation(order: Order): void {
// Send confirmation email to customer
}
}
class InvoiceGenerator {
generate(order: Order): Invoice {
// Generate PDF invoice
return new Invoice(order);
}
}
class AnalyticsTracker {
trackOrderPlaced(order: Order): void {
// Send event to analytics service
}
}
// Coordinator that uses all services
class OrderProcessor {
constructor(
private validator: OrderValidator,
private paymentProcessor: PaymentProcessor,
private inventory: InventoryManager,
private notifications: OrderNotificationService,
private invoiceGen: InvoiceGenerator,
private analytics: AnalyticsTracker
) {}
async process(order: Order, payment: PaymentMethod): Promise<void> {
const validation = this.validator.validate(order);
if (!validation.isValid) {
throw new Error('Order validation failed');
}
const paymentResult = await this.paymentProcessor.process(order, payment);
if (!paymentResult.success) {
throw new Error('Payment failed');
}
this.inventory.updateStock(order);
this.notifications.sendConfirmation(order);
this.invoiceGen.generate(order);
this.analytics.trackOrderPlaced(order);
}
}How to Identify SRP Violations
Class name contains "and" (e.g., UserAndPermissionManager)
Class has too many methods (rule of thumb: >10 methods)
Class changes for multiple unrelated reasons
Difficult to name the class concisely
Class depends on many different libraries or frameworks