Learn SOLID Design Principles in Java by Coding It | by Pedro Luiz | Mar, 2022

Photo by Max Duzij on Unsplash
  1. What is SOLID and why you should bother using it
  2. Single Responsibility Principle
  3. Open Closed Principle
  4. Liskov Substitution Principle
  5. Interface Segregation Principle
  6. Dependency Inversion Principle

In this article, we will be discussing the SOLID design principles. First, we will understand why they came out, and then we will understand how to implement each principle with code examples.

In his paper Design Principles and Design Patterns, Robert C. Martin introduced the principles, and later on, Michael Feathers introduced that acronym.

Design principles in general encourage us to write better software, more maintainable, understandable, and flexible. It also improves the developer experience of those who are going to be part of your team in the future.

SOLID stands for:

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

This principle states that a class should have only one responsibility, in other words, a class should have only one reason to change.

It means that only when there is a change on the functionality that our class has, it should change.

You can have benefits, such as, onboarding new members will be easier, testing will be easier, and so on.

Many frameworks and libraries follow this principle. For example, CrudRepository from Spring Data, the Validation API, the Date/Time API.

Use Case

Imagine that we have a class ProductService that has two concerns and responsibilities:

  1. Manipulating crud operations on a product.
  2. Sending SMS and EMAIL notifications based on crud operations.

SingleResponsibility.java

What happens if the requirements of this class change, and now we have a text email to be sent and an HTML email to be sent that demand different implementations of the sendEmail() method ?

And then later on we can have a different product manipulation requirement that also demands changes.

Following this, the ProductService class changes based on notification related reasons and product related reasons.

It would be better to separate those concerns:

SingleResponsibility.java

Even though we are creating different methods on each requirement change, the idea is to separate concerns here, the solutions to solve this kind of problem is a whole different topic.

Common Discussions

Every engineer/team has their own idea of ​​a reason to change. There is no strict idea to follow when it comes to deciding a class purpose. It all depends on your own business rule, your project, your team, etc.

The key is not to overthink. Try to think of a single responsibility being, even if methods perform different operations, do they operate on the same purpose?

This principle states that software entities (classes, modules, functions, etc.) should be opened to extension but closed to modification.

It means that you should avoid having to modify the logic of something on your system when the components are growing. It is easier to visualize in a real example.

Use Case

Imagine that we have the famous problem of calculating the area of ​​geometric shapes.

AreaCalculator.java

Even with a basic example, it can be boring having to change AreaCalculator Each time a new shape area needs to be calculated, with a new method because the logic changes each time.

We can make use of simple abstraction and polymorphism to handle this.

AreaCalculator.java

that way, AreaCalculator won’t know which and how many shapes there are to handle, and its own implementation.

Now that class is opened for extension (Triangle, Circle, Rectangleetc.) and closed for modification(won’t have a method with a different logic being added every time a new shape comes up).

This principle states substitutability of a class by its subclass, so a class can be replaced by its subclass in all practical usage scenarios, meaning that you should use inheritance only for substitutability.

“Subtypes must be substitutable for their base types.”

— Robert C. Martin

“If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the unchanged when o1 is substituted behavior for o2 then S is a subtype of T.”

— Barbara Liskov

In a nutshell:

A ChildClass should only extend a ParentClass if we can replace a ParentClass object by a ChildClass object without changing the behavior of the program, otherwise we should use Composition or Delegation.

Use Case

Imagine we have a parent class Bird. We can have many child classes, such as Sparrow, Ostrich, Eagle, Falcon, etc.

Is it right to have a Sparrow and an Ostrich class, extending a Bird class ? Following the Liskov Substitution Principle, it isn’t.

Even though an Ostrich is also a Bird, it doesn’t make sense to have its object being able to fly, because it can’t.

We could break up the inheritance into a smaller level to follow this principle.

That way, the Bird class won’t be replaced in the wrong way by a Sparrow class in any scenario.

This principle states that a client shouldn’t be forced to implement an interface that it doesn’t use.

It is kind of like the Single Responsibility Principle, but at an interface level.

Use Case

Imagine if we have an interface Worker with two methods, work() and sleep(). That way, every concrete worker class will be able to work and sleep.

Does it make sense to have the Robot worker implementing the sleep() method even though we know it can’t ?

We can solve this by breaking up the interface into smaller and more specific interfaces.

That way, we can reduce the side effects of using larger and general interfaces, and have each interface serving a single purpose.

“High-level modules should not depend on low-level modules. Both should depend on abstractions.

— Robert C. Martin

“Abstractions should not depend on details. Details should depend on abstracts.

— Robert C. Martin

This principle states that we should invert the classic dependency between higher level modules and lower level modules, by abstracting their interaction.

It splits the dependency between the high level and low level modules, by introducing an interface or abstract class between them.

At the end of the day, you end up with a high level module that depends on an abstraction and a low level module that depends on an abstraction.

Use Case

UML Diagram

Our high-level would be CustomerService, our low-level would be MySqlImpl and PostgreSqlImpl and our abstraction would be CustomerRepository.

CustomerService (high-level)

CustomerRepository (abstraction)

MySqlImpl (low-level)

PostgreSqlImpl (low-level)

That way, any implementation of CustomerRepository you use with CustomerService will be independent of a unique database for example, in case you need to change to SqlServer eventually, you will depend on having another implementation, without your high-level CustomerService knowing what’s going on.

Leave a Comment