SOLID Principles of Object-Oriented Programming

  • Post published:May 9, 2020

In object-oriented programming, SOLID is an acronym for 5 important design principles intended to make software design more reusable, extensible, simplistic and maintainable. The 5 principles are:

Single-Responsibility PrincipleA class or module should have one, and only one, reason to be changed.
Open-Closed Principle“Software entities (classes, modules, methods, etc.) should be opened for extension, but closed for modification.”
Liskov Substitution Principle“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
Interface Segregation Principle“Clients should not be forced to depend upon interfaces that they do not use.”
Dependency Inversion Principle“High level modules should not depend upon low level modules. Both should depend upon abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.”

These principles are promoted by Robert C. Martin.

Single-Responsibility Principle (SRP)

A class or module should have one, and only one, reason to be changed.

If a class has more than one reason to change, then it has more than one responsibility. This is a violation of the single-responsibility principle. A class should have only a single responsibility; “do one thing and do it really well”.

As an example, consider the below class:

The above Report class can compile and print a report. This class is a violation of the single-responsibility principle since this class can be changed for two reasons. First, the content of the report could change when compiling a report. Second, the format of the report could change when printing the report.

The single-responsibility principle says that these two aspects of the problem are really two separate responsibilities, and should therefore be in separate classes. It is a bad design to couple two things that change for different reasons at different times.

To fix this bad design, the printing of a report feature should be moved to a separate class:

The Report class now has a single responsibility of compiling a report whereas the ReportPrinter class has a single responsibility of only printing the report.

Open-Closed Principle (OCP)

Software entities (classes, modules, methods, etc.) should be opened for extension, but closed for modification.

The purpose of this principle is to ensure that a software entity can allow its behavior to be extended without modifying its source code. By doing this, existing code is not being modified, thus preventing new bugs from been introduced.

As an example, consider the below class:

The above Wallet class was a new feature that a company offered to enable potential customers to create a Wallet to handle deposits, withdrawals, transfers and viewing of the wallet balance.

After a few months of offering the Wallet feature, the business decided that the Wallet was a great concept, thus decided to offer a savings wallet, whereby a savings wallet has an additional feature of earning interest.

Since the business is growing rapidly and a lot of pressure is been applied to the technical team to deliver the new feature as early as possible, it might be tempting to just add an interest method to the existing Wallet. This design will be very error prone and will cause more issues later on if the business decides to add more variations of a Wallet.

Instead, by using the open-closed principle and re-evaluating the design, the existing Wallet can be extended to create a new SavingWallet:

Extending the Wallet class ensures that the existing features of the application are not impacted when adding the new SavingWallet feature.

Liskov Substitution Principle (LSP)

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

The purpose of this principle is to ensure that a sub-class (or derived class) can be substituted with their base (or parent) class.

As an example, consider the below feature:

Create a feature to calculate the fees for each product. The fee requirements are as follows:

  • All products will always have a fee for admin costs. The admin fee will be 1% of the cost of the product.
  • If the cost of a product is more than 1000.00, then an additional fee of 10.00 is to be charged.

Below is an example implementation of this feature:

After examining the above implementation, it can be seen that this is a violation of the liskov substitution principle since the AdditionalFee sub class cannot be substituted with its base class when looking at the FeeHandler implementation. The AdditionalFee class created a new Calculate overload method that accepts an additionalFee.

The below is a possible solution to resolve this issue and to adhere to the liskov substitution principle:

The above code now satisfies the liskov substitution principle. The liskov substitution principle is arguably the most complex of the 5 principles making it a challenge to spot and resolve this violation.

Interface Segregation Principle (ISP)

Clients should not be forced to depend upon interfaces that they do not use.

If a class implements an abstract class or an interface, then the class should not be forced to implement parts that it does not care about.

As an example, consider the below class:

In the above code, a computer AutomatedSwitchboardOperator does not need to take a break, thus the AutomatedSwitchboardOperator does not care about the TakeABreak method. This implementation violates the interface segregation principle.

Instead of having a single interface, the interface segregation principle can be applied as follows:

The above code now satisfies the interface segregation principle.

Dependency Inversion Principle (DIP)

High level modules should not depend upon low level modules. Both should depend upon abstractions.

Abstractions should not depend upon details. Details should depend upon abstractions.

This principle is aimed at decoupling software modules and ensures that dependencies for a class depend on abstractions rather than concrete implementations.

As an example, consider the below class:

After inspecting the above code, it can be clearly seen that the EmailValidator is tightly coupled to logging an error to a console. If the logging mechanism changes in the future to a file logger or database logger, then this file needs to be modified thereby running the risk of possibly breaking this feature.

In order to rectify this, the logging should be moved to a class of its own, i.e. invert the responsibility of the logging feature from the EmailValidator to another logging class, thereafter make the new logging class a dependency in the EmailValidator class.

Consider the below solution:

The above implementation has successfully decoupled the logging feature from the EmailValidator, however it has not fully satisfied the dependency inversion principle since the EmailValidator is dependent on a concrete implementation of the Logger. The EmailValidator should be dependent on an abstraction, not a concrete implementation.

Consider the below possible solution:

The above code now satisfies the dependency inversion principle. Different types of loggers can now be created by implementing the ILogger interface without impacting the EmailValidator implementation.

Summary

The SOLID principles are a very powerful set of design principles to help create reusable, extensible, simplistic and maintainable code. Once these principles are mastered, it becomes very easy to design features.

Further information on SOLID can be found at the following link: