You’re on your second cup of coffee. You have two screens open trying to understand this class that was written 3 years ago, by somebody who no longer works for the company. You take a step back, scratch some facial hair, then eye stab the monitor once again. This typically happens for the next few minutes as you try to figure out where to actually make those dam changes.
Those actions are typically associated with software that goes through “The Transformation”. In the beginning, it was perfectly architected and designed. It got taste, it was elegant and code sponsored by Mr. Clean. As if the Gods were insulted by such a design, this well-designed software starts to change, of course, it doesn’t happen all at once. *cue dramatic music* as the sky darkens, little baby thunders start rolling around and now the once-beloved system starts becoming…rigid and fragile.
The cause of the degradation of software design is well understood. Requirements change in ways that were not intended by the original design. New engineers have to make quick changes to a design they are not familiar with. Over time, we get software that is hard to maintain and eventually needs to be redesigned.
Be that as it may, requirements changing should no longer be the excuse we tell ourselves when software becomes hard to maintain. By now, we know requirements will ALWAYS change from this day until the end of days. So we should design our system in a way that is resilient to change. Using SOLID Design Principles can help us design more robust software.
In this article, we will discuss:
- What are SOLID Design Principles
- Benefits of using SOLID Design Principles
- How to apply SOLID Design Principles to your project
What are SOLID Design Principles?
SOLID is one of the most popular sets of design principles in object-oriented software development that is intended to make software easier to understand and more maintainable. It’s a mnemonic acronym that stands for:
- Single Responsibility Principle
- Open Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Injection
These principles were introduced by Robert C. Martin (Uncle Bob), in his paper Design Principles and Design Patterns, and later named by Michael Feathers.
Benefits of using SOLID Design Principles
Applying these principles will enable you to write better software and cleaner code, therefore, become a better developer.
The intention of these principles is to make software designs more understandable, easier to maintain and easier to extend. Overall SOLID Design Principles help us to obtain the following:
- Makes code more maintainable
- Easier to extend software functionality
- Easier to read and understand
- Reduces the introduction of new bugs
- Makes software more robust
How to apply SOLID Design Principles to your project
Single Responsibility Principle
A class should have one, and only one, reason to change
A class should do ONE THING. Otherwise, will we have tight coupling between modules and our code will become brittle. The aim of this approach is to have high cohesion. The term cohesion is used to indicate the degree to which a class has a single, well-focused purpose.
Here you can see that this Student class has multiple responsibilities that violate Single Responsibility Principles.
storeStudent(Student student) // line 12
This function has the responsibility of storing a student in a database. This functionality of persistence should be outside of the student class.
generateStudentReport(String studentId) // line 22
This function has the responsibility of creating a report about a student. This functionality should also be outside the student class.
getStudentById(String studentId) // line 17
This function has the responsibility of getting a student from a database. This functionality should also be outside the student class.
Instead, we should move these unwanted functions from outside the student class into separate classes themselves. This way we can keep student class lean and focused on just representing a student model.
Our improved Student class
Create a StudentDatabase class to focus only on storing and retrieving student information
Create a StudentReportGenerator class to focus only on generating reports for students.
This way each class has a singular focus and doesn’t have functionality that overlaps.
Open Closed Principle
A module should be open for extension but closed for modification.
This is one of the most important principles in object-oriented design. We want to change what the modules do, without changing the actual source code of the modules. The idea is that we should be able to easily add new functionality/features to our software without changing the existing code. Applying this principle means we will get fewer errors and a stronger core codebase as we won’t be forced to change code that is already working.
A Charmander class
A draw class that displays each pokemon on the screen
However, with close observation, we can see that each time we add a new pokemon we have to update our Draw class. This is going against the open-close principle because we have to modify existing code to add a new feature.
A better approach could be to create an interface with the draw function for all pokemon
Our Draw class can now look like
This enables us to not have to touch the draw pokemon function and easily add or remove Pokemon as requirements change.
Liskov Substitution Principle
Subclasses should be substitutable for their base classes.
The Liskov Substitution Principle says that objects must be replaceable by instances of their subtypes without altering the correctness of our system. Our code should expect the same result regardless of the instance type, given that instance/object shares an is-a relationship. This allows code to be more reusable and resilient to change.
Interface Segregation Principle
Many client specific interfaces are better than one general purpose interface
This principle forces us to not have to implement unwanted functions we do not need because the contract from a parent class or interface forces us to implement them. Implementing a specific interface is better than implementing a general one. Which goes without saying we should create an interface that is specific to one thing. This makes code easier to modify.
Looking at the code above we can see the Animal interface has two functions, run() and fly(). However, while the Owl class is perfectly ok as Owls are birds that can run and fly. The Dog class, on the other hand, cannot fly and now has a fly method that it cannot use.
A better approach could be to move the run() and fly() functions outside the Animal interface and create separate interfaces to perform these actions.
This way, the Dog class can implement only the action that it needs and nothing more.
Depend upon Abstractions. Do not depend upon concretions.
Dependency Injection principle forces us to depend upon interfaces or parent classes, rather than using concrete instances. Applying this principle will reduce dependency on concrete implementations, thus making our code loosely coupled and less dependent on a specific time of object.
The code above gives an idea of what it means to not depend on the concrete versions of the class by looking at the Handler class // line 21. We pass an Animal type instead of Owl or Dog, that way we don’t have to depend on what the animal is.
In this article, we have covered the 5 principles of SOLID and how they can be applied to our projects to make them more maintainable and robust. However, these principles are just that, principles, not rules. You should apply your own judgment where necessary about when to apply these principles and to what degree. SOLID is not something to be achieved, use SOLID to try to create maintainable code.