8

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()
    {
    }
}
7
  • 4
    I think you misunderstand - a 'client' is the code which uses the interface (interacts with objects) from the outside (e.g. calls its methods), and not the class(es) which implement the interface. i.e. Shape/Square/Rectangle are not clients of IDraw or IMove because they aren't the ones actually interacting with objects via the interface. – Ben Cottrell Jul 24 '20 at 8:50
  • @BenCottrell But the quote says "A client should never be forced to implement an interface...", does implement here mean inherit and implement? edit - oh i notice now that the statement differs depending on the source! wikipedia says "no client should be forced to depend on methods it does not use" whereas the one from above says OR implements.etc – Richard Bamford Jul 24 '20 at 8:57
  • @BenCottrell I think i'm seeing where you come from: from drive.google.com/file/d/… it says "CLIENTS SHOULD NOT BE FORCED TO DEPEND UPON INTERFACES THAT THEY DO NOT USE." and if clients are users of its interface then that makes sense, since inherited classes aren't clients. – Richard Bamford Jul 24 '20 at 9:03
  • @BenCottrell: I agree with your definition of "client" but the referenced quote doesn't use that definition. It specifically talks about clients implementing an interface, not "clients handling an object of an interface's type". I think the confusion can be avoided if the quote was rephrased to use "consumer". – Flater Jul 24 '20 at 9:24
  • 6
    I'm not sure why this question is being downvoted. While OP may have a lacking understanding of the principles in focus (hence the question), the question is clear, understandable and well-formed. – Flater Jul 24 '20 at 9:34
8

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.

4
  • 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? – Richard Bamford Jul 24 '20 at 9:35
  • @RichardBamford that pain is entirely caused by the fact that your model doesn't make any sense - why can only rectangles be drawn? Why can only squares be moved? The LSP only matters at the level of the interface being consumed - anything that claims it implements IDraw should be interchangeable from the perspective of a consumer of that interface. – jonrsharpe Jul 24 '20 at 9:38
  • @RichardBamford I edited my answer. In short: you don't abide LSP when replacing Rectangle with Shape. But this is not possible either (the type system does not allow it), which is because the type system is based on LSP. – null Jul 24 '20 at 10:29
  • Ooh! i think this is where my error in logic is, i thought that LSP was bidirectional! Thanks :) – Richard Bamford Jul 24 '20 at 10:53
5

Do LSP and ISP contradict one another?

No. These principles actually work in tandem, or at least part of their problem domain overlaps and tackles a similar issue.

You could generalize them into the "don't claim and/or pretend to be something you're not" principle, as that sort of gets at the core issue that either of them focuses on.

But to be fair, it may be easier to observe them separately. I'm going to call it the Principle Separation Principle or PSP :-)


Which example is correct?

Your example is a bit nonsensical. The code is legible, but which version of the code is correct is a business decision. Depending on those contextual decisions, the correctness of the code is decided

Should every Shape be both drawable and movable?

If yes, then the first example is correct. The second example is demonstrably incorrect then, as you wouldn't be able to draw or move a Shape object.

If no, then the second example is correct, assuming that shapes should only be drawable (not movable) and rectangles should only be movable (not drawable).

In the "no" case, the first example would then be a violation of LSP, as you're ending up with classes who (indirectly) implement an interface they have no intention of actually complying with (rectangles don't want to be drawn, squares don't want to be moved)

More importantly, you cannot judge LSP/ISP here without knowing the business requirements.


How do you violate or comply with LSP/ISP?

Your current interpretation of what LSP and ISP aim to solve are not correct. They're in the right direction, but misapplied.

LSP

LSP effectively states that when a type inherits/implements a base type/interface, it must therefore behave exactly like that base type/interface claims to behave.

In your examples, the clearest example is that when you state that Shape : IMove, then every derived Shape must comply with that IMove contract. If it doesn't, it violates LSP.

Maybe a clearer way of looking at it is that LSP would be violated if you implemented your IDraw/IMove interfaces in such a way that some derived Shape classes would have to implement duds (i.e. they choose to not move, or not be drawn). Whether that dud is an empty method body, throws an exception, or does something completely unrelated; is irrelevant here - it's an LSP violation in all three cases.

Note also that LSP predominantly explains itself using inheritance between classes, as opposed to interface implementation. However, I don't see a reason to make this distinction when considering good practice, as the problem domain is effectively the same whether your base type is a class or an interface.

ISP

ISP effectively states that independent "features" should be separated because they are independent, so they don't have to carry each other as baggage.

If in your codebase your want to have objects that are movable-but-not-drawable, or drawable-but-not-movable, then you should have separate IDraw and IMove interfaces so they can be independently apply to one another.
This also applies to "obvious" cases, where you are still advised to separate two obviously independent behaviors even if you currently happen to always apply both (or neither) of them to your objects. The question here is whether they logically always belong together or not. In the latter case, interface segregation is warranted.

Your examples don't actually include any ISP violations, as you always deal with separate IMove and IDraw interfaces.

If you were to merge these into a IDrawAndMove interface, and some of the classes implementing that interface would be trying to do one and not the other (i.e. movable-but-not-drawable, or drawable-but-not-movable), then that would be an ISP violation as you should instead separate your interface into IDraw and IMove and independently apply them to the classes that actually want to comply with them.

15
  • thanks! i have one question though, if i use seperate IDraw and IMove, doesn't that nullify the LSP since one can't replace Rectangle and Square with Shape and then back to either? – Richard Bamford Jul 24 '20 at 9:45
  • "you cannot judge LSP/ISP here without knowing the business requirements" – This. Especially for the LSP, the (somewhat simplified) definition is "For all properties p provable about a program P, an instance s of a type S <: T should be substitutable for an instance of T without removing any property from the set of provable properties." Or something like that. A more colloquial formulation would be "substituting an instance of a subtype for an instance of a super type doesn't change the desirable properties of the program". Which should make it immediately obvious, that before we – Jörg W Mittag Jul 24 '20 at 9:48
  • … even begin to talk about the LSP, we first need to define what the "desirable properties" even are. – Jörg W Mittag Jul 24 '20 at 9:48
  • @RichardBamford: You can, in fact, interchangeably use a Square or Rectangle whenever there is a Shape variable/method parameter. But if you don't define a Shape itself as IMove, then your logic could never expect to be able to move that Shape, regardless of whether the derived Shape class (e.g. Rectangle) implements it or not. If your method requires anything that can be moved, use the IMove type. If it requires anything that's a shape, use the Shape type. Ask what you need - and don't expect to get anything else as you won't be guaranteed to get it. – Flater Jul 24 '20 at 11:21
  • 1
    @JörgWMittag & Flater: To me it looks like the common theme here is that we should let go of the idea that you should try and get the design completely right from the start. Instead (1) exercise some restraint to leave your options open (e.g., don't generalize on too few examples), and (2) observe what's going on as the actual code changes, so that you can react (restructure/refactor) in a timely manner, to approach the right design as you go along. One side of the coin is baking in the wrong assumptions too soon, the other is that we tend to miss this window where it pays to course-correct. – Filip Milovanović Jul 28 '20 at 17:28
1

Well yes, your example violates these principles, since it doesn't really do anything. If methods are never used at all, they should be removed. That's not the point of the SOLID principles though. The point is that in a real world example, either Shape or one of its subclasses would actually need to be drawable and thus would need an implementation of draw() somewhere. The question then is: Where is this requirement located in the class hierarchy?

If all subclasses of Shape are supposed to be drawable, then Shape should implement the IDraw interface (which should be renamed to IDrawable), even if it makes the draw() method abstract, because it lacks specific knowledge on how to draw. But it would probably use the draw() method somewhere else and rely on its concrete subclasses to provide the specific implementation of it. This is way it becomes a (compiler-enforced) part of the contract of the Shape class that Shapes are always drawable.

If not all Shapes are supposed to be movable, then it should not implement the IMove interface (which should be renamed to IMoveable). Instead an intermediate class should implement it, say MoveableShape, which should probably only implement IMoveable if it actually uses the move() method somewhere else in its code. Now it is part of the contract that MoveableShapes are moveable (in addition to being drawable).

The ISP advises you that you should separate interfaces that do separate things, like moving and drawing. This is exactly because if they are separate things, the requirement for where in the class hierarchy they should apply will likely be different and therefore you need different interfaces so classes can be defined with the most appropriate set of interfaces for them. They will only implement what they actually need for their function. And this is even true if the separate things are still related in one direction, say if the move() method is supposed to call the draw() method to update the drawn image on the screen after the internal position state has changed. Then the draw()-part is still of independent value without the move-method and should be segregated into a separate interface.

The Substitution principle plays very well with this scenario: If any Shape-instance should be replaceable by any Square-instance, then obviously Square-instances need to be drawable too. Happily, this is already guaranteed by the rules of inheritance of the programming language (I hope), because Square will inherit the draw() method from Shape. Assuming that Square is a concrete class, then it is forced to provide an implementation for this method to fulfil its contract.

If Squares are not necessarily movable, but Circles are, then Circle should inherit from MoveableShape instead. Again, the substitution principle is satisfied: Every time you see an object with declared type Shape, you can rely on it being drawable, so it could be a Square or a Circle. But you can't and shouldn't rely on it being moveable. But if you see a MovableShape somewhere, you can rest assured that you can call the move() method on it and it will have an implementation for it.

2
  • Yes i think i understand your solution and i think i will use this technique of an intermediate class but then isn't LSP broken between Shape and MoveableShape? – Richard Bamford Jul 24 '20 at 9:54
  • No. If the contract of Shape says it must be drawable and the contract of MoveableShape says it must be drawable and moveable, then you can use a MovableShape where is Shape is expected, since MovableShape satisfies all the conditions of Shape. Note that not implementing a certain interface like IMoveable doesn't mean that Shape is forbidden to be movable, it only means that you cannot rely on it being movable. – Johannes Hahn Jul 24 '20 at 10:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.