In my last article I posted about “What is APIE?“, if you haven’t read this yet and don’t know what APIE is, I recommend reading that article first.
Just like APIE states the 4 basic concepts of OOP, SOLID defines 5 OOP principles which you should always follow when writing code. The letters stand for Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion. As much as all of these principles help in achieving a maintainable code base, the Open-closed and Interface segregation principles aren’t as useful as the others in my opinion as they’re harder to implement effectively and sometimes don’t give much benefit. Regardless I will explain what you need to know about each of these.
“A class should only have one responsibility”
This principle is without a doubt one of the easiest to breach without even knowing you’re doing it. The idea is that when building your classes, you make sure that each class does only one thing. You might interpret this as a user class has one responsibility and so storage of a users data, modification of the stored data and interactions with the data can all reside within one class. Thinking this would be wrong and to abide my the single responsibility principle, this functionality should be broken down into 3 classes or possibly more. One class for storing and retrieving data, this is frequently called a Data Transfer Object (DTO), another class for persisting data back to a database, these are called data mapper classes and finally a manager class for interacting with the DTO. An example of this could be checking if a user is currently online. The manager class could be split into any number of classes depending on what you are doing with the data.
Some developers will say that this level of single responsibility is not necessary and that saving records should also be permitted by the domain model aka DTO. Doing so would be using the active record pattern. It does have the benefit of making your code slightly easier to digest, but like anything that breaks single responsibility, you pay the price of having less maintainable code.
“Software entities … should be open for extension, but closed for modification”
In my opinion this is the least useful principle of the 5. There are 2 interpretations of this principle although instead of telling you the differences between these (which you really don’t need to know), I’ll give a general overview of the similarities.
The aim of the open-closed principle is to prevent breaking existing code. In theory this shouldn’t really be a problem if you have a decent set of regression tests. Nevertheless open-closed suggests that your entities (in most case these are your classes) should not be modified once they are created and instead if you want to extend the functionality of a class, this should be done by creating a child class.
I personally take this principle on a case by case basis, if I think a new method belongs in an existing class, I would always put it there. The problem with this principle is that systems grow over time and functionality changes. If you were forced to create new classes every time you changed or added functionality, the amount of classes you would have would be ridiculous, not to mention the extra difficulty you would have writing tests and maintaining the code base….. use with caution.
“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program”
Liskov substitution is all about making use of interface and abstract classes and thus making you concrete classes polymorphic. This is commonly known as programming to an interface, not to an implementation. By sticking to this rule, never in your application should one object depend on another object, but rather it should depend on an interface, this way it doesn’t matter how much your application grows, you already have the abstraction in place to cater for change. Imagine you have a Shop class that relies on an Item class, this would not make sense. Later down the line, you may create a new class called DiscountItem, where all items are 50% cheaper. Doing this would mean you would have to refactor your Shop class. However if you made your Shop class depend on an interface of an item, it wouldn’t matter how many different Item related classes you create, because Shop would alway depend on an Item interface.
This is slightly edging onto dependency inversion, but another key advantage to programming to an interface, is the ability to easily unit test your code. Each unit test you write, should test one unit of code. E.g one small method. If that one small method depends on a whole different class instance existing, your test would no longer be a unit test and become more of an integration test. This doesn’t make your tests worthless, in fact you should have unit and integration tests running. However when an integration test fails it will be harder to debug and the underlying problem could be far worse than for a unit test failing. A lot of programmers do not program to an interface. This is why those developers are incapable of writing unit tests and code bases often get filled up with lots of integration tests instead.
“Many client-specific interfaces are better than one general-purpose interface”
Interface Segregation is probably the 2nd least popular principle of the 5. Mainly due to it not adding much benefit and in some cases it may give no benefit and just be a pain in the ass – metaphorically. The aim of the principle is to get developers to create multiple interfaces (interface or abstract classes) for similar but different class blueprints. An example of this would be to create different interfaces for two different factory classes. The reason for this could be because one has a helper method, that maybe another factory will not have and so a class should not be forced to implement a method which it doesn’t need.
There are tonnes of use cases when you will find interface segregation useful, however I’ve never met a developer who is strict on applying it. The main reasoning for this being that to implement interface segregation, you have to sacrifice polymorphism to a degree, by having to cater for multiple interfaces instead of just one.
“Depend upon Abstractions. Do not depend upon concretions. Dependency injection is one method of following this principle.”
Dependency inversion means not creating dependencies. This can be accomplished by injecting dependencies into your classes using the Inversion of Control (IoC) technique or by using a dependency injection container (DIC). Dependency injection and IoC were briefly mentioned in the section on Liskov Substitution, they’re both tightly coupled, if you are doing one of these well, chances are that you’re doing the other well too.
Any application which you work on is going to need to store data about something, somewhere… usually in a database. It would be quite easy to make a call in each of your classes to go and get a database connection, but this would be wrong. Doing so would mean your class has to use that specific connection in every instance. Instead if you were to create a database object outside of the class and pass it into the class through the construct or even better, a mutator method, if you ever needed to use a different connection you could do. If you ever plan on writing unit tests for your application, this is something you’re definitely going to want to do.
So that is the basics of the SOLID principles covered. There are tonnes of other great resources online for you to read. If you have any questions, I’ll be happy to answer them.4 Love This