Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 44

SOLID Principles: SRP & OCP in Practice

Recommended resources

Hello! Welcome to a new module focused on a cornerstone of modern software design: the SOLID principles.

In our previous modules, we took a deep dive into data structures and algorithms, building the computational foundation for writing efficient code. Now, we're shifting our focus to a higher level of abstraction. This module is designed to help you cultivate the "architectural mindset" you're aiming for. Instead of asking "How can I make this code work?", we will start asking, "How can I structure this code so it is robust, easy to maintain, and simple to extend in the future?"

This lesson kicks off our journey with the first two SOLID principles: the Single Responsibility Principle (SRP) and the Open/Closed Principle (OCP). By the end of this lesson, you will be able to explain what these principles are, why they are crucial for good design, and how to apply them to refactor a piece of code from a state of poor design to one that is clean, modular, and extensible.

1. The Single Responsibility Principle (SRP)

The Single Responsibility Principle is perhaps the most fundamental of the five. It states:

A class should have one, and only one, reason to change.

This means a class should have only one job or responsibility. If a class is responsible for multiple, distinct tasks, a change in the requirements for one task can unintentionally break the logic for another. Adhering to SRP leads to classes that are smaller, more focused, and easier to understand, test, and maintain.

SOLID Design Principles Explained: Single-Responsibility Principle

To start, let's explore a clear definition of SRP with a simple, illustrative example. This article from DigitalOcean demonstrates a class that violates SRP by both calculating areas and formatting the output, and then shows how to fix it by separating these concerns.

Read the section 'Single-Responsibility Principle'. Pay close attention to the two stages of refactoring: first, moving the area calculation into the shape classes themselves, and second, creating a separate SumCalculatorOutputter class. This clearly shows how to identify and separate different 'reasons to change'.

The core idea is to achieve high cohesionโ€”ensuring that the methods and properties within a class are all related to a single, well-defined purpose.

Applying the Single Responsibility Principle to an Employee Class
This diagram shows a classic SRP refactoring. An `Employee` class initially handles employee data, tax calculations, and promotion logic. This violates SRP because tax policy and promotion rules are separate responsibilities from managing core employee data. The refactored design separates these concerns into dedicated `FinITCalculations` and `HRPromotions` classes, leaving the `Employee` class with the single responsibility of managing employee information.

As a web developer, you've likely encountered this. For example, a single UserService class that queries the database for user data, validates user input from a web form, sends a welcome email, and logs user activity clearly has too many responsibilities. According to SRP, the email-sending logic, validation logic, and logging logic should each be in their own dedicated classes.

Test your understanding!

You are building a ReportGenerator class. Its generate method fetches data from a database, processes the data into a summary, and then writes the summary to a PDF file. How many responsibilities does this class have, and what are they? How would you refactor it to adhere to SRP?

Show answer

The ReportGenerator class has three distinct responsibilities:

  1. Data Fetching: Interacting with the database.
  2. Data Processing: Performing the business logic to create the summary.
  3. Data Formatting/Persistence: Writing the summary to a PDF file.

Each of these is a "reason to change." The database schema could change, the summary logic could change, or you might need to support a new output format (like CSV or HTML).

A proper SRP refactoring would involve creating separate classes for each responsibility:

  • ReportRepository: Responsible for fetching data from the database.
  • ReportProcessor: Responsible for taking raw data and generating the summary.
  • PdfWriter (or a more general ReportWriter): Responsible for writing the processed data to a file.

The ReportGenerator class would then act as a coordinator, orchestrating the calls to these other classes.


2. The Open/Closed Principle (OCP)

The second principle, OCP, builds on the idea of creating stable, maintainable systems. It states:

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

This means you should be able to add new functionality to a system without changing existing, working code. Every time you modify existing code, you risk introducing bugs. OCP encourages you to design your components so that new features can be added by writing new code (e.g., new classes) that plugs into the existing structure.

Code Example Violating Open/Closed Principle
This image shows a C# method `LogMovement` that uses a long `if-else if` chain to handle different types of sports. This is a classic OCP violation. To add a new sport, like 'Tennis', you would have to modify this method directly, which risks breaking the existing logic for 'Football' or 'Hockey'. The code is not 'closed for modification'.

How do we achieve this? The most common way is by programming to abstractions (interfaces or abstract classes), not to concrete implementations.

SOLID Design Principles Explained: Open-Closed Principle

Let's revisit the AreaCalculator example from the DigitalOcean article to see how it also violates OCP and how to fix it.

Read the section 'Open-Closed Principle'. Notice how the initial sum method, with its if/elseif checks for Square and Circle, must be modified every time a new shape is added. The solution is to introduce a ShapeInterface. This decouples the AreaCalculator from concrete shapes, allowing it to work with any new shape that implements the interface, thus being 'open' to extension without 'modification'.

