Solid Principles With (almost) Real Life Examples In Java

2 minute read


A different look at the 5 SOLID principles

SOLID principles are some of the oldest rules in the software world. They enable us to write maintainable, readable, reusable code. In this text, I am trying to accomplish a somewhat real-life example, obeying the SOLID principles.

Single Responsibility

Each class should have only one sole purpose, and not be filled with excessive functionality. Consider the following example:

This class is implemented for hashing passwords, as the name implies. It should not be its responsibility to save them to the database. Each class should have a single responsibility to fulfill. There should not be “god classes” that have a broad variety of functionality that has too much to accomplish. Instead, we should write our classes as modular as possible. Implement the saving operation in another class.

Open-Closed Principle

Classes should be open for extension, closed for modification. In other words, you should not have to rewrite an existing class for implementing new features. Let’s continue to our password-hasher example. Suppose we want our class to be able to hash with a variety of algorithm options. If we implemented this way, we would break the O in SOLID so bad. Every time a new algorithm is implemented, we need to modify the existing class, and it looks ugly. Thanks to OOP, we have abstraction. We should make our initial class an interface/abstract class and implement the algorithms in the concrete classes.

In this way, we can add new algorithms without touching the existing codebase.

Liskov-Substitution Principle

A sub-class should be able to fulfill each feature of its parent class and could be treated as its parent class. To demonstrate our example, let’s create the Model (Data) Classes to use our hashing algorihms.

And we implemented the same for other encodings… To fulfill Liskov’s Rule, each other extension of Hashed should use a valid implementation of hashing function and return a hash. For example, if we extend the Hashed class with a class called “NoHash” that uses an implementation that returns exactly the same password without any encoding will break the rule, since a subclass of Hashed is expected to have a hashed value of the password.

Interface Segregation Principle

Interfaces should not force classes to implement what they can’t do. Large interfaces should be divided into small ones. Consider we add decoding feature to the interface.

This would break this law since one of our algorithms, the SHA256 is not decryptable practically, (it’s a one-way function). Instead, we can add another interface to the applicable classes to implement their decoding algorithm.

Dependency Inversion Principle

Components should depend on abstractions, not on concretions. We have a password service like the following:

We violated the principle since we tightly coupled the Base64Hasher and PasswordService. Let’s decouple them and let the client inject the hasher service needed with the constructor.

Much better. We can easily change the hashing algorithm. Our service does not care about the algorithm, it’s up to the client to choose it. We don’t depend on the concrete implementation, but the abstraction.