Home SOLID 101 - Part 1
Post
Cancel

SOLID 101 - Part 1


Here we go again to talk about this widely repeated subject. Why are so many internet articles bringing the same explanation about these five famous principles? I consider it necessary enough to be repeated once again, and especially after several years as a developer, I can safely affirm that such a fundamental and straight-forward guideline is neglected almost everywhere, even by developers with years in the field, so I decided to give my humble take on this. To make the reading more pleasant, I will split it into two articles.


A SOLID base


As developers, we aim (or should) to write good software that can survive changes in business and technology. If companies knew the importance of this, they could save a lot of money instead of repeating the never-ending cycle that forces them to remake entire projects when it becomes impossible to maintain and support new features.

Why have I used the Egyptian Pyramids for the cover of the article? Well, pyramids have always fascinated me. They’re among the most beautiful examples of engineering, precision, and resilience humankind has ever built. Besides all the controversial efforts put into making such a project become a reality, no one can deny they only survived through wars and erosion because of their solid foundation.

Also, pyramids are a perfect fit to illustrate SOLID principles; a typical practical implementation used everywhere is demonstrating principles through interchanging objects representing geometric shapes. I applied all 5 SOLID principles for crafting this 101, and I’ll cover them individually in detail.


What does SOLID mean?


Robert C. Martin (aka Uncle Bob) coined this acronym at the beginning of the century and published it with an article exactly in the year 2000. They are nothing more than 5 principles of object-oriented design (OOD), which are techniques for building a software solution based on object concepts, that can be implemented using object-oriented programming (OOP) language, such as C#.

The 5 SOLID principles are:

S - Single-responsibility principle

A class should have a single responsibility.

O - Open-closed principle

Classes should be open for extension, but closed for modification.

L - Liskov Substitution principle

If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.

I - Interface segregation principle

Clients should not be forced to depend on methods that they do not use.

D - Dependency Inversion principle

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


SRP - Single-responsibility principle


Don’t build Swiss army knives!

Following the order of the acronym, let’s start with the (SRP) Single-responsibility principle, but notice how one principle naturally leads to the next.

The main goal of this program is to render different geometric shapes and display their calculated area. It’s as simple as that. So, one can think of writing some ShapeRenderer class containing multiple methods for rendering different shapes: one for a Square, another for a Triangle, another for a Circle, and so on. Also, these methods can calculate the shape area and display it all. Does it look straightforward? Building this Swiss army knife can be tempting but will only be scalable in the short run. Know that many corporate systems are being written like this as you read this article. Now, think about how this could evolve or change to adhere to new requirements. It can become a massive ball of mud very quickly.

SRP is objective and clear: A class should have a single responsibility. The best way to adhere to this principle is to assign duties to specialized shapes and renderer classes. With that mindset, I put the knowledge for calculating areas into the shapes CalculateArea() method and Render() into the renderers.

Notice that a rectangle has attributes like Height and Width, while a circle has Radius. Hence, their area is calculated differently, as is how they’re rendered.

From a mathematical perspective, a square is a rectangle. Making it inherit from a rectangle looks like an obvious approach, but in OO design, this can be problematic and violates LSP. We’ll see further; I’ll explain why. For now, knowing that the square and rectangle have different fields and need different renderers, I kept them separated with single responsibilities.

All other classes and services in the project have a single and well defined responsibility.


OCP - Open-closed principle


Closing a door, opening a window

We saw that although having multiple specialized shapes, they all inherit from the Shape base class (any given shape is one Shape) and share a common but polymorphic method CalculateArea().

The OCP principle states that Classes should be open for extension, but closed for modification. We achieve that by extension, and that’s why Shape is an abstract class, however, CalculateArea() also must be abstract to define that doesn’t have a standard implementation but instead a common specification. Such implementation will be overridden by each one of the shapes. The Shape class is closed for modification but open to extension. We can inherit any new shape from the base class without modifying the base if we need to support a new calculation logic.

An alternative to inheritance would be only implementing the IShape interface for all shapes with no need for the Shape base class, like I did for all shape renderers, once inheritance for this use-case doesn’t bring any shared implementation but only establish a hierarchical structure. However, I wanted to exemplify you can still achieve OCP with a coupled structure like that. This is important because, unfortunately, it’s widespread to deal with highly coupled codebases due to abused inheritance, making the maintenance hard and adding regression throughout the whole application when changing some core class nobody wants to touch. Either way, having the interface to specify the contract for the base class is also a good practice for loose coupling and allows more flexibility for other aspects of the project.

We will see more about subtypes and interfaces when covering the Liskov Substitution, Interface segregation, and Dependency Inversion principles in the next chapter. Applying these two principles will start making a huge difference in your design, and as I mentioned earlier, one principle leads to another.


Check the project on GitHub



This post is licensed under CC BY 4.0 by the author.