π Demystifying the Single Responsibility Principle (SOLID) π
It's time to clarify one of the most common misconceptions surrounding the Single Responsibility Principle (SRP). Many of us believe that SRP is all about doing only one thing. While itβs a key aspect, SRP goes beyond that, emphasizing a critical aspect of code design: change management, and to be totally honest I used to explain it that way too π until I read about the original intent from Robert C. Martin π
The essence of SRP lies in the principle that a piece of code, whether it's a class, file, or module, should have only one reason to change. In other words, it should respond to modifications initiated by a single actor or a single kind of change.
π€Ώ Let's dive deeper into the concept:
1οΈβ£ Responsibly managing change: SRP advocates for separating concerns, enabling us to isolate and encapsulate parts of our code that are likely to change due to different reasons. By assigning specific responsibilities to each component, we ensure that changes initiated by one actor or reason do not affect unrelated parts of the codebase.
2οΈβ£ Enabling maintainability: Embracing SRP fosters maintainable codebases. When each component has a clear, well-defined responsibility, it becomes easier to understand, test, and modify specific parts of the system without unintended side effects. This promotes modularity, reusability, and reduces the risk of introducing bugs when making changes.
3οΈβ£ Promoting code readability: SRP improves the readability of our code. When each class, file, or module has a single responsibility, it becomes easier to comprehend its purpose and functionality. This enhances collaboration among team members, simplifies debugging, and contributes to the overall quality of the codebase.
π Implementing SRP effectively:
β Identify potential actors of change: Analyze your codebase to identify the various actors or reasons that might initiate modifications. These can include business requirements, user interface changes, performance improvements, etc.
β Encapsulate responsibilities: Once you've identified the actors, encapsulate their responsibilities into separate components. Each component should have a well-defined purpose and handle changes initiated by a specific actor or reason.
β Balance granularity and cohesion: Strive for an appropriate balance between granularity and cohesion. While it's important to have focused components, overly granular ones can lead to unnecessary complexity. Aim for cohesive components that encapsulate related responsibilities.
β Embrace iterative refinement: The SRP is not a one-time process; it evolves with your codebase. Regularly review your components, ensuring they adhere to the principle and refactor as needed. Iteratively refining your codebase helps maintain a healthy and adaptable architecture.
Let's dive into an illustration:
Imagine we're developing an educational application that involves a user model representing individuals who interact with the app.
Later on, a new requirement emerges, asking for the ability of certain users (teachers) to create new courses.
At this point, you're faced with a critical decision. If you fail to ask the right questions, you may end up extending the user model to accommodate the new requirement for teachers.
As time passes, the application grows, and we encounter another type of user, students. Unfortunately, we end up including them in the user model as well.
While you might convince yourself that you have different functions performing distinct tasks for each user type and that you're adhering to SRP, it's actually incorrect. The user model can change due to various actors, such as admin, teachers, or students, leading to an improper application of SRP.
This situation worsens when the company expands, and different teams oversee different areas, like admin, teachers, or students. Since everything is consolidated in one place, two main problems arise: making a change that only affects teachers but unintentionally alters the behavior of other user types, or encountering merge conflicts when working on code influenced by requirements from different actors.
However, this scenario shouldn't be surprising, as it's what we're used to. So, let's reconsider the same scenario with the correct application of SRP.
In our educational app, we have the user model and a new requirement arises to handle teachers.
Instead of directly incorporating the code into the user model, you pause and ask yourself:
1οΈβ£ Are the user and teacher related?
2οΈβ£ Could a new requirement apply solely to users or teachers?
3οΈβ£ Does changing the user impact the teacher, and vice versa? Is that desirable or undesirable?
4οΈβ£ In terms of the user interface, should we treat users and teachers identically?
If, at this point, you determine that they are distinct or that multiple actors can influence the code, it's a clear indication to separate them.
After answering these questions, you decide to split the teachers into separate classes, files, or modules.
By the time the student features are introduced, we already have a code structure that encourages placing them in different locations. Even if you're uncertain, you can repeat the process of questioning whether more than one actor can potentially change the code you're considering placing in the user model, and proceed accordingly.
π€ By embracing the true essence of the Single Responsibility Principle, we can enhance our codebases, promote maintainability, and foster collaboration among development teams.