Published on

[SOLID] Liskov Substitution Principle

Authors
  • avatar
    Name
    Khánh
    Twitter

When to Use

Statement

If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.

This principle states that every subclass should be substitutable for its superclass without altering the correctness of the program.

Example

class Rectangle {
   protected w!: number;
   protected h!: number;

   public getW(){
    return this.w;
   }

   public setW(newW: number){
    this.w = newW
   }

   public getH(){
    return this.h;
   }

   public setH(newH: number){
    this.h = newH
   }

   public calcArea(){
       return this.w * this.h;
   }
}

class Square extends Rectangle {
  public calcArea (){
    return this.w * this.h;
  }

  public setH(side: number){
    this.w = side;
    this.h = side;
  }

  public setW(side: number){
    this.w = side;
    this.h = side;
  }
}

const rect = new Rectangle();
rect.setH(2);
rect.setW(3);
console.log(rect.calcArea())

const square = new Square();
square.setH(2);
square.setW(3);
console.log(square.calcArea())

Since a square is a special case of a rectangle with two equal sides, it inherits from the Rectangle class. However, using the Square class to replace the Rectangle class violates the Liskov Substitution Principle.

To address this issue, we create a Shape class and have both Square and Rectangle inherit from it.


abstract class Shape {
  abstract calcArea():number
}

class Rectangle extends Shape{
  protected w!: number;
   protected h!: number;
   public getW(){
    return this.w;
   }

   public setW(newW: number){
    this.w = newW;
   }

   public getH(){
    return this.h;
   }

   public setH(newH: number){
    this.h = newH
   }

   public calcArea(){
       return this.w * this.h;
   }
}

class Square extends Shape {
  protected s!: number;
  public calcArea (){
    return this.s * this.s;
  }

  public setSide(side: number){
    this.s = side;
  }

}

let rect = new Rectangle();
rect.setH(2);
rect.setW(3);
const square = new Square();
square.setSide(2)
const result:Shape[] = [square, rect];
result.forEach((shape: Shape) => console.log(shape.calcArea()))

Looking at the code snippet above, we observe that the Square and Rectangle classes no longer inherit from each other but inherit from the Shape class instead. Therefore, all classes inheriting from Shape can be called into a function (calculated in the loop through the calcArea function). However, because they inherit from Shape and Square, Rectangle are separate classes, the results returned by the calcArea function will differ

Conclusion

This principle generates significant controversy, so it's crucial to understand it thoroughly when designing to consider relationships and correctness when substituting a parent class with a subclass.

References