Skip to main content

Solid Principles

Single responsibility principle

S — SRP — Single responsibility principle

Definitions

A class or function or modules should have only one reason to change.A class or function or modules should have only one responsibility.Gather together the things that change for the same reasons. Separate things that change for different reasons.Each module should do one thing and do it well.

This principle is closely related to the topic of high cohesion. Essentially, your code should not mix multiple roles or purposes together.

Object oriented example

/***
  Violating SRP, Order Class/ createOrder doing to many things.
*/
public class OrderService {
  public void createOrder(Order order) {
        // Validate order data
        validateOrder(order);
        // Calculate total price
        double totalPrice = calculateTotalPrice(order);
        order.setTotalPrice(totalPrice);
        // Persist order to database
        try {
            databaseConnection.persistOrder(order);
        } catch (SQLException e) {
            throw new OrderCreationException("Failed to persist order", e);
        }
        // Send order confirmation email
        emailService.sendOrderConfirmationEmail(order);
    }
    // ... other methods for order retrieval, updates, etc.
}/***
Adhering to SRP, broken down into smaller classes with specific responsbility.
*/

// OrderValidator class for validation
public class OrderValidator {
    public void validate(Order order) {
        // ... validation logic
    }
}
// OrderRepository class for database interactions
public class OrderRepository {
    public void save(Order order) {
        // ... database persistence logic
    }
}
// EmailService class for sending emails
public class EmailService {
    public void sendOrderConfirmationEmail(Order order) {
        // ... email sending logic
    }
}
public class OrderService {
    private final OrderValidator orderValidator;
    private final OrderRepository orderRepository;
    private final EmailService emailService;
    public OrderService(OrderValidator orderValidator, OrderRepository orderRepository, EmailService emailService) {
        this.orderValidator = orderValidator;
        this.orderRepository = orderRepository;
        this.emailService = emailService;
    }
    public void createOrder(Order order) {
        // Validate order data
        orderValidator.validate(order); // Validating through OrderValidator
        // Persist order to database
        orderRepository.save(order);
        // Send order confirmation email
        emailService.sendOrderConfirmationEmail(order);
    }
}

Benefits of SRP in above code

  • Improved readability and maintainability: Each class has a clear purpose and responsibility.
  • Enhanced testability: Classes can be tested independently.
  • Reduced coupling: Changes in one area are less likely to affect other parts of the system.
  • Promotes reusability: Components can be used in different contexts.
  • Encourages better fault isolation: Issues can be traced more easily to specific components.

How can we apply this in functional programming. Let’s take the same example in functional programming.

Functional programming example.


/***
Violating SRP
*/
function createOrder(order) {
  // Validate order data
  if (!validateOrder(order)) {
    throw new Error("Invalid order data");
  }
// Calculate total price
  const totalPrice = calculateTotalPrice(order);
  // Persist order to database
  persistOrder(order, totalPrice)
    .then(() => {
      // Send order confirmation email
      sendOrderConfirmationEmail(order);
    })
    .catch((error) => {
      console.error("Failed to create order:", error);
    });
}/***
Adhering to SRP
*/
const createOrder = pipe(
  validateOrder,
  withOrderData(calculateTotalPrice),
  persistOrder,
  then(sendOrderConfirmationEmail)
);

// Helper functions for composition and error handling
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
const withOrderData = (fn) => (order) => ({ ...order, result: fn(order) });
const then = (fn) => (result) => result.then(fn); // Assuming promise-based functions
// Individual functions with clear responsibilities
function validateOrder(order) {
  // ... validation logic
  if (!isValid) {
    throw new Error("Invalid order data");
  }
  return order;
}
function calculateTotalPrice(order) {
  // ... calculation logic
  return totalPrice;
}
function persistOrder(order) {
  // ... database persistence logic
  return promise; // Assuming returns a promise
}
function sendOrderConfirmationEmail(order) {
  // ... email sending logic
}

Benefits of SRP in above code

  • Enhanced readability: The code structure clearly conveys the order of operations and data flow.
  • Improved testability: Individual functions can be tested in isolation.
  • Flexibility: Functions can be easily reused and combined in different ways.
  • Decoupled logic: Changes to one function don’t affect others, promoting maintainability.
  • Encourages functional programming paradigms: Promotes immutability, pure functions, and declarative style.

Here are some guidelines to help you follow the Single Responsibility Principle (SRP):

1. Identify Reasons to Change:

  • Analyze classes and modules for potential “reasons to change.”
  • Ask yourself: “What might cause this code to be modified in the future?”
  • If a class has multiple reasons to change, it likely violates SRP.

2. Cohesive Responsibilities:

  • Ensure each class or module has a single, well-defined responsibility.
  • Its methods and properties should all be closely related to this core responsibility.
  • Avoid classes that perform unrelated tasks or handle multiple concerns.

3. Single Reason to Change:

  • Strive for classes that have only one reason to change.
  • If a change in one requirement necessitates modifying multiple classes, SRP is likely violated.

4. Separate Concerns:

  • Decompose classes with multiple responsibilities into smaller, more focused classes.
  • Each new class should have a single, cohesive responsibility.

5. Delegate and Compose:

  • Use delegation and composition to combine classes with distinct responsibilities.
  • This promotes loose coupling and avoids tight dependencies.

6. Consider Interfaces:

  • Utilize interfaces to define contracts for classes with specific roles.
  • This enables flexibility and easier testing.

7. Refactor Proactively:

  • Regularly review code for SRP violations.
  • Refactor classes when they accumulate multiple responsibilities over time.

Additional points:

  • Naming: Use clear and descriptive names for classes and modules that reflect their responsibilities. This helps in improving readability of code.
  • Testing: SRP makes code easier to test as each class can be tested in isolation.
  • Balance: Strive for a balance between granularity and practicality. Avoid overly small classes for simple tasks.
  • Experience: Applying SRP effectively requires practice and judgment.

Remember: SRP is not a strict rule but a guideline to produce more maintainable, flexible, and testable code. By following these guidelines, you can create software that is easier to understand, evolve, and manage over time.