Test your understanding!

You have a PaymentProcessor class with a method processPayment(paymentType, amount). Inside, it uses a switch statement on paymentType ("credit_card", "paypal", "stripe"). The company now wants to add Apple Pay. Why does this design violate OCP, and what is the general approach to fix it?

Show answer

This design violates OCP because adding Apple Pay requires modifying the existing processPayment method to add a new case to the switch statement.

The general approach to fix this is to use polymorphism:

  1. Define a PaymentProvider interface with a pay(amount) method.
  2. Create concrete classes for each payment method that implement this interface: CreditCardProvider, PayPalProvider, StripeProvider, and a new ApplePayProvider.
  3. The PaymentProcessor would then accept an object of type PaymentProvider and simply call its pay(amount) method, without needing to know the concrete type. New payment methods can be added by creating new classes, without ever touching PaymentProcessor.

3. Applying SRP and OCP: A Practical Refactoring

Theory is great, but seeing these principles applied to a real-world coding problem is where the understanding solidifies. We are going to watch a detailed walkthrough of a refactoring exercise. The initial code has a method that violates both SRP and OCP. We'll see how to fix it step-by-step.

The example is in C#, but the concepts are universal and the syntax is very similar to other C-family languages you might know from your web development experience. Focus on the design patterns and why changes are being made, not the specific syntax.

Step 1: Applying SRP - Separating Validation

First, let's look at the AddUser method. It's responsible for adding a user, but it also contains a lot of complex validation logic. This is a clear SRP violation. The method has two reasons to change: if the user creation process changes, or if the validation rules change.

The refactoring test (2) - Open-Closed, Single Responsibility | Cracking the .NET interview

In this first part of the video from Nick Chapsas, we'll see how to identify the SRP violation and refactor the code by extracting the validation logic into its own dedicated UserValidator class.

Watch from 02:42 to 10:36. Observe how the presenter moves all the validation checks out of the AddUser method and into a new class. This makes the AddUser method cleaner and more focused on its single responsibility.

Step 2: Applying OCP - Removing the if/else Chain

Now, look at the main part of the AddUser method. There's a large if/else if/else block that changes the user's credit limit based on the client's name ("VeryImportantClient", "ImportantClient", etc.). This is a textbook OCP violation. Every time a new special client type is introduced, this method must be modified.

The solution is to use a strategy-like pattern. We will define an interface for providing credit limits and create separate classes for each client type. A "factory" will then be responsible for choosing the correct strategy at runtime.

The refactoring test (2) - Open-Closed, Single Responsibility | Cracking the .NET interview

This is the core of the refactoring. Let's see how to dismantle the rigid if/else structure and replace it with a flexible, extensible design that adheres to OCP.

Watch from 10:36 to 30:23. This is a dense but incredibly valuable section. Focus on these key architectural changes: The Violation (10:36 - 13:00): The presenter identifies the if/else block as a clear OCP violation. The Abstraction (13:00 - 15:19): An ICreditLimitProvider interface is created. This defines a contract for any class that can provide credit limit details. The Concrete Implementations (15:19 - 20:50): Separate classes (DefaultCreditLimitProvider, ImportantClientCreditLimitProvider, etc.) are created, each implementing the interface with its specific logic. The Factory (20:50 - 30:23): A CreditLimitProviderFactory is built to select the appropriate provider class based on the client name. This decouples the main service from the concrete providers. Don't get too bogged down in the C# reflection details toward the end. The key takeaway is the final design: the AddUser method no longer needs to be modified to support new client types. You just add a new class that implements the ICreditLimitProvider interface, and the factory picks it up automatically.

This refactoring perfectly demonstrates the shift to an architectural mindset. We've replaced a rigid, fragile piece of code with a flexible, pluggable system that is open for extension but closed for modification.


Conclusion

In this lesson, we've taken our first steps into the world of high-level software design. By understanding and applying just the first two SOLID principles, we can dramatically improve the quality of our code.

Key Takeaways:

  • Single Responsibility Principle (SRP): A class should have only one reason to change. This leads to smaller, more cohesive classes that are easier to manage.
  • Open/Closed Principle (OCP): Software should be open for extension but closed for modification. This is typically achieved using abstractions (interfaces/abstract classes) to allow for new functionality to be "plugged in" without altering existing code.
  • Refactoring: We saw how to apply these principles to transform code with if/else chains and mixed responsibilities into a clean, modular design using focused classes, interfaces, and factories.

Preview of the Next Lesson:
Our journey through SOLID continues. In the next lesson, we will explore the Liskov Substitution Principle (LSP) and the Interface Segregation Principle (ISP). These principles refine how we should design and use our abstractions (interfaces), ensuring that our flexible systems are also correct and easy to use.