Overview of SOLID Principles
Beginners guide to understanding SOLID Design Principles
Table of contents
No headings in the article.
SOLID Principles
One of the widely used Design Principles, SOLID is an acronym for the five principles which constitute it.
- S => Single Responsibility Principle
- O => Open Closed Principle
- L => Liskov Substitution Principle
- I => Interface Segregation Principle
- D => Dependency Inversion Principle
Single Responsibility Principle
A class should have one and only one reason to change, meaning a class should have only one job.
What it essentially means is that every object should have a single responsibility and all its services should be aligned with that responsibility.
Example:'
Let us take a Book class, which has methods:
- getTitle() => returns title of Book
- getAuthor() => returns author of Book
- getLocation() => returns book's location in library
The getLocation() method here is a violation of the SRP Principle because this method is not related to book entity.
A right approach to solve this problem would be: Having a separate BookLocator class which handles this method.
Modified Example
Book class:
- getTitle() => returns title of Book
- getAuthor() => returns author of Book
BookLocator class:
- getLocation() => returns book's location in library
Open Closed Principle
Software entities should be open for extension but closed for modification.
The idea here is that rather than modifying an already working code, focus should be on extending features on the top of it. This way we will have to test only the extra added feature because rest is already tested.
Example:
Lets take an AreaCalculator class which has a method:
- calculateArea(): calculates and adds area of different shapes.
When we add different shapes, code modification will be needed (for calculating area of newly added shape) and this will be violation of Open Closed Principle.
A right approach to solve this problem would be:
- Have an interface IShape which has area method.
- Every shape implements IShape interface and area is calculated in the class itself.
- calculateArea() method in AreaCalculator class simply adds the area of all shapes and returns it.
Liskov Substitution Principle
If S is a subtype of T, then objects of type T may be replaced by objects of type S.
In simpler terms, if a base class satisfies a contract, then LSP says that derived classes must also satisfy that contract.
Example:
Let us take a Bird class which has a method:
- fly(): how the bird flies
This class seems fine for birds which fly, but what about the birds like Ostrich which do not fly?
Ostrich class is subtype of class Bird, but it can't use the fly() method. This is violation of Liskov Substitution Principle.
How do we correct this?
Let's have a separate class for birds who fly named FlyingBirds which extends Bird class.
public class Bird {}
public class FlyingBirds: Bird
{
public void fly() {}
}
public class Pigeon: FlyingBirds {}
public class Ostrich: Bird {}
Interface Segregation Principle
Classes should not be forced to depend upon interfaces that they do not use.
If we have methods in the interface which are of no use to the implementing class, we will have to implement those methods in the class to satisfy the compiler. This is bad practice and leads to "fat interface".
Example:
interface OrderService() {
public void orderFries();
public void orderBurger();
}
Now when a burger service implements this interface, it will have to implement orderFries() method too. If you look closely, this is a violation of Single Responsibility Principle as well.
Solution to this would be having separate interfaces for Burger and Fries.
interface IBurgerService() {
public void orderBurger();
}
interface IFriesService() {
public void orderFries();
}
Dependency Inversion Principle
High level modules should not depend on low level modules; both should depend on shared abstractions. In addition, abstractions should not depend on details, instead details should depend on abstractions.
This principle is mainly used to decouple the modules from each other. The idea here is that the high-level modules should not be affected by the changes made in low level modules. This can be achieved by providing an abstraction that decouples both the modules from each other.
The abstractions are usually in form of a class or interface.
Traditional implementation
High Level (HL) Code --> Low Level (LL) Code.
With dependency inversion
High Level (HL) Code --> HL/LL service interface <-- Low Level (LL) Code.
using System;
public class HelloWorld
{
public static void Main(string[] args)
{
Monitor monitor = new Monitor();
Keyboard keyboard = new Keyboard();
Desktop desktop1 = new Desktop(monitor);
Desktop desktop2 = new Desktop(keyboard);
desktop1.getComponent();
desktop2.getComponent();
}
}
public class Desktop
{
private IComponent component;
public Desktop(IComponent _component)
{
this.component = _component;
}
public void getComponent()
{
component.setComponent();
}
}
// this interface defines contract for all components of Desktop class
public interface IComponent
{
public void setComponent();
}
public class Monitor : IComponent
{
public void setComponent()
{
Console.WriteLine("this is monitor");
}
}
public class Keyboard : IComponent
{
public void setComponent()
{
Console.WriteLine("this is keyboard");
}
}
References : Wikipedia, Stackify, YouTube etc.
Cover Image : freecodecamp.org/news/solid-principles-expl..