This is the second article in our S.O.L.I.D Principles Using C# series. In the previous article, we discussed an introduction to S.O.L.I.D principles. In this post, we will dive deeper into the S in S.O.L.I.D, the Single Responsibility Principle (SRP), using examples in C#.
Single Responsibility Principle (SRP)
The Single Responsibility Principle (SRP) is one of the five SOLID principles of object-oriented programming (OOP). It was first introduced by Robert C. Martin, also known as Uncle Bob. SRP states that a class should have only one reason to change, meaning that it should have only one responsibility or job to do.
In other words, a class should do one thing and do it well. This makes the class easier to understand, test, maintain, and reuse. If a class has multiple responsibilities, it becomes more difficult to modify without introducing bugs or unexpected behavior.
Let's delve deeper into understanding SRP with a practical example in C#.
Example Scenario
Suppose we are building a library management system where users can borrow books, renew their loans, pay fines, etc. We might start by creating a Library class that handles all these operations:
This implementation seems simple enough, but let's analyze its violations of SRP:
- The BorrowBook method checks if a user exists, if a book exists, if a book is available, updates the book status, adds a loan record, and throws exceptions on failure. That's at least four different reasons to change, such as adding validation rules, updating the database schema, changing error messages, etc.
- The RenewLoan method checks if a user exists, if a book exists, if a book is checked out by this user, updates the loan due date, and throws exceptions on failure. Again, that's several different reasons to change, such as changing the loan policy, updating the database schema, changing error messages, etc.
- The PayFine method checks if a user exists, calculates the fine balance, validates payment amounts, deducts payments, and throws exceptions on failure. Yet another set of reasons to change, such as changing the fine calculation formula, updating the currency format, changing error messages, etc.
Each method has multiple responsibilities that could change independently, making it harder to isolate and manage changes. Moreover, each method contains duplicate code for checking if a user exists, which increases complexity and maintenance costs.
Applying SRP
To fix these issues, we need to refactor our code according to SRP. Here's an improved version:
Here's what changed
- We separated concerns into smaller classes, each having only one responsibility, following SRP. For instance, LoanManager manages loans, while PaymentProcessor handles financial transactions. Each class depends on abstractions rather than concrete implementations, adhering to Dependency Injection Principle (We will cover DIP in a later article).
- Instead of duplicating code for existence checks, we created two repository classes that handle data access logic for users and books. These repositories expose methods like Exists(), GetById(), etc., which simplify usage within higher-level classes like LoanManager.
- Within LoanManager, we removed redundant exception throwing in favor of simpler argument checking. Also, we moved some business logic related to loan expiration into separate methods like HasActiveLoanForBook and GetActiveLoanForBook into the User class (not shown here), further improving readability and modularity.
By applying SRP and separating concerns into smaller classes, we made our codebase more robust, extensible, and maintainable, reducing potential side effects when making modifications.
Summary
The Single Responsibility Principle is a fundamental concept in software development that promotes better design, maintainability, and scalability of code. By adhering to SRP, you can create classes that are focused, reusable, and easier to maintain, leading to more robust and flexible applications.