The SOLID Principles of Object Oriented Design

SOLID principles are the five principles that simplify our work by making code more flexible and maintainable. They have object-oriented programming in mind, but they are helpful with any kind of software engineering.

These principles are often discussed during job interviews, so make sure to remember and understand them.

These principles are:

  • S – Single responsibility principle: A class should have only one reason to change.
  • O – Open-closed principle: Objects or entities should be open for extension but closed for modification.
  • L – Liskov substitution principle: Derived classes must be substitutable for their base classes.
  • I – Interface segregation principle: Make fine-grained interfaces that are client-specific.
  • D – Dependency inversion principle: Depend on abstractions, not on concretions.

Single responsibility principle

This principle states that there should be only one reason to change a class. It applies when the class is doing only one thing, hence the single responsibility name.

Here are a few practical ways to make sure we follow the single responsibility principle:

  • Do not manage different entities in the same class: Imagine we’re writing a concert capacity management application and have a service with methods that manage concerts and reservations. It makes sense to split classes like these into two.
  • Check if the functionality should be in the same class: If we have a service that exports concert data to CSV and PDF in the same application, it should have two different classes. It may seem like a service like this has a single responsibility of exporting concerts, but exporting to CSV and PDF are separate use cases and should be treated as such.
  • Make sure to avoid mixing different levels of logic in one place: This one is probably the hardest one to notice. For example, imagine the concert exporting service retrieves the data from the database, aggregates it into some metrics, and renders it as a PDF. These are three logic levels, and we should divide them into three different classes. Again, it might look like the service is doing one thing, exporting concerts, but each level of logic has its own responsibility.

Making a separate class for each case might seem wasteful, but it will help us in the future.

  • It minimizes the chance of breaking something unrelated when we change something.
  • It simplifies testing our classes.
  • It simplifies reusing our classes.
  • It decreases the chance of conflicts when merging different branches.
  • It helps to keep everything clear in our mind.

If we don’t follow this principle, we might end up with bloated classes that are difficult to maintain because they do too much.

Open-Closed principle

This principle states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. Usually, this means we should design modules in a way that allows us to extend them without modifying them. It’s vital when creating a library or an application to be used by someone else because they have no way to change it.

It’s not always obvious how to achieve this in practice, but one thing might help us is moving dependencies into configurable imports.

Example

Imagine we have a statistics exporter service that aggregates concert statistics and exports them to either comma-separated values (CSV) or PDF. It might look like this:

Get hands-on with 1200+ tech skills courses.