SOLID: Five Fundamental Principles in OOP

Fadhriga Bestari
The Startup
Published in
7 min readJun 26, 2019

SOLID, for those of you that are uninitiated in the world of Object Oriented Programming, is an acronym for five fundamental principles that needs to be abided by programmers to create a more understandable, maintainable, extendible, and ultimately better software.

This past week, I’d been tasked by my mentor to create a mini-game that can be played in macOS’ command line. The premise of the mini-game is to hone my SOLID fundamentals in OOP and also to hone my ability in adopting design patterns in my software.

Along the way, I created some mistakes in regards to the SOLID principles. Earlier in the project, I also use the wrong design pattern in my project. In this article, I’m going to show you these mistakes and design flaws that I made along the way, and how I managed to correct it in the end.

If you are interested in my project as a whole, you can access it here.

S — Singe Responsibility Principle (SRP)

Single Responsibility means that every class should only be responsible for a single functionality.

That explanation doesn’t really explain what a Single Responsibility Principle means, you say? I’m sorry, but SRP really, in essence, is self-explanatory. A single class should not do more that it should. Although it is a fairly simple and easy to understand principle, I find that this principle is the easiest to get overlooked. This principle is violated mostly because of one’s laziness. Instead of creating additional classes and separating responsibilities, lazy programmers just cram all functionality in one god class.

There is another reason for someone to break this principle, is that to mistakenly think that a function is this class’ job to do, but in truth, it’s that class’ job to do it instead. This is a fairly tricky problem to detect, but there is a simple solution to this problem. When you’re determining a function inside a class, ask yourself these questions.

In the future, will other programmers and I know that this functionality resides in this class?

If the specifications change later, will it be clear for other programmers and I to change this functionality here and only here?

To give you more understanding, I’m going to give you this example from my mini-game that I mentioned earlier. To give you some context I made a command line application that asks users their input to create their personal Marvel character. I designed the game so that each character can print their current health, damage, name, and abilities using a status function.

It might be reasonable for us to put the print status function in each character, because after all, each character can print their respected status. What you need to keep in mind that just because a class can do a particular functionality, doesn’t mean that the class needs to be the one that implements said functionality. Character has stats, but it doesn’t mean that it’s Character’s responsibility to print it. After this realization, I decided to separate printing function from Character and put it in a Printer class instead.

Here, it’s clear that the responsibility of a Printer is to print. If there as a change in how we should print each character’s status, we now know that we need to change the implementation inside of the Printer class instead of going through each individual class and change the status function there.

O — Open/Closed Principle

Each class needs to be open for extension, but closed for modification.

Open for extension means that a class needs to be extendible when new specification arises. The easiest way to understand this principle is by using inheritance as an example. Here is an example of my initial Action class that i made in my mini-game

By using if-else statement, I limit my software for extension, as more and more actions will mean more and more if-else statement inside my Action class. To remedy this, I instead make the Action a protocol with execute function that needs to be conformed by all it’s subclasses.

Here, my program is easier to extend (keep in mind that each subclass can be placed in different files and modules). All we need to do is create a new class that conforms to the Action protocol, without changing anything else inside the main logic of the program itself. It is way cleaner to do Action implementation this way, as it is more readable, understandable and easier to extend in the future. Imagine if you have 20 action commands, would you want to have 200 lined file with 20 if-else statement in a single file?

This method of abstraction is also known as a Strategy Pattern, where we group an algorithm and make them interchangeable from one another at runtime. We define the abstraction in a protocol, and hide the implementation details in each subclasses.

L — Liskov Substitution Principle

This principle states that:

Let ϕ(x) be a property provable about objects x of type T.
Then ϕ(y) should be true for objects y of type S, where S is a subtype of T.

— Barbara Liskov and Jeannette Wing

If you’re like me, then you might not really understand what any of those means. How do we even detect when our software violates this principle? This statement is exactly why I find this principle the hardest to understand and actively avoid during my programming process.

There is, however an easier way of understanding this principle. There are key points you should observe while programming your software. These key points are:

  1. A subclass shouldn’t have more preconditions than its parent class, meaning that if a subclass override its parent function, that function should not then return an exception when its original function will not return an exception.
  2. A subclass shouldn’t have fewer postconditions than its parent class, meaning that a subclass should return the same expected outcome as their parent class.
  3. A subclass shouldn’t have an unused function that it inherited from its parent class.
  4. A subclass shouldn’t use different parameters for the same function.

Most importantly, though, each subclass should behave the same way as its parent class, and be interchangeable at runtime. It should not be noticeable for the client to run the same command be it for the parent or the subclass. If it is different, then you might have violated the Liskov Substitution Principle.

See this example of my Action protocol.

Initially, I put two execute function inside the Action protocol. This was due to there being two different kinds of action, the ones that only take one character and the ones that take two. If I implemented this, then all subclasses would have at least one function that’s unused. A StatusAction only takes one parameter, so it won’t override the execute with two parameters. The code will work, but it will violate the Liskov Substitution Principle. We need to separate these functions so that each subclass only need to conform to their respected execution contract.

Here, after abstracting the two available action, we can see that each subclass of SoloAction behaves exactly as they should, only needing one parameter to execute their command. The same applies to DuoAction, where each subclass needs two parameter to execute.

I — Interface Segregation Principle

If you have been reading my examples carefully, you might have seen an entity called a protocol. A protocol is virtually the same as an interface in other languages (with a few minor differences). I personally use protocols specifically to abide to this principle.

This principle states that each entity shouldn’t be forced to depend on a function that it doesn’t need. By using interface, we can abstract each functionality of a class, and then let each class that needs that particular functionality to implement it themselves.

Let’s see this example from my mini-game

Initially, I thought that well, describable would mean that a class would have a name and a description. But what happens when a new class is created, and it only needs a name but without description? We need to separate these two functionality and create two separate interfaces so that each class can decide which interface they want to implement themselves.

This way, if a class needs to implement only a name or only a description, they can do so. If a class needs both, then they can implement both interfaces at the same time.

D — Dependencies Inversion Principle

To abide to this principle, there are two key points you need to pay attention to:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

One simple way to understand this is that a lower-level module should not determine the dependency of itself. Let’s see this example right here:

My GameHandler class has a few attributes. Because I know that it needs those specific attributes, I could just define them inside the GameHandler class itself. This, however, would complicates stuff when new specifications surface itself and it turns out I need to change the inputManager input into SpecialInputManager, or the printer into BeautyPrinter. Higher-level module wouldn’t be able to make changes on the fly, rather, we need to manually change it inside the GameHandler class.

With this change in place, we no longer depend on GameHandler to define its own attributes, rather, the higher-level module can define it themselves.

Conclusion

Abiding to these 5 fundamental principles would mean creating a better software. It is understandable to initially create a software that doesn’t abide the SOLID fundamentals, but it’s our job as a programmer to go back and fix these mistakes so that our software is more maintainable and extendible in the future, not just by us, but by other programmers as well.

--

--

Fadhriga Bestari
The Startup

An up and coming software engineer, learning to love to write.