Search code examples
javaoopliskov-substitution-principleinterface-segregation-principle

Liskov substitution principle VS Interface Segregation principle


I have some troubles with understanding these two principles. This is a bit long-read-question, so be patient.

Lets assume that we have a class

abstract class Shape {
    abstract void onDraw();
}

and interface

interface SideCountable {
  int getSidesCount();
}

And then we create two children classes

class Quad extends Shape {

   @Override
   public void onDraw() {
   //some draw logic here
   }
}

class Triangle extends Shape {

   @Override
   public void onDraw() {
   //some draw logic here
   }
}

And now we will make Shape implements SideCountable

abstract class Shape implements SideCountable{
    abstract void onDraw();
}

and make changes in children classes

class Quad extends Shape {

   @Override
   public int getSidesCount() {
       return 4;
   }

   @Override
   public void onDraw() {
   //some draw logic here
   }
}

class Triangle extends Shape {

   public int getSidesCount() {
       return 3;
   }

   public void onDraw() {
   //some draw logic here
   }
}

And create test function for this structure (following LSP) :

public void printSidesCount(List<Shape> shapes) {
    for(int i=0; i < shapes.size(); i++) {
        System.out.println(shapes.get(i).getSidesCount());
    }
}

And here I want to stop because actually in the next step I was stucked. What if we'll create a third class Circle?

class Circle extends Shape {

   public int getSidesCount() {
       return ???;
   }

   @Override
   public void onDraw() {
   //some draw logic here
   }
}

Circle has no sides, so implementation of SideCountable for this children sounds ridiculous. Ok, we can move implementation to Quad and Triangle only, but in this case LSP will not working anymore. Can someone describe what the best way I should to do?

  • Leave SideCountable in Shape class and return 0 for Circle and break interface segregation principle?
  • Move SideCountable to Quad and Triangle and break LSP principle?

Solution

  • First of all, your method printSidesCount only needs to know that the list contains SideCountable objects. So giving its parameter the type List<Shape> is more specific than necessary. Give it a List<SideCountable> instead:

    public void printSidesCount(List<SideCountable> sideCountables) {
        for(int i=0; i < (); i++) {
            System.out.println(sideCountables.get(i).getSidesCount());
        }
    }
    

    Or even List<? extends SideCountable> which means "a list of some arbitrary unknown type that implements SideCountable":

    public void printSidesCount(List<? extends SideCountable> sideCountables) {
        for(int i=0; i < sideCountables.size(); i++) {
            System.out.println(sideCountables.get(i).getSidesCount());
        }
    }
    

    If not all shapes have a countable number of sides, then class Shape should not implement interface SideCountable. Instead, make classes Quad and Triangle implement interface SideCountable besides extending class Shape:

    class Quad extends Shape implements SideCountable {
        // ...
    }
    
    class Triangle extends Shape implements SideCountable {
        // ...
    }
    

    And make class Circle extend Shape but not implement SideCountable:

    class Circle extends Shape { // Not SideCountable
        // ...
    }
    

    When you do it like this, the type system will help you:

    • you can pass a List<Quad> or List<Triangle> to printSidesCount since those types implement SideCountable
    • you cannot pass a List<Circle> to printSidesCount, which is good because trying to do this would not make sense for a list of circles