The SOLID principles are a set of 5 rules to follow for Object-Orientated Programming (OOP). These rules or principles can be used to create software that is easy to extend and maintain while avoiding code smells and allowing simple refactoring. Obviously, these 5 principles are not a silver bullet and they won’t fix any codebase or make your codebase magic. Even while following these rules you can write code that makes maintenance and refactoring difficult. These rules should act as good things to keep in mind while designing and writing software but they can also be used in conjunction with coding guidelines and other concepts, such as design patterns.
I will be covering all of the 5 principles in this post but I will also be linking to other posts where I explain each principle in more depth with code examples and snippets. If you don’t understand OOP or want a refresher I would suggest reading these posts where I explain the basics of OOP before continuing
Single Responsibility Principle
The Single Responsibility Principle (SRP) is probably the easiest of the 5 principles to understand. Every class should have one and only one job. This seems simple and most developers will be able to tell you this even if they struggle to follow it. The difficult thing to understand with this principle is that the single responsibility of a class can be very specific or a bit more abstracted. For example, you could have one class whose purpose is to hold the data for a rectangle, it’s size and position, but then you could have a more general-purpose for a class where it holds all the data for a rectangle but also handles calculating it’s area and perimeter. The second example has multiple jobs it can do, it can calculate it’s area or perimeter but it still has a single responsibility – to handle the rectangle. If this same class also handled user sign it no longer has one responsibility and therefore breaks the SRP.
The important thing to remember about the single responsibility principle is that each class should have a single clearly defined purpose for existing and do only one thing. This one thing can be very specific, such as handling a rectangle, or a bit more generic and have multiple operations, such as handling user authentication. This principle can also be extended to a smaller scope, each class should have a single purpose and each method within that class should have a more specific single purpose.
Read more about the Single Responsibility Principle here.
The Open-Closed principle states that objects should be open for extension but closed for modification. This means that any object should be easy to extend or add functionality too but shouldn’t be modified or changed. This principle is quite difficult to understand to begin with, as it doesn’t make sense outside of an OOP paradigm. Let’s imagine we have a piece of code that handles a particular functionality, we then compile that code so that we cannot change it. We should be able to add functionality to the code, to make it open for extension, without modifying the source code, to make it closed for modification. Obviously outside of OOP, this is impossible as to add functionality we must first change the source code and then recompile the code. However, in OOP you can use the compiled source of a class and inherit from it in a new class – the new class would be a child of the pre-compiled class. Inheriting from a base class allows you to add functionality that the base class doesn’t have but also keeps the base class closed from modification. Simply put, when inheriting from a base class you should not have to change or modify the base class.
Read more about the Open-Closed Principle here.
Liskov Substitution Principle
The Liskov Substitution Principle states that any subclass should be able to be replaced in any place where the base class is used. This means that if there is a place in your code where you are using a base class, or any class that has children, then any of its children should be able to be used without the code-breaking or changing. It shouldn’t make a difference or create an error or bug if you replace a base class with an implementation of any of its children classes. This principle ensures an inheritance tree that actually follows proper inheritance and enforces you to think about your inheritance hierarchy and don’t just extend from a base class to copy functionality.
Read more about the Liskov Substitution Principle here.
Interface Segregation Principle
The Interface Segregation Principle states that a class should not be forced to implement an interface or method that they do not use or that doesn’t make sense in that context. It is common to use polymorphism when coding in an OOP context, so you can use generic code on many different concrete implementations. However, this can lead to making a class implement an interface, to allow polymorphism, that doesn’t actually make sense. For example, a class that deals with contact information shouldn’t implement from a class or interface that deals with a person’s wage or hourly rate. Another reason people break the interface segregation principle is because their interfaces have too many methods in them. Let’s say you have an interface that forces two methods to be implemented “talk” and “fly” which we use on implementations of different types of birds. However, some birds don’t fly, so if you create a class for the Dodo bird you will implement from this interface and then be forced to implement the “fly” method which doesn’t make sense in this context. This doesn’t mean every interface should just have one method. Breaking your interfaces down into sensibly sized components is a difficult thing and will most likely result in refactoring your codebase later. The Entity Component System design pattern is a good example of the interface segregation principle.
Read more about the Interface Segregation Principle here.
Dependency Inversion Principle
The Dependency Inversion Principle states that any entity should depend on abstractions and not concrete implementations. This means that you should use polymorphism to make your classes depend on interfaces or abstract classes instead of the concrete implementations of your dependencies. This allows you to easily swap out the concrete implementations without having to refactor your base class. This is a useful rule to follow but it can get out of control quite quickly, as you could create an interface for every concrete class in your codebase.
Read more about the Dependency Inversion Principle here.
The SOLID principles are great rules to follow and can keep your codebase easy to maintain, extend, and avoid smells. However, taking any of these principles and running with it can actually make your codebase worse. For example, making your code completely closed to modification will prevent any refactoring or feature changes, making every concrete class depend on interfaces that will result in a huge codebase to manage, etc. It is important to review your codebase and if you can’t follow a principle completely that is okay. As with a lot of software-related tips and tricks, treat them as good guidelines not as strict regimes that must be followed completely.