It has been said that anyone who can come up with a good acronym can write a best selling book on programming. Whether that is a commentary on the usefulness of acronyms or the fickleness of tech-pedagogy is a matter of interpretation. Whatever your take, S.O.L.I.D. is one of the most well known acronyms in the programming world and encompasses the most important principles of object oriented programming.
In object oriented programming, you find solutions to problems by making abstract models of objects that interact with each other. Sometimes these ‘abstract’ models are obvious and not so abstract; for instance, if you were designing a program that stored information about a list of cars, you would probably use a “Car” class to store information about each individual vehicle. Other times, object abstractions can be creatively conceptual models that exist only in lines of code. Perhaps you might have a CarDescriptionFormatter object that accepts a Car object and displays a formatted description of the vehicle.
It is because of the latter case that we need to have a set of guiding principles for creating object abstractions. In the obvious cases, we intuitively know that we should be able to run a function ‘pet’ on both a Cat object and the Kitten object that inherits from it. However, when objects become more abstract, this intuition might not be as keen.
S.O.L.I.D. stands for:
S — Single Responsibility Principle
O — Open/Closed Principle
L — Liskov Substitution Principle
I — Interface Segregation Principle
D — Dependency Inversion Principle
Each one of these principles can be (and has been) expounded on in great detail, but I’d like to give some very simple and short explanations of each.
Single Responsibility Principle
The single responsibility principle states that objects should have one job. This is a bit contrary to objects in real life, like duck-tape, that seem like they can do just about anything. However, the single responsibility principle keeps code manageable and maintainable. Not if, but when your code breaks you will need to track down what part is breaking. If every object has only one responsibility, then isolating the problem will be easier by an order of magnitude.
The open closed principle is also designed to make code more manageable, but is a bit more relatable to real world objects. I have a pair of headphones which has cleverly concealed microphone that slides out when you need a gaming headset. It’s perfect for me, but probably unnecessary for most people. If someone were to suggest that we change the blueprint of all headphones everywhere to have slide out microphones on them, I think you would agree this would generally be a bad idea. My headset is a specialized version of the ‘headphones’ object and should have its own separate but inherited blueprint. Likewise, the Open/Closed principle states that object should be open for extension but closed for modification. If we want a pair of headphones with extra features, we shouldn’t modify the base class, but rather extend the class with specialized features.
Liskov Substitution Principle
This is my favorite one because I get to use cats as an example. Let’s say you have a Cat object. For illustration:
You can pet this cat using a ‘pet’ function. Diagram B:
Still with me? Now, let’s make a Kitten class:
Shouldn’t you be able to pet this kitten with the same function? Of course! It would be organizationally and conceptually awkward to have a pet function and a separate pet_kitten function.
As I mentioned above, this might not seem as obvious with more abstract examples, so it’s important to apply this principle consistently. The Liskov substitution principle states that if an object is inherited from, the child class should be able to be substituted for the parent class without breaking the code.
Interface Segregation Principle
The Interface Segregation Principle states that objects should not be forced to implement interfaces they don’t use.
If I have a Cat class and a Robocat class, they will share many common features. You could pet them, play string with them, or take them for a walk. However, the Robocat is a robot and doesn’t need to eat or drink. If the Cat class and Robocat class are using an interface of methods, the interfaces should be separated in such a way that the Robocat class doesn’t have to have a method for eating food that it won’t use.
Dependency Inversion Principle
The Dependency Inversion Principle is somewhat of a reinforcement of other principles in SOLID. If you adhere to Liskov substitution and the Open/Closed Principle, you probably have already achieved the goals of the Dependency Inversion Principle.
Dependency inversion basically calls for a separation of high level logic and low level logic. If I have a CatHearder object, I don’t want to write the class to specifically depend on my Cat class. What if tomorrow cats become obsolete and everyone has robocats? In that case, I want to be able to just make a Robocat class and have it interface with my CatHearder class in the same way. Dynamic/Duck typing in a language like Ruby almost makes this principle difficult to violate. Because variables can hold ANY type and easily change types, all I would have to do is make my Robocat class be able to act like a Cat object and make sure I’m not explicitly creating or invoking Cat objects anywhere in CatHearder.