Object Oriented programming enables developers combine data, with the same purpose or functionality in one class to serve a single purpose. This is done regardless of the entire application. It allows developers to model their programmes after real world objects. Improperly designed programs that use object oriented programming however tend to be confusing or unmaintainable. These five (5) principles of Object Oriented Design, developed by Robert C. Martin, makes it easy for developers to write clean, maintainable and readable programs.
These principles called the S.O.L.I.D principles is an acronym derived by Michael Feathers. S: Single Responsibility Principle O: Open-Closed Principle L: Liskov Substitution Principle I: Interface Segregation Principle D: Dependency Inversion Principle
Applying the SOLID principles depend on individual use cases. The most important thing is to understand and know how to apply and follow the principles. In order to paint a descriptive picture of how to follow these principles, we will discuss the story of Bolu.
Bolu is a freelance android developer from Lagos, Nigeria. Bolu loves football and can’t resist the smell of Amala and Abula. Recently, Bolu got a contract to maintain an android app that lists all the cool amala joints in the users’ immediate area. He is enthused about working on this app because the pay is great and the idea is awesome. It will also be a great addition to his portfolio because the app has a solid user base.
On importing the source code to android studio, Bolu discovered the following about the app:
- Fragility: This means that a change in one part of the app, had unexpected effects and broke features in other parts of the app.
- Immobility: Components which were hard to reuse in other parts of the app or in other projects caused a lot of code duplication.
- Rigidity: It was really hard to make changes to the code base.
If you’re reading this article, there’s a high chance you have been a Bolu at some point in your career and you’re probably wondering how can you avoid putting other people (and yourself) in Bolu’s situation? Using examples from Bolu’s work with the amala app, we will now explain each principle in details.
S: Single Responsibility Principle
Every class should have just one responsibility. If a class has more than one responsibility, it becomes coupled and a change in one responsibility means there’ll be a change in its other responsibility. In Bolu’s case, his client discovered that the app doesn’t show the correct error message when there’s no internet or the backend is down. Bolu has to do some data validation to know if the data fetched from the backend is correct. The existing “FoodRepository” class had a number of problems; it was not testable, and did a lot of work that was not necessarily its concern.
Bolu attempted to work with it at first, then the client started asking for more validation steps:
- To check if the data from the backend was empty.
- To check if the data contained key “food”
The list of checks seemed like it will keep growing with Bolu having to change the logic of the “parse()” method each time. The worst part was that he couldn’t reuse all the code he was writing in the “parse()” method without some “copy and paste”.
To combat this, Bolu refactored the FoodRepository class to ensure each step was ‘contracted’ to another class. So, implementing the S principle helped Bolu to facilitate reuse and remove some of the problems he had around Immobility.
O: Open-Closed Principle
Software entities (Classes, modules, functions) should be open for extension, not modification. If you want to create a class easy to maintain, it must have two important characteristics:
- Open for extension: You should be able to extend or change the behaviours of a class without too much effort.
- Closed for modification: You must extend a class without changing the implementation.
You can achieve these characteristics thanks to the abstraction.
Bolu was beginning to have fun with the project, SERIOUS fun. Well, until the client decided on categorizing the type of amala joints listed in the app, the client decided on two categories:
- Buka: Each Buka should show up in the app with its name and the color in which it’s shop is painted.
- Restaurant branches: Restaurant branches are usually painted the same color, so they should show with name and address of the branch.
The first idea Bolu had was to display a list for each category, this worked but the client didn’t like it because it did not look good on the app. After some thought, Bolu also concluded it was inefficient because he’d need to add more lists if the client ever wanted to add more categories.
However, after reading about the Open-Closed principle, Bolu knew what to do. He created an interface called Printable so he could take advantage of abstraction in order to enable the app display any category in the same list. He then made both categories currently existing to implement the interface, that way each one could display data however way it wants and he didn’t have to do too much work if other categories were ever added.
Bolu could now show all categories in one list and additions of new categories didn’t mean a lot of extra work on the UI of the app. He was slowly getting rid of the rigidity in the codebase.
L: Liskov Substitution Principle
A subclass must be substitutable for its superclass. Basically this means that you shouldn’t make subclasses which don’t have the same functionality of the superclass if you need to use them interchangeably. Inheritance may be dangerous and you should use composition over inheritance to avoid a messy codebase. This principle can help you to use inheritance without messing things up.
One day, Bolu’s client called and told Bolu of plans to quickly organize a promotion on Valentine’s day. Bolu needed to categorize different types of amala by their colours and he needed to do it fast. He created 3 classes to model the types of amala and implemented methods that printed the different details of each type. But, it quickly became clear that the program was not semantically correct because all the types of amala had provisions for food colouring and this wasn’t right. It didn’t help the readability of the code and added more problems to the already fragile code base. This time, Bolu called his mentor, Ibe to tell him of the problem he was currently facing. Ibe advised him to refactor into a base class containing all the shared features of the amala types and then create another base class for amala that contains food coloring. That way each type has only the information it needs and the codebase is not as fragile. This basically is the Liskov Substitution Principle since all the types of amala can now be used interchangeably with the base supertype without breaking any code. And the program remains semantically correct.
I : Interface Segregation Principle
Make fine grained interfaces that are client specific. Clients should not be forced to depend upon interfaces that they do not use. This principle deals with the disadvantages of implementing fat interfaces. An interface is called “fat” when it has too many members or methods which are not cohesive and contains more information than we really want. This problem can affect both classes and protocols.
The UI Designs for the amala App had buttons in all shapes, sizes and functionality types and the client was very serious about making things look and work exactly how they were specified in the designs. At first, Bolu was working with buttons that shared just one functionality —single clicks; then things got complicated. He had to handle double clicks, long presses, swipes etc. with buttons that sometimes share just one or two of all the functions.
This was when he started to really notice the fragility in the code. Because, all the buttons shared the same interface each time a new feature type was added to one button. All the other buttons broke. At this point, Bolu didn’t really know he was breaking any rules, but he knew adding new functionality to the buttons was slowing him down. So he hit the books ʘ‿ʘ. After doing some reading (on StackOverflow), he discovered the Interface Segregation Principle. To implement it, he broke down the large interface that all buttons implemented and only allowed buttons to implement an interface if they absolutely needed the functionality it describes.
By doing so, extending or changing functionality became a whole lot easier and he didn’t have to spend a lot of time fixing code each time he changed something about a button. He was finally making some good progress removing rigidity and fragility in the codebase.
D: Dependency Inversion Principle (DIP)
Dependency should be on abstractions not concretions.
- High-level modules should not depend upon low-level modules. Both should depend upon abstractions.
- Abstractions should not depend on details. Details should depend upon abstractions.
After a lot of hard work, Bolu was almost done with the app, all he needed to do was to improve on the cart. The requirements were as follows:
- The user should be able to save items to the cart (already implemented).
- After all items have been saved, the cart should send data to the backend if the user checks out and pays.
- The data should persist in the file system or database if the user doesn’t checkout.
- Bolu took one look at the current implementation of the cart and knew that is was terribly wrong. The current implementation was tightly coupled to the CloudStorage and hence was not in anyway reusable, changing it in its current format also meant it would be hard to test and Bolu prefers testing his code because he doesn’t have to type too much documentation when he writes tests.
At this point Bolu had done quite a lot of reading on proper architecture and he was sure he needed to apply the Dependency Inversion Principle so that his class was easier to test and reuse. He created an interface called “Storage” so that all types of storage mediums —backend, filesystem & cache— implemented it and the cart didn’t need to know anymore about the storage mediums than necessary. He then made sure the dependencies of the cart were injected via the constructor so that it wasn’t tightly coupled anymore and could be easily reused with any kind of storage. At the end, Bolu ended up with a Cart implementation that was not only easy to test but also very reusable.
- Bolu ran tests that passed.
- He built the app and shipped it for a very very happy client.
- Bolu got paid and went to eat amala at his favorite joint.
- He was also able to continuously update the app with ease.
- The speed and quality of his work increased by 2x because he was able to maximise code reuse, changes were easy to make to the app and he could quickly isolate and fix problems within his app.
We should always keep in mind that applying the SOLID principles requires good judgement of when and how to apply.
“Principles will not turn a bad programmer into a good programmer. Principles have to be applied with judgement. If they are applied by rote, it is just as bad as if they aren’t applied at all” —Robert C. Martin