In a previous article, we talked about objects and classes, and how they can help you better organize your code. The SOLID principles of Object Oriented Design are a set of principles you can follow to make better use of objects and classes. These principles are not rules. They are more guidelines to try to stick to while developing your applications.
As you become more familiar with these principles, you will find that there are times when it makes sense to follow these principles. And there will be times when you are better off not following the principles as they for one reason or another do not make sense in your application. Or in that specific use-case in your application.
What are the S.O.L.I.D. principles?
As was said earlier, the SOLID principles are general design principles designed to help you make better decisions about how to design your application. While SOLID sounds interesting enough all on its own. SOLID is an acronym for the principles you should follow. SOLID stands for:
- S – Single-responsibility principle (Only does one thing)
- O – Open-closed principle (Open to extension, closed to changes)
- L – Liskov substitution principle (LSP)
- I – Interface segregation principle (Don’t implement things that are not used by the class)
- D – Dependency Inversion Principle (One class should not depend on another class)
In the following sections, we will go into more detail on what each of these principles is and give some examples.
Single Responsibility Principle
The idea of the Single Responsibility Principle is you want each class to only do one thing, and do that one thing really well. For example, you might have a class that is for telling you what time it is. You want it to tell you what time it is, maybe return the time in a couple of different formats. But in the end, you only want it to tell you the time and very closely related to other things. You don’t want to re-use that class to then run network diagnostics. Doing so would violate the single responsibility principle.
Something you can do that would not violate this principle is to expand it from time and also include the date. This is a popular way to do things. In most languages, you actually use the date class to tell the time. These are two closely related topics as the date is another way of showing the passage of time.
Open Closed Principle
The Open Closed Principle states that you want your classes to be open for extension, but closed for modification. The idea is you are going to be writing other code that will depend on this class. As you are writing code that uses this class, you don’t want to break your other code because you had to make an update to your class.
A good example of this is you might create a class for calculating the area of a rectangle. This is a very simple scenario, you just create a class with an internal function that multiplies length and width. You then ship your area calculating class.
Next week you get a call that we now need to calculate the area of a triangle. The calculation for a triangle is, of course, different from the calculations for a rectangle. You now need to change your code to support calculating the area of a triangle. If you have written any other code since shipping your original class, you might need to regression test that code after updating your calculate area class.
One possible solution to this is to create your area calculating class. Then create a collection of subclasses for each shape you may want to calculate. Following this model means that after you have created your rectangle subclass and it is working, it is closed for modification. However, you can easily extend your calculate area class by adding additional subclasses for each additional shape you can dream up.
Liskov Substitution Principle (LSP)
The Liskov substitution principle is not long the hardest one to say, but it is also the hardest one to explain. The Liskov substitution principle states that Functions that use pointers or references to base class must be able to use objects or derived classes without knowing it. What does that mean?
If we think back to our calculate area class in the single responsibility principle section, we start off with a calculate area class that is meant for calculating the area of a rectangle. That class will take two inputs, height, and width.
Later on, we move the job of calculating the area of a rectangle into a subclass which inherits all of the properties of the original class. Those properties being height and width. So far it is not a problem, because all rectangles need to have the height and width specified to accurately calculate the area.
Next, we add a new subclass for calculating the area of a square. You can specify the height and width of a square separately. And as long as you specify the same height and width, everything works fine. But what if you specify a different height and width? everything will fall apart with your class. This is the essence of the LSP. You don’t want to have your subclasses inheriting stuff that they can’t or shouldn’t be using. In our case we just want the height or width to calculate the area of a square. To correct this problem, we would probably move the property gathering from the parent class down into the subclass in order to solve the problem.
I found this meme on the internet a while ago, but I can’t remember where. I wish I could give attribution to it. But I think it helps describe the Liskov substitution Principle very well:
In this meme, we might have a duck class. But only one of them takes batteries. If the real duck is a subclass of the rubber duck, then we have a problem because the regular duck does not use batteries.
Interface Segregation Principle
The interface segregation Principle tells us not to implement things that we are not going to use within the class. This rule is really meant to keep your code cleaner. If you create your calculate area class, but you add in a bunch of extra functions for checking the weather. It not only violates the Single Operation Principle. But it also violates the interface segregation principle because at no time do we need to check the weather while we are calculating the area of a rectangle.
A more realistic example would be to add a volume function within your class. If you are only ever calculating two-dimensional shapes, then you are never going to calculate the volume. If you ever get to the point where you want to calculate the volume of a 3D shape, you can add that function at that time. Or you can create a new class specific to 3d shapes.
Dependency Inversion principle
The dependency inversion principle tells us that we should not have one class depend on another class. Instead, classes should depend on abstractions. This means that we can still use other classes. But we don’t want to depend on the specifics of those classes. Instead of we want to depend on the non-specifics of that class.
For example, let’s assume that we have created a new class that does some data manipulation and saves the data to a MySQL database. You might have a function that calls:
The problem with this code is if you ever change the database engine from MySQL to MS SQL, it will require a code change in your data manipulation class. This is calls a concretion. Instead, you want to create an abstraction in your datamanipulation class that references a generic term instead of a specific term….more abstract. You could change your code to:
In the case of the code above you have now made the DB connect function very generic. If you want to change database vendors you simply specify a different connection string in a configuration file instead of having to make a code change.
Today we have discussed what the SOLID principles of object-oriented design are. These are merely guiding principles and not hard and fast rules. There are times when you need to break these rules for one reason or another. However, it is important to always be aware of what these principles are and take them into consideration any time you are designing your classes. That way you are aware that you are not following the guidelines and can weigh the pros and cons of that decision.
One approach to this would be to use emergent design when building your classes. Emergent design is where you focus on the technical requirements vs the end state. After you have fulfilled the technical requirements, you then do more iterations on your design to improve it. These iterations may be immediate. Or they could end up being the first time you need to make a code change to your project. It is up to you on what approach you want to take.