Liskov Substitution Principle vs Interface Segregation Principle – SOLID Conflict?

interfacesliskov-substitutionobject-orientedsolid

I am confused by the two principles of SOLID, liskovs substitution principle and interface segregation principle. It seem as though they conflict each other's definitions.

How can a class that implements interfaces also guarenttee that it also fits the liksov subsitution?

For example, in this code, if a client to make a new shape class they must still implement IDraw and IMove. Therefore, doesn't that make the concept of ISP nullified since it states that:

"A client should never be forced to implement an interface that it doesn't use or clients shouldn't be forced to depend on methods they do not use."

    // In this example all clients **must** implement IDraw() and IMove()

    public interface IDraw
    {
        void Draw();
    }
    
    public interface IMove
    {
        void Move();
    }
    
    public abstract class Shape : IDraw, IMove
    {
        public abstract void Draw();
        public abstract void Move();
    }
    
    public class Square : Shape
    {
        public override void Draw()
        {
        }
        public override void Move()
        {
        }
    }
    
    public class Rectangle : Shape
    {
        public override void Draw()
        {
        }
        public override void Move()
        {
        }
    }
    

Alternatively, if I put interfaces "halfway" in the class heirachy, LSP is nullified but now ISP is preserved, for example:

// In this example the classes Rectangle and Square are no longer interchangeable, so LSP is broken. 

using System;

public interface IDraw
{
    void Draw();
}

public interface IMove
{
    void Move();
}

public abstract class Shape
{
}

public class Square : Shape, IDraw
{
    public void Draw()
    {
    }
}

public class Rectangle : Shape, IMove
{
    public void Move()
    {
    }
}

Best Answer

Alternatively, if I put interfaces "halfway" in the class hierarchy, LSP is nullified

classes Rectangle and Square are no longer interchangeable

Yes and no. Some things are mixed up here. And some are omitted.

mixed up stuff

LSP according to wikipedia

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

LSP is not concerned about two sibling types Rectangle and Square being interchangeable with each other. It's concerned about interchangeability of a supertype and one of its subtype.

LSP in code is basically this:

Shape shape = new Rectangle(); // should be OK to treat a rectangle like a shape
Shape shape = new Square(); // should be OK to treat a square like a shape

In a sense, you could say that Rectangle and Square are interchangeable here, both being possible substitutions for Shape, but this is merely a result of LSP relationships of Rectangle and Square to their superclass Shape respectively.

Every type has an individual LSP relationship to each of its supertypes. So given Square : Shape, IDraw and Rectangle : Shape, IMove the above is still valid:

Shape shape = new Rectangle(); // still OK to treat a rectangle like a shape
Shape shape = new Square(); // still OK to treat a square like a shape

What you are likely referring to as a sign of non-interchangeability of Rectangle and Square is that you cannot do this:

IDraw draw = new Rectangle(); // nope
IMove move = new Square(); // nope

But there's no supertype-subtype relationship between IDraw and Rectangle / IMove and Square respectively, which means LSP isn't nullified here, it simply doesn't apply. Expecting interchangeability here is "begging the question". LSP still applies to each supertype-subtype relationship individually:

IDraw draw = new Square(); // ok
IMove move = new Rectangle(); // ok

Just because Rectangle and Square have one common supertype Shape, which according to LSP they are each individually interchangeable with, does not (necessarily) mean they are interchangeable with each other.

This sort of LSP interchangeability explained above is fulfilled by the type-system already, because every subtype is also all its supertypes. There's more to this than just types.

comment

But given that Rectangle uses IDraw and Square uses IMove, how do you abide by LSP when replacing it with the base class Shape, since shape doesn't use IDraw or IMove?

The LSP relationship has a "direction". You can use a subtype where a supertype is expected, but not the other way round.

If you have a Rectangle object in place somewhere in your code and you use Draw of IDraw, then you are correct that you could not substitute that with Shape object, "since shape doesn't use IDraw". This expectation however is unreasonable or simply wrong in terms of LSP. LSP is not suggesting that you can do this.

Again, you are begging the question by asking "how do I abide by LSP if I do something that doesn't".

As a rule of thumb: You cannot break LSP with just the type system, because the hierarchical type system is equivalent to LSP.

omitted stuff

The actually important thing about LSP is not types, but behaviour. Your example is entirely free from any functionality and concentrates on compatibility of types. All your methods are empty.

There's always an "implicit" part to a type definition. Sometimes this is referred to as an "implicit contract". This includes things like:

  • Under which conditions will this method throw an exception?
  • What properties/variables/fields (more general: what members) of the class are expected to be updated after calling a method?

Here's a modified example of your code:

public interface IDraw
{
    void Draw(); // draw object into the buffer
    DrawingBuffer GetBuffer();
}

This new version of IDraw demands that you update the drawing buffer to be retrieved later.

disclaimer: Whether this sort of interface design is a good idea or not is questionable. It might be perfectly fine or it might be better to have only one method: DrawingBuffer Draw(); For the sake of this explanation, let's assume it is the way to go.

Now - strictly speaking - the code as is breaks LSP, because it is not updating the buffer:

public class Square : Shape
{
    public override void Draw()
    {
         // not updating the buffer here
    }
    public override void Move()
    {
    }
}

And it's the same with the other one:

public class Square : Shape, IDraw
{
    public void Draw()
    {
        // not updating the buffer here
    }
}

Of course, if actually updating the buffer is optional, this is might be ok to opt-out for implementation of special cases, like if the shape hasn't changed.

But when it comes to Exceptions, you might accidentally opt-in, where you shouldn't:

public interface IMove
{
    void Move(); // don't throw exception here
}


public class Rectangle : Shape, IMove
{
    public void Move()
    {
         _x = screenSize / _somePrivateVariableThatMightBeZero;
    }
}

Depending on your programming language, types of _x, screenSize and _somePrivateVariableThatMightBeZero and the value of the latter, the above code might throw an exception due to a division by 0;

This breaks the contract of IMove and thus LSP. A user of IMove would expect to be able to call Move() without having to deal with (likely implementation specific) exceptions being thrown.