This is part one of the SOLID OOP design pattern series. You may wish to subscribe to the blog to stay tuned in as I post new issues in this series.
SOLID is not a language, it’s not a framework, it’s a set of principles (practices) to help guide your team to design better applications. Using these principles will help ensure your code is adaptive to change. In this series, I’ll be using C# for examples, but the same principles apply to any OOP language: Python, Java, C++, etc.. Using SOLID design principles does require a fundamental understanding of classes, interfaces, and class inheritance.
The first letter, ‘S’, in our acronym stands for SRP: Single Responsibility Principle and states: “A class should have only a single responsibility.” I’m not a fan of this definition. It’s vague, the word “responsibility” implies too many things. Your code should be responsible for many things: for running, for not containing bugs, for naming conventions, etc. What this principle is trying to articulate is a class should have only one reason to change. Let’s jump into an example to better understand what this means.
Let’s say the company we work for hasn’t purged termed employees from their HRM software in over 10 years. The software now crawls through hundreds of thousands of records making it impossible for payroll to complete their work by their deadlines. Some employees are understandably furious when they receive their paycheck weeks late. Something must be done. Luckily, our company has a great development team :). After we’ve spoken with our coworkers in payroll, we’ve agreed on the requirements for a solution: We’ll update payroll’s HRM software to purge termed employees every 180 days and move old records to a separate database for backup. This will keep the system up-to-date and running smoothly.
If we work in a team new or unfamiliar with programming principles, it’s likely we’ll see a similar solution to the following:
UML Class Diagram
Pseudo Sample Code:
Looking at this code, how many reasons for change can you think of? How many different requirement changes could be made that would force your team to change this code?
Here’s a few to get started:
- HR wants to purge employees from a provided list instead of relying on DB record details
- HR wants two options: purge after 180 day and 90 day. The first for full-time employees and the other for contract employees.
- HR wants to delete all records/backups after an employee record has been purged for 30 days
Each one of the requests above would mean altering or, perhaps worse, repeating our class object: CleanRecords.cs. Currently, our class has more than one reason to change. For such a simple class, this might not seem like a big deal, but altering your code to add/change requirements leads to overly-complex logic, aka spaghetti code. Altering your code also makes it more likely you’ll break existing functionality and we want to remove any likelihood of this when, not if, requirements change. .
We’ll need to change the class so that it only has one reason to change. We’ll separate each responsibility, each reason for change, into separate classes, a process known as encapsulating. Once we’ve encapsulated our employee, backup and delete logic into separate classes, our new CleanRecords.cs class will have one responsibility:
Our class is no longer responsible for where and how employees are set and it’s no longer responsible for any database logic. It’s only reason to change now is if the process itself changes. The classes single method, Purge, accepts three parameters: an employees list interface, a backup interface, and a delete interface. Let’s first look at the interfaces and then what value we get out of this structure.
IGetEmployees | IDelete | IBackup
Using these interfaces as parameter types in our CleanRecords.cs’ Purge method allows us to “plug-and-play” inheriting classes . As requirements change, we can call the Purge method any way we’d like. For example, we could call the employees from the DB like so:
Purge(EmployeesFromDB, BackupEmployees, DeleteEmployees)
If HR wanted to use a file instead of getting employees from the DB, we could call the exact same method and pass in a class importing employees from file:
Purge(EmployeesFromFile, BackupEmployees, DeleteEmployees).
We could change data sources, databases, etc. All without changing a single part of our existing architecture or disrupting working code in our application.
Your classes and interfaces should be so simple, that it becomes hard (almost impossible) to write bad code. With SRP we get the benefit of modular parts we can interchange. It forces us to write simpler code making our application more maintainable. We no longer require ourselves to change our main class or interfaces when requirements change. Instead, we can now add functionality to our existing/working code.
Next week we’ll continue our SOLID principles building on what we’ve learned today with the OCP: Open/Closed Principle. See you then!