Design Principles (S.O.L.I.D)

What are SOLID Principles ?
5 Concepts that make up the SOLID principles.
  1. Single Responsibility
  2. Open - Closed
  3. Liskov Substitution
  4. Interface Segregation
  5. Dependency Inversion


1. Single Responsibility Principle (SRP)

A class should have only one responsibility and It should only have one reason to change.

When designing our classes, we should aim to put related features together, so whenever they tend to change they change for the same reason. And we should try to separate features if they will change for different reasons. —- Steve Fenton

How does this principle help us to build better software ?


Note :- This principle applies not only to classes, but also to software components and microservices.


Example:

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
    saveAnimal(a: Animal) { }
}
How does this Animal Class violate SRP ?
How will this design cause issues in the future?
To make this conform to SRP, create another class to handle the sole responsibility of storing an animal to a database.
class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}

class AnimalDB {
    getAnimal(a: Animal) { }
    saveAnimal(a: Animal) { }
}
How to validate the design ?


2. Open-Closed Principle (OCP)

Software entities(Classes, modules, functions) should be open for extension but not for modification.

This principle is the foundation for building code that is maintainable and reusable. —- Robert C. Martin

How can something be open and closed ?


Example:

class Discount {
    getDiscount() {
        return this.price * 0.2
    }
}
class Discount {
    getDiscount() {
        if(this.customer == 'fav') {
            return this.price * 0.2;
        }
        if(this.customer == 'vip') {
            return this.price * 0.4;
        }
    }
}
How the above class fails OCP ?
To make it follow the OCP principle, we will add a new classes that will extend the Discount and implement its new behavior.
class VIPDiscount: Discount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}

class SuperVIPDiscount: VIPDiscount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}
How do we achieve OCP every time ?
How do we know whether to make Shape a class or an interface ?


3. Liskov Substitution Principle (LSP)

A sub-class must be substitutable for its super-class.

What is wanted here is something like the following substitution property:

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T. —- Barbara Liskov

What is LSP ?


Example:-

//...
class Pigeon extends Animal {
        
}
const animals[]: Array<Animal> = [
    //...,
    new Pigeon();
]
    
function AnimalLegCount(a: Array<Animal>) {
    for(int i = 0; i <= a.length; i++) {
        if(typeof a[i] == Lion)
            log(LionLegCount(a[i]));
        if(typeof a[i] == Mouse)
            log(MouseLegCount(a[i]));
         if(typeof a[i] == Snake)
            log(SnakeLegCount(a[i]));
        if(typeof a[i] == Pigeon)
            log(PigeonLegCount(a[i]));
    }
}
AnimalLegCount(animals);
To make this function follow the LSP principle, follow this LSP requirements postulated by Steve Fenton:
class Animal {
    //...
    LegCount();
}

//...
class Lion extends Animal{
    //...
    LegCount() {
        //...
    }
}
//...

function AnimalLegCount(a: Array<Animal>) {
    for(let i = 0; i <= a.length; i++) {
        a[i].LegCount();
    }
}
AnimalLegCount(animals);

Note :- The Animal class have to implement or define a LegCount method and its sub-classes have to implement the LegCount method.


4. Interface Segregation Principle (ISP)

Make fine grained interfaces that are client specific.

Clients should not be forced to implement interfaces they do not use. —- Robert C. Martin

What is ISP ?


Example:

interface IShape {
    drawCircle();
    drawSquare();
    drawRectangle();
}
class Circle implements IShape {
    drawCircle(){
        //...
    }    drawSquare(){
        //...
    }    drawRectangle(){
        //...
    }    
}
class Square implements IShape {
    drawCircle(){
        //...
    }    drawSquare(){
        //...
    }    drawRectangle(){
        //...
    }    
}
class Rectangle implements IShape {
    drawCircle(){
        //...
    }    drawSquare(){
        //...
    }    drawRectangle(){
        //...
    }    
}
To make our IShape interface conform to the ISP principle, we segregate the actions to different interfaces:
interface IShape {
    draw();
}

interface ICircle {
    drawCircle();
}

interface ISquare {
    drawSquare();
}

interface IRectangle {
    drawRectangle();
}

class Circle implements ICircle {
    drawCircle() {
        //...
    }
}

class Square implements ISquare {
    drawSquare() {
        //...
    }
}

class Rectangle implements IRectangle {
    drawRectangle() {
        //...
    }    
}

class CustomShape implements IShape {
   draw(){
      //...
   }
}

Alternatively:

class Circle implements IShape {
    draw(){
        //...
    }
}

class Triangle implements IShape {
    draw(){
        //...
    }
}

class Square implements IShape {
    draw(){
        //...
    }
}

class Rectangle implements IShape {
    draw(){
        //...
    }
}                                            


5. Dependency Inversion Principle (DIP)

Depend on abstractions, not on concretions.

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

B. Abstractions should not depend upon details. Details should depend upon abstractions. —- Robert C. Martin

What is Dependency Inversion Principle ?


Example:

class XMLHttpService extends XMLHttpRequestService {
}

class Http {
    constructor(private xmlhttpService: XMLHttpService) { }
    get(url: string , options: any) {
        this.xmlhttpService.request(url,'GET');
    }    post() {
        this.xmlhttpService.request(url,'POST');
    }
    //...
}
To make our Http class conform to DIP we introduce Connection Interface abstraction.
interface Connection {
    request(url: string, opts:any);
}
class Http {
    constructor(private httpConnection: Connection){}    
    get(url: string , options: any) {
        this.httpConnection.request(url,'GET');
    }    post() {
        this.httpConnection.request(url,'POST');
    }
    //...
}
class XMLHttpService implements Connection {
    const xhr = new XMLHttpRequest();
    //...
    request(url: string, opts:any) {
        xhr.open();
        xhr.send();
    }
}

class NodeHttpService implements Connection {
    request(url: string, opts:any) {
        //...
    }
}

class MockHttpService implements Connection {
    request(url: string, opts:any) {
        //...
    }    
}