SOLID Principles to write a better code

A quick guide to writing better code with help of the SOLID Design Principles, illustrated with Python examples.

Mohammed Vaghjipurwala
Geek Culture
8 min readApr 5, 2023

--

Any fool can write code that a computer can understand. Good programmers write code that humans can understand — Martin Fowler

Photo by James Harrison on Unsplash

MY CODE WORKS — WHY DOES IT NEED TO BE BETTER?

  • It’s easy to hack together solutions and move on to the next problem — and in a perfect world, where we could just write code and forget about it, perhaps that would’ve been acceptable. However, code tends to evolve over time; we come back to it to modify it, tweak it, and fix bugs — and well-designed code is much easier to come back to. Bad code becomes harder and harder to maintain, slowing down development efforts. Good code comes with future benefits.
  • Also, bad code can easily have side effects; and side effects mean that you can make a change in one place, only to cause a hard-to-find bug somewhere else, resulting in a painful debugging exercise. It’s much harder for bugs to hide in well-designed, easy-to-read code.

So, What is SOLID?

The principle of SOLID is an acronym originated by Robert C. Martin. SOLID refers to a set of principles of Object Oriented Programming and Design. These principles are intended to help developers to write maintainable, extensible code. The principles are as follows

  1. Single Responsibility Principle
  2. Open/Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

Let‘s explore each principle in detail

1. Single Responsibility Principle

A class should do one thing and therefore it should have only a single reason to change

In other words, every component of your code (in general a class, but also a function) should have one and only one responsibility and it should perform the task efficiently. As a consequence of that, there should be only a reason to change it.

Following the Single Responsibility Principle (SRP) is important. First of all, because many different teams can work on the same project and edit the same class for different reasons, this could lead to incompatible modules.

Second, it makes version control easier. For example, say we have a persistence class that handles database operations, and we see a change in that file in the GitHub commits. By following the SRP, we will know that it is related to storage or database-related stuff.

Merge conflicts are another example. They appear when different teams change the same file. But if the SRP is followed, fewer conflicts will appear — files will have a single reason to change, and conflicts that do exist will be easier to resolve.

In the above example, We have a class Album. This stores the album name, artist, and track list, and can manipulate the contents of the album, such as adding songs or deleting them. Now, if I add a function to search albums from the same artist, It breaks the Single Responsibility Principle. Album class would have to change if we decide to store albums in a different way (for example by adding the record label or storing the track list as a dictionary of track name and length), and Album class would also need to change if we change the database to store these albums ( for example moving to an online database from an Excel sheet). It is clear that these are two distinct responsibilities.
Instead, We should create a different class for interacting with the Albums database. This could be expanded by searching albums by starting letters, number of tracks, etc.

Note: Oversimplifying the class will make the code lengthy and make it hard to follow the long chain of objects being passed between different components and may lead to code fragmentation. This principle does not literally mean for each class to do one single thing as in one method but as one concept.

2. Open/Closed Principle

classes should be open for extension and closed for modification.

Modification means changing the code of an existing class, and extension means adding new functionality.

This principle states that We should be able to add new functionality without touching the existing code for the class. This is because whenever we modify the existing code, we are taking the risk of creating potential bugs. So we should avoid touching the tested and reliable (mostly) production code if possible.

If this principle is not followed, the result could be a long list of changes in depending classes, regression on existing features, and unnecessary hours of testing.

Let’s use the same Example

Now, Let‘s say We want to search by artist and genre What if we plan to add the release year? We’ll have to write a new function every time (in total (2^n)-1 to be precise), and the number grows exponentially.

Instead, We should define a base class with a common interface for the specification, and then define subclasses for each type of specification that inherits this interface from the base class

This allows us to extend the searches with another class when we want (e.g. by release date). Any new search class will need to satisfy the interface defined by Searchby, so we won’t have surprises when interacting with our existing code. To browse by the search by criteria, we now need to create a SearchBy object first and pass that into AlbumBrowser.

But what about multiple criteria? For This, we can use & for join browsing criteria to be joined together

This & method can be a bit confusing, so the following example demonstrates its usage:

3. Liskov Substitution Principle

This principle was formulated by Barbara Liskov and states that

subclasses should be substitutable for their base classes.

This means that, given that class B is a subclass of class A, we should be able to pass an object of class B to any method that expects an object of class A and the method should not give any weird output in that case.

This is the expected behavior because when we use inheritance we assume that the child class inherits everything that the superclass has. The child class extends the behavior but never narrows it down.

Therefore, when a class does not obey this principle, it leads to some nasty bugs that are hard to detect.

In the classic example with rectangles and squares, we create a Rectangle class, with width and height setters. If you have a square, the width setter also needs to resize the height, and vice versa to keep the square property. This forces us to make a choice: we either keep the implementation of the Rectangle class, but then Square stops being a square when you use the setter on it, or you change the setters to make height and width the same for squares. This could lead to some unexpected behavior if you have a function that resizes the height of your shape.

While this might not look like a big deal (surely you can just remember square changes the width too?!), this becomes a bigger issue when the functions are more complicated or when you are using someone else’s code, and just assume the subclass behaves the same.

4. Interface Segregation Principle

Many client-specific interfaces are better than one general-purpose interface. Clients should not be forced to implement a function they do not need.

Segregation means keeping things separated, and the Interface Segregation Principle is about separating the interfaces.

If you have a base class with many methods, possibly not all of your subclasses are going to need them, maybe just a few. But due to inheritance, you will be able to call these methods on all the subclasses, even on those that don’t need it. This means a lot of interfaces that are unused, and unneeded and will result in bugs when they get accidentally called.

This principle is meant to prevent this from happening. We should make interfaces as small as possible so that we don’t need to implement functions we don’t need. Instead of one big base class, we should split them into multiple ones. They should only have methods that make sense for each and then have our subclasses inherit from them.

In the next example, we will be using abstract methods. Abstract methods create an interface in a base class that has no implementation but is forced to be implemented in every subclass that inherits from the base class. Abstract methods are essentially enforcing an interface.

Instead, we could have a class for the singing and the music separately (assuming guitar and drums always happen together in our case, otherwise we need to split them up even more, perhaps by instrument.) This way, we only have the interfaces we need, we cannot call sing lyrics on instrumental songs.

Now our model is much more flexible and extendable, and the clients do not need to implement any irrelevant logic.

5. Dependency Inversion Principle

Our classes should depend upon interfaces or abstract classes instead of concrete classes and functions.

In his article (2000), Uncle Bob summarizes this principle as follows:

“If the Open/Closed Principle states the goal of Object Oriented architecture, then Dependency Inversion Principle states the primary mechanism”.

In other words, High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).

Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

If your code has well-defined abstract interfaces, changing the internal implementation of one class shouldn’t break your code. A class it interacts with should not have knowledge of the inner workings of the other class, and should be unaffected as long as the interfaces are the same. An example would be changing the type of database you use (SQL or NoSQL) or changing the data structure you store your data in (dictionary or list).

This is illustrated in the following example, where ViewRockAlbums explicitly depends on the fact that albums are stored in a tuple in a certain order inside AlbumStore. It should have no knowledge of the internal structure of Albumstore. Now if we change the ordering in the tuples in the album, our code would break.

Instead, we need to add an abstract interface to AlbumStore to hide the details, that can be called by other classes. This should be done as in the example in the Open-Closed Principle, but assuming we don’t care about filtering by anything else, I’ll just add a filter_by_genre method. Now if we had another type of AlbumStore, that decides to store the album differently, it would need to implement the same interface for filter_by_genre to make ViewRockAlbums work.

Benefits of applying SOLID Principles

  • Encourages good code-level design.
  • Drives consistency in design.
  • Increases cohesion in classes.
  • Reduce Coupling between classes.
  • Reduce the likelihood of breaking changes.
  • Simplifies debugging.
  • Simplifies Code maintenance.
  • Simplifies Unit testing.

Potential Offenders of SOLID Principles

  • Utility Classes (i.e. where everyone puts all the functions they don’t want to think about).
  • User Interfaces where business logic is built on the UI layer.
  • Classes that become massive ‘GOD Objects’ containing most business logic for an entire application.
  • Code that is written in such a way that it has to do explicit instanceOf checks before operating on objects.

Conclusion

The SOLID design principles are meant to be a guideline to write maintainable, expandable, and easy-to-understand code. It is worth keeping them in mind next time you think of a design, to write SOLID code.

Good Design adds value faster than it adds cost

— Thomas.C.Gale

References

  1. The SOLID Principles of Object-Oriented Programming Explained in Plain English — https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/
  2. Udemy Course — https://www.udemy.com/course/1-hour-crash-course-using-solid-to-write-better-code/

If you found this blog useful do leave a clap 👏 or comment 💬 . Any feedback or constructive criticism is welcomed 🙌.

You can reach out to me on LinkedIn or Twitter or in the comment section below.

Happy Coding ..!!! 💻

--

--

Mohammed Vaghjipurwala
Geek Culture

Principal Architect | Programmer by day| Avid Reader | Caffeine Addict