The SOLID design principles are meant to be a guide for designing software that’s easy to maintain, extend, and understand. As a software engineer, these 5 principles are essential to know!
In this article, we will be covering these principles, giving examples of how they are violated, and how to correct them so they are compliant with SOLID. Examples will be given in C#, but applies to any OOP language.
What are SOLID Design Principles?
SOLID is an acronym formed by the names of 5 design principles centered around better code design, maintainability, and extendability. The principles were first introduced by Robert C. Martin (more familiar in the developer circles as Uncle Bob) in his 2000 paper Design Principles and Design Patterns. The SOLID design principles will guide you to:
- write code that’s easy to maintain;
- make it easier to extend the system with new functionality without breaking the existing ones;
- write code that’s easy to read and understand.
Great developer experience means you can easily navigate through the code and understand what it does. Ultimately, it results in spending less time figuring out what the code does and more time actually developing the solution.
So, let’s dig into the 5 SOLID design principles for an object-oriented design that can help us become better developers.
S — Single responsibility principle
In programming, the Single Responsibility Principle states that every module or class should have responsibility for a single part of the functionality provided by the software. You may have heard the quote: “ Do one thing and do it well”. This refers to the single responsibility principle.
In the article Principles of Object Oriented Design, Robert C. Martin defines a responsibility as a ‘reason to change’, and concludes that a class or module should have one, and only one, reason to be changed. Let’s do an example of how to write a piece of code that violates this principle.
We notice how the CreatePost() method has too much responsibility, given that it can both create a new post, log an error in the database, and log an error in a local file. This violates the single responsibility principle. Let’s try to correct it.
By abstracting the function that handles the error logging, we no longer violate the single responsibility principle. Now we have two classes that each has one responsibility; to create a post and to log an error, respectively.
How to make sure your code follows the Single Responsibility Principle?
Write small classes with very specific names as opposed to large classes with generic names. For example, instead of throwing everything inside an Employee class, separate the responsibilities into EmployeeTimeLog, EmployeeTimeOff, EmployeeSalary, EmployeeDetails, etc. Now you have a designated place for everything and you know exactly where to look when you get back to your code a year later.
O — Open/closed principle
In programming, the open/closed principle states that software entities (classes, modules, functions, etc.) should be open for extensions, but closed for modification.
If you have a general understanding of OOP, you probably already know about polymorphism. We can make sure that our code is compliant with the open/closed principle by utilizing inheritance and/or implementing interfaces that enable classes to polymorphically substitute for each other. This may sound confusing, so let’s do an example that will make it very clear what I mean.
In this code snippet, we need to do something specific whenever a post starts with the character ‘#’. However, the above implementation violates the open/closed principle in the way this code differs from the behavior on the starting letter. If we later wanted to also include mentions starting with ‘@’, we’d have to modify the class with an extra ‘else if’ in the CreatePost() method. Let’s try to make this code compliant with the open/closed principle by simply using inheritance.
By using inheritance, it is now much easier to create extended behavior to the Post object by overriding the CreatePost() method. The evaluation of the first character ‘#’ will now be handled elsewhere (probably at a higher level) of our software, and the cool thing is, that if we want to change the way a postMessage is evaluated, we can change the code there, without affecting any of these underlying pieces of behavior.
How to make sure your code follows the Open/Closed SOLID Design Principle?
Let’s say you need to implement an e-commerce module with multiple payment options. You can create a class Pay and add different payment options as separate methods. This would work, but it would result in changing the class whenever we want to add or change a payment option.
Instead, try creating a PayableInterface that will guide the implementation of each payment option. Then, for every payment option, you will have a separate class that implements that interface. Now the core of your system is closed for modification (you don’t need to change it every time you add a new payment option), but open for extension (you can add new payment options by adding new classes).
L — Liskov substitution principle
This one is probably the hardest one to wrap your head around when being introduced for the first time. In programming, the Liskov substitution principle states that if S is a subtype of T, then objects of type T may be replaced (or substituted) with objects of type S. This can be formulated mathematically as
Let<ϕ(x) be a property provable about objects x of type T.
Then ϕ(y) should be true for objects y of type S, where S is a subtype of T.
More generally it states that objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program. Let’s take a look at an example of how to violate this principle.
Observe how the call of CreatePost() in the case of a subtype MentionPost won’t do what it is supposed to do; notify the user and override existing mention. Since the CreatePost() method is not overridden in MentionPost the CreatePost() call will simply be delegated upwards in the class hierarchy and call CreatePost() from its parent class. Let’s correct this
By refactoring the MentionPost class such that we override the CreatePost() method rather than calling it on its base class, we no longer violate the Liskov substitution principle. This is but one simple example of how to correct a violation of this principle, however, this situation can manifest in a broad variety of ways, and is not always easy to identify.
How to make sure your code follows the Liskov Substitution Principle?
If your programming language supports type-hinting for return types in interface methods, you can use that to avoid the issue of having a different return type in different implementations. Still, it doesn’t solve everything. Throwing an exception in the middle of a method implementation where it’s not expected is also a violation of the Liskov Substitution Principle PHP. So, the best way to make sure you follow this principle is mindful programming. Always keep in mind what the system expects when you’re implementing its functionality.
I — Interface segregation principle
This principle is fairly easy to comprehend. In fact, if you’re used to user interfaces, chances are that you’re already applying this principle. If not, it’s time to start doing it!
In programming, the interface segregation principle states that no client should be forced to depend on methods it does not use. Put more simply: Do not add additional functionality to an existing interface by adding new methods. Instead, create a new interface and let your class implement multiple interfaces if needed. Let’s look at an example of how to violate the interface segregation principle.
In this example, let’s pretend that I first have an IPost interface with the signature of a CreatePost() method. Later on, I modify this interface by adding a new method ReadPost(), so it becomes like the IPostNew interface. This is where we violate the interface segregation principle. Instead, simply create a new interface. If any class might need both the CreatePost() method and the ReadPost() method, it will implement both interfaces.
How to make sure your code follows the Interface Segregation Principle?
Split the Employee class into smaller classes with specific interfaces. Once you do that, adjust the controllers so they all depend only on the interfaces they need. This way, you’ll have a clean structure where the client depends only on the methods it uses. Changing something in a certain class will only affect the parts of the system that actually depend on it.
D – Dependency inversion principle
Finally, we got to D, the last of the 5 SOLID design principles. In programming, the dependency inversion principle is a way to decouple software modules. This principle states that
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
To comply with this principle, we need to use a design pattern known as a dependency inversion pattern, most often solved by using dependency injection. Dependency injection is a huge topic and can be as complicated or simple as one might see the need for. Typically, dependency injection is used simply by ‘injecting’ any dependencies of a class through the class’ constructor as an input parameter. Let’s look at an example.
Observe how we create the ErrorLogger instance from within the Post class. This is a violation of the dependency inversion principle. If we wanted to use a different kind of logger, we would have to modify the Post class. Let’s fix this by using dependency injection. By using dependency injection we no longer rely on the Post class to define the specific type of logger.
How to make sure your code follows the Dependency Inversion Principle?
By combining the dependency injection technique with the concept of binding an interface to a concrete implementation, you can make sure you never depend on concrete classes. This will allow you to easily change the implementation of specific parts of the system without breaking it. A good example of this is switching your database driver from SQL to NoSQL. If you depend on the abstract interfaces for accessing the database, you’d be able to easily change the specific implementations and make sure the system works properly.
Video Tutorial For SOLID Design Principles
The SOLID design principles are meant to be a guide for designing SOLID software that’s easy to maintain, extend, and understand. If used properly, they will help your team spend less time understanding what the code does and more time building cool features. Which is all we dream of, right?