SOLID Design Principles: Why and How?

Priyanka Singh
8 min readJun 6, 2021

--

Not many of the developers know the concept of SOLID, even after spending enough time in development career. Simply knowing the acronym of SOLID is not going to help. We should also understand the importance of SOLID principle and how to apply this concept in the real time project.
Before going into detail of SOLID, Let’s understand major three factors for a successful software.

A successful software application, depends on:
1. Architecture
2. Design Patterns
3.Design Principles

Software Architecture: Architecture serves as a blueprint for a system. It defines a structured solution to meet all the technical and operational requirements, while optimising the common quality attributes like performance and security.

Design Patterns: Software design provides a design plan that describes the elements of a system, how they fit, and work together to fulfil the requirement of the system.

Design Principles: Software design principle is a set of rules, guidelines and recommendations that a developer should follow during the development and program implementation. It will help developer to write beautiful, clear and maintainable code.

Design Principles: Application development process need to follow the design principles
Design Patterns: We need to choose correct design patterns to build the software

Common Problems:
We face many challenges when we don’t follow design principles
* Working in legacy code
* Re-read code multiple times to get the part you need to change
* Hard to Understand what a method does
* Spending a lot of time to fix a minor bug
* And Finally you spent more time to reading than writing code.

Why SOLID is important for development:
i. Achieve reduction in complexity of code:
- A single programme can be solved by multiple algorithm. choose the most optimised one.
- Do some smart observation and remove the unnecessary part of programme and optimise your code.
- Use fast input output method.
ii. Increase readability, extensibility and maintenance
iii. Reduce error and implement reusability
iv. Achieve better testability
v. Reduce tight coupling

Coupling and Decoupling:
When Class A depends heavily on ClassB, the chances of ClassA being affected when Class B is changed are high. This is strong coupling.
If Class A depends lightly on ClassB, then the chances of ClassA being affected in any way by a change in the code of ClassB, are low. This is loose coupled relationship.

Loose coupling is good because we don’t want the components of our system to heavily depend on each other. We want to keep our system modular, where we can safely change one part without affecting the other.

Coupling describes the degree of dependency between one entity to another entity. Often classes or objects.
Too much responsibility leads to coupling
.

Decoupling is a coding strategy that involves taking the key parts of your classes’ functionality (specifically the hard-to-test parts) and replacing them with calls to an interface reference of your own design. With decoupling, you can then instantiate two classes that implement that interface.

Decoupling is generally all about seeing whether or not two things need to closely work together or can be further made independent.

SOLID in detail:

Single Responsibility Principle:
Most of the times, developers get it in another way, they think a class should do only one thing. If it is true means a class will have a single public method only and this is really a misconception,
Instead, this principle is all about cohesion. Cohesion is making sure that things that are related are grouped together, and things that aren’t related are not.

The Single Responsibility Principle states that we want to group only those things that satisfy a single responsibility.

Suppose that there is a Shape class that draws a shape and fills it in with color.

This class can be changed in the future for shape and color.
This means that the single responsibility principle is being violated because the class can be changed for more than one reason.
Conforming to the single responsibility principle would result in two separate classes: one for drawing and another for coloring.

Open Closed Principle:
It advises that we should build our functions/classes/modules in such a way that they are “open for extension, but closed for modification”.

Open for Extension: Able to add new features to the classes/modules without breaking existing code. This can be achieved using inheritance and composition.

Closed for Modification: Not to introduce breaking changes to the existing functionality, because that would force us to refactor a lot of existing code and write a whole bunch of tests to make sure that the changes work.

Lets take a look on below example,
What if we add a new shape? What if we remove a shape? What if we want to change the area algorithm for one of the shapes?
Obviously method needs to change,

We can declare the method of area in abstract class shape, Rectangle, Circle and if any new shape comes, All these have to implement area method, as shown in below example.

The standalone calculateArea() method would now look like this:

calculateArea() is now responsible just for looping around the shapes, and invoking the area() method of individual shapes.

Liskov’s Substitution Principle:
The Liskov Substitution Principle is a concept in Object Oriented Programming that states: Every child/derived class should be substitutable for their parent/base class without altering the correctness of the program. In other words, the objects of your subclass should behave in the same way as the objects of your superclass.

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

One of the things people try to do with object oriented programming, is to use inheritance even when it is not appropriate. They may do it just for the sake of reusing the code. Have a look at this example:

The testArea method makes an assumption that is true for Rectangle: setting the width respectively height has no effect on the other attribute. This assumption does not hold for Square.

The Rectangle/Square hierarchy violates the Liskov Substitution Principle (LSP)! Hence Square is behaviourally not a correct substitution for Rectangle.

Interface Segregation Principle:
It states that no client should be forced to depend on methods it does not use. You should split large interfaces into more specific ones that are focused on a specific set of functionalities so that the clients can choose to depend only on the functionalities they need.

The moment you have a fat interface, any changes to that interface will result in changes in all your implementations.

Clients should not be forced to implement interfaces that they don’t use.
ISP recommends that you keep your interfaces as small as possible.

Let’s look at an example:

In above case, both Dog and Tiger need to provide implementations for the groom(). Now, the groom() makes sense for a Dog, but not so much for a Tiger.
However, we are forced to provide a dummy implementation in Tiger to make the code compile.

Here is the solution:
The new interface Pet extends the existing Animal, and also adds its own abstract method groom(). Now, Dog will extend Pet, as it needs both feed() and groom(), whereas Tiger chooses to extend just Animal for feed().

Dependency Inversion Principle:
It states that High-level modules should not depend on low-level modules, but only on their abstractions.
Abstraction should not depend on their details.
Details should depend on the abstraction.

In simple words, It suggests that you should use interfaces instead of concrete implementations wherever possible.
This decouples a module from the implementation details of its dependencies. The module only knows about the behavior on which it depends, not how that behavior is implemented. This allows you to change the implementation whenever you want without affecting the module itself.

Let’s get started with some code that violates that principle.
Say you are working as part of a software team. We need to implement a project. For now, the software team consists of: Backend developer and FrontEnd developer

So as we can see, the Project class is a high-level module, and it depends on low-level modules such as BackEndDeveloper and FrontEndDeveloper. We are actually violating the first part of the dependency inversion principle.

In order to tackle this problem, we shall implement an interface called the Developer interface with refactoring of BackEndDeveloper and the FrontEndDeveloper classes.

In order to tackle the violation of the first part, would be to refactor the Project class so that it will not depend on the FrontEndDeveloper and the BackendDeveloper classes.

The outcome is that the Project class does not depend on lower level modules, but rather abstractions. Also, low-level modules and their details depend on abstractions.

What’s hard is writing code that’s easy to adapt when your requirements change.

If you enjoyed this story, please click the 👏 button and share to help others find it!

--

--