OOP Design Patterns — SOLID Design Principles

Dinesha Karunathilake
3 min readMay 29, 2020

SOLID is an important set of principles in OOP software development and the name is an acronym for the five principles it explains.

1. Single Responsibility Principle

The idea of this principle sounds simpler than it actually is and it says that a class should do one thing. Another way to understand this is that a class should have one and only one reason to change.

When the code respects the single responsibility principle, it has the following benefits.

  • More Understandable code
  • Less Impact of a change: lowers the complexity of a change
  • Lesser Frequency of change with the requirement changes
  • Avoids any unnecessary coupling between responsibilities

But it is important not to oversimplify the code. We have to take the principles with a grain of salt and decide depending on the requirement and circumstances.

2. Open Closed Principle

Classes/components must be open for extension but closed for modification. We should be able to add new functionality without changing the existing code.

Inheritance is one way suggested to do this, but inheritance introduces tight coupling if the subclasses depend on implementation details of their parent class. Interfaces are preferred in this case over inheritance (Polymorphic Open Closed Principle)

This results in less coherent more flexible code.

3. Liskov Substitution Principle

Definition: An object of type T may be substituted with any object of any subtype S without altering the desirable properties of S. That requires the objects of subclasses to behave in the same way as the objects of the superclass.

Composition over inheritance is a way to write code so that this principle is not violated. And Interfaces should be segregated as possible to make the system more flexible

4. Interface Segregation Principle

Interfaces should be broken into small related collections of members that will be implemented by classes that implement that interface according to requirement.

Many client-specific interfaces are better than one general-purpose interface.

When a change comes and there is one big interface, which must be used in many places in the code, every place must be changed. But not all these implementing classes will share the same properties.

It is better to consider the problem and requirement and segregate the interface to smaller ones and implement them accordingly.

For example, if there is an Interface Animal with multiple behaviours like swim, eat, fly, each subclass of Animal will be forced to override all those. But if its a Fish, it won’t have a fly behaviour. So best is to have interfaces Flyable, Swimmable and implement them accordingly.

5. Dependency Inversion Principle

High-level modules, which provide complex logic, should be easily reusable and unaffected by changes in low-level modules, which provide utility features. To achieve that, you need to introduce an abstraction that decouples the high-level and low-level modules from each other. Both high level and low-level modules should depend on same abstraction.

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

This means we can change the behaviour at runtime than in compile-time and this improves the testability of code

Example:

There is an Interface Animal with a speak method and Cat, Dog etc classes implementing it. Cat class will instantiate a CatSpeak behaviour and Dog class will instantiate a DogSpeak behaviour and which will return the required behaviour. But this means the Cat class is coupled to a low-level module.

Instead what should be done according to DI principle is that CatSpeak behaviour must be injected into Cat class when it is constructed (Constructor Injection) or to the speak method as a parameter (Parameter injection)

--

--