When Adhering to Liskov Substitution Principle (Lsp) Can a Child Class Implement Additional Interface

When adhering to Liskov Substitution Principle (LSP) can a child class implement additional interface?

The Liskov Substitution Principle has nothing to do with classes. It is about types. Ruby doesn't have types as a language feature, so it doesn't really make sense to talk about them in terms of language features.

In Ruby (and OO in general), types are basically protocols. Protocols describe which messages an object responds to, and how it responds to them. For example, one well-known protocol in Ruby is the iteration protocol, which consists of a single message each which takes a block, but no positional or keyword arguments and yields elements sequentially to the block. Note that there is no class or mixin corresponding to this protocol. There is no way for an object which conforms to this protocol to declare so.

There is a mixin which depends on this protocol, namely Enumerable. Again, since there is no Ruby construct which corresponds to the notion of "protocol", there is no way for Enumerable to declare this dependency. It is only mentioned in the introductory paragraph of the documentation (bold emphasis mine):

The Enumerable mixin provides collection classes with several traversal and searching methods, and with the ability to sort. The class must provide a method each, which yields successive members of the collection.

That's it.

Protocols and types don't exist in Ruby. They do exist in Ruby documentation, in the Ruby community, in the heads of Ruby programmers, and in implicit assumptions in Ruby code, but they are never manifest in the code.

So, talking about the LSP in terms of Ruby classes makes no sense (because classes aren't types), but talking about the LSP in terms of Ruby types makes little sense either (because there are no types). You can only talk about the LSP in terms of the types in your head (because there aren't any in your code).

Okay, rant over. But that is really, really, really, REALLY important. The LSP is about types. Classes aren't types. There are languages like C++, Java, or C♯, where all classes are also automatically types, but even in those languages it is important to separate the notion of a type (which is a specification of rules and constraints) from the notion of a class (which is a template for the state and behavior of objects), if only because there are other things besides classes which are types in those languages as well (e.g. interfaces in Java and C♯ and primitives in Java). In fact, the interface in Java is a direct port of the protocol from Objective-C, which in turn comes from the Smalltalk community.

Phew. So, unfortunately none of this answers your question :-D

What, exactly, does the LSP mean? The LSP talks about subtyping. More precisely, it defines a (at the time it was invented) new notion of subtyping which is based on behaviorial substitutability. Very simply, the LSP says:

I can replace objects of type T with objects of type S <: T without changing the desirable properties of the program.

For example, "the program does not crash" is a desirable property, so I should not be able to make a program crash by replacing objects of a supertype with objects of a subtype. Or you can also view it from the other direction: if I can violate a desirable property of a program (e.g. make the program crash) by replacing an object of type T with an object of type S, then S is not a subtype of T.

There are a couple of rules we can follow to make sure that we don't violate the LSP:

  • Method parameter types are contravariant, i.e. if you override a method, the overriding method in the subtype must accept parameters of the same types or more general types as the overridden method.
  • Method return types are covariant, i.e. the overriding method in a subtype must return the same type or a more specific type as the overridden method.

These two rules are just the standard subtyping rules for functions, they were known long before Liskov.

  • Methods in subtypes must not raise any new exceptions that are not only raised by the overridden method in the supertype, except for exceptions whose types are themselves subtypes of the exceptions raised by the overridden method.

These three rules are static rules restricting the signature of methods. The key innovation of Liskov were the four behavioral rules, in particular the fourth rule ("History Rule"):

  • Preconditions cannot be strengthened in a subtype, i.e. if you replace an object with a subtype, you cannot impose additional restrictions on the caller, since the caller doesn't know about them.
  • Postconditions cannot be weakened in a subtype, i.e. you cannot relax guarantees that the supertype makes, since the caller may rely on them.
  • Invariants must be preserved, i.e. if the supertype guarantees that something will always be true, then it must also always be true in the subtype.
  • History Rule: Manipulating the object of a subtype must not create a history that is impossible to observe from objects of the supertype. (This one is a bit tricky, it means the following: if I observe an object of type S only through methods of type T, I should not be able to put the object in a state such that the observer sees a state that would not be possible with an object of type T, even if I use methods of S to manipulate it.)

The first three rules were known before Liskov, but they were formulated in a proof-theoretical manner which didn't take aliasing into account. The behavioral formulation of the rules, and the addition of the History Rule make the LSP applicable to modern OO languages.

Here is another way to look at the LSP: if I have an inspector who only knows and cares about T, and I hand him an object of type S, will he be able to spot that it is a "counterfeit" or can I fool him?

Okay, finally to your question: does adding the sneer_majesticly method violate the LSP? And the answer is: No. The only way that adding a new method can violate LSP is if this new method manipulates old state in such a way that is impossible to happen using only old methods. Since sneer_majesticly doesn't manipulate any state, adding it cannot possibly violate LSP. Remember: our inspector only knows about Animal, i.e. he only knows about walk and run. He doesn't know or care about sneer_majesticly.

If, OTOH, you were adding a method bite_off_foot after which the cat can no longer walk, then you violate LSP, because by calling bite_off_foot, the inspector can, by only using the methods he knows about (walk and run) observe a situation that is impossible to observe with an animal: animals can always walk, but our cat suddenly can't!

However! run could theoretically violate LSP. Remember: objects of a subtype cannot change desirable properties of the supertype. Now, the question is: what are the desirable properties of Animal? The problem is that you have not provided any documentation for Animal, so we have no idea what its desirable properties are. The only thing we can look at, is the code, which always raises a NotImplementedError (which BTW will actually raise a NameError, since there is no constant named NotImplementedError in the Ruby core library). So, the question is: is the raiseing of the exception part of the desirable properties or not? Without documentation, we cannot tell.

If Animal were defined like this:

class Animal
# …

# Makes the animal run.
#
# @return [void]
# @raise [NotImplementedError] if the animal can't run
def run
raise NotImplementedError
end
end

Then it would not be an LSP violation.

However, if Animal were defined like this:

class Animal
# …

# Animals can't run.
#
# @return [never]
# @raise [NotImplementedError] because animals never run
def run
raise NotImplementedError
end
end

Then it would be an LSP violation.

In other words: if the specification for run is "always raises an exception", then our inspector can spot a cat by calling run and observing that it doesn't raise an exception. However, if the specification for run is "makes the animal run or else raises an exception", then our inspector can not differentiate a cat from an animal.

You will note that whether or not Cat violates the LSP in this example is actually completely independent of Cat! And it is in fact also completely independent of the code inside Animal! It only depends on the documentation. That is because of what I tried to make clear in the very beginning: the LSP is about types. Ruby doesn't have types, so the types only exist in the programmer's head. Or in this example: in documentation comments.

Does Liskov Substitution Principle also apply to classes implementing interfaces?

Does LSP also apply to interfaces, meaning that we should be able to use a class implementing a specific interface and still get the expected behavior?

LSP applies to the contract. The contract may be a class or an interface.

If that is indeed the case, then why is programming to an interface considered a good thing ( BTW- I know that programming to an interface increases loose coupling ), if one of the main reasons against using inheritance is due to risk of not complying to LSP? Perhaps because:

It's not about an interface or a class. It's about a violation of the contract. Let's say that you have a Break() method in a IVehicle (or VehicleBase). Anyone calling it would expect the vehicle to break. Imagine the surprise if one of the implementations didn't break. That's what LSP is all about.

a) benefits of loose coupling outweight the risks of not complying to LSP

ehh?

b) compared to inheritance, chances that a class ( implementing an interface ) will not adher to LSP are much smaller

ehh?

You might want to read my SOLID article to understand the principle better: http://blog.gauffin.org/2012/05/solid-principles-with-real-world-examples/

Update

To elaborate - with inheritance virtual methods may consume private members, to which subclasses overriding these virtual methods don't have access to.

Yes. That's good. members (fields) should always be protected ( = declared as private). Only the class that defined them really know what their values should be.

Also, derived class inherits the context from parent and as such can be broken by future changes to parent class etc.

That's a violation of Open/closed principle. i.e. the class contract is changed by changing the behavior. Classes should be extended and not modified. Sure, it's not possible all the time, but changes should not make the class behave differently (other than bugfixes).

Thus I feel it's more difficult to make subclass honour the contract than it is to make class implementing an interface honour it

There is a common reason to why extension through inheritance is hard. And that's because the relationship isn't a true is-a relationship, but that the developer just want to take advantage of the base class functionality.

That's wrong. Better to use composition then.

What is an example of the Liskov Substitution Principle?

A great example illustrating LSP (given by Uncle Bob in a podcast I heard recently) was how sometimes something that sounds right in natural language doesn't quite work in code.

In mathematics, a Square is a Rectangle. Indeed it is a specialization of a rectangle. The "is a" makes you want to model this with inheritance. However if in code you made Square derive from Rectangle, then a Square should be usable anywhere you expect a Rectangle. This makes for some strange behavior.

Imagine you had SetWidth and SetHeight methods on your Rectangle base class; this seems perfectly logical. However if your Rectangle reference pointed to a Square, then SetWidth and SetHeight doesn't make sense because setting one would change the other to match it. In this case Square fails the Liskov Substitution Test with Rectangle and the abstraction of having Square inherit from Rectangle is a bad one.

Sample Image

Y'all should check out the other priceless SOLID Principles Explained With Motivational Posters.

How to comply with Liskov's Substitution Principle (LSP) and still benefit from polymorphism?

LSP says that you must be able to use a derived class in the same way you use it's superclass: "objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program". A classic inheritance that breaks that rule is deriving Square class from Rectangle class since the former must have Height = Width, while the latter can have Height != Width.

public class Rectangle
{
public virtual Int32 Height { get; set; }
public virtual Int32 Width { get; set; }
}

public class Square : Rectangle
{
public override Int32 Height
{
get { return base.Height; }
set { SetDimensions(value); }
}

public override Int32 Width
{
get { return base.Width; }
set { SetDimensions(value); }
}

private void SetDimensions(Int32 value)
{
base.Height = value;
base.Width = value;
}
}

In this case, the behavior of Width and Height properties changed and this is a violation of that rule. Let's take the output to see WHY the behavior changed:

private static void Main()
{
Rectangle rectangle = new Square();
rectangle.Height = 2;
rectangle.Width = 3;

Console.WriteLine("{0} x {1}", rectangle.Width, rectangle.Height);
}

// Output: 3 x 2

Liskov substitution principle violation

Strictly speaking, yes. The wiki article summation of Liskov says:

"...in a program...without altering any of the desirable properties of that program"

If you go back to the original paper by Barbara Liskov, it's literally stricter in its wording, 3.3.
Type Hierarchy
:

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

(Empahsis mine)

So if you replace an instance of Cat with another instance that does something different, i.e. returning stripped not grey, then that is a Liskov violation in the original sense, because a program could be easily defined that relies on the color being grey, here:

program(Cat c){
println(c.furColor);
}

The behaviour of that program will change if you pass it a Tiger in place of a Cat.

However, in the normal way LSP is applied, it is not a violation if you did not add extra preconditions or postconditions. This is a more practical, less academic definition as people accept that when replacing an instance of one concrete type with another you do intend to change the behaviour of the program while maintaining desirable properties of that program. So presuming the client code can handle stripped like any other colour, and grey was not required for a "desirable" property of the program then it does not violate.

liskov substitution principle violations

Interesting question.

  1. As far as I understand, it would not be a violation iif leaving the method empty does not change the expected behaviour of that type. Basically, it strengthens the sub-typing requirements by not only respond to "a Square is a Rectangle", but also that its whole interface yields the same behaviour. For instance, what you mentioned about setting a rectangle's width should not affect its height.

  2. Definitely inheritance should not be used only for code re-use. If it actually applies to all possible sub-types, then go ahead, you'll probably be fine. If it only applies to a sub-set of them, you'll find yourself constantly overriding methods and, at the end of the day, writing more code.
    You can instead encapsulate that common code into meaningful components that can then be used within your classes by means of composition.

  3. In the case of Numbers, I think you're parting from the wrong premise. I would not extend Integer class to implement Naturals. Think when subtracting two naturals, where the second is higher than the first one: ie. 3 - 5. In here, you'd need to make a choice, which would be either to throw an Exception or to return something that is no longer a Natural. A different approach would be to extend an abstract Number class, where you could define a set of methods as follows:

    abstract class Number {
    public abstract Number sum(Number other);
    public abstract Number subtract(Number other);
    public abstract Number multiply(Number other);
    public abstract Number divide(Number other);
    }

    This implementation would not be perfect, since it would require some assumptions to be made, like what implicit conversions (casts) to do in case you're operating upon different types. But in this case, it'd allow you to apply the Liskov Substitution principle a bit more freely.

Can I violate LSP (Liskov substitution) in this case?

I think you will violate LSP.

From the Wikipedia page for LSP (that is always your friend ;):

"More formally, the Liskov substitution principle (LSP) is a particular definition of a subtyping relation, called (strong) behavioral subtyping"

"Behavioral subtyping is a stronger notion than typical subtyping of functions defined in type theory, which relies only on the contravariance of argument types and covariance of the return type. Behavioral subtyping is trivially undecidable in general"

Looks similar to your case:

"A typical example that violates LSP is a Square class that derives from a Rectangle class, assuming getter and setter methods exist for both width and height. The Square class always assumes that the width is equal with the height. If a Square object is used in a context where a Rectangle is expected, unexpected behavior may occur because the dimensions of a Square cannot (or rather should not) be modified independently. This problem cannot be easily fixed: if we can modify the setter methods in the Square class so that they preserve the Square invariant (i.e., keep the dimensions equal), then these methods will weaken (violate) the postconditions for the Rectangle setters, which state that dimensions can be modified independently. Violations of LSP, like this one, may or may not be a problem in practice"

Liskov substitution principle and Streams

The Can... methods mean that Stream doesn't break LSP. Stream provides the capability to read, write and seek, but no guarantee that any implementing class will honour it. The Can... methods make this an explicit feature of the Stream contract - derived classes must implement them to allow client code to check whether a derived class implements certain behaviour before a call is made. So, any code that attempts to write to a Stream should check CanWrite before calling Write, for example, and this can be done with any correctly-implemented derivative of Stream. Ergo, they're interchangeable, as LSP requires.

I think it's certainly true that adding methods to flag whether or not derived classes implement a particular feature could be abused - if a team were undisciplined, they may end up with a very broad, bloated interface that breaks ISP. I think that Stream and IList<T> are well-designed in this respect - they don't break LSP, and they define a narrow enough contract of closely-related behaviours to stay within ISP. Clearly, their design has been considered.

I think that in the case of Square inheriting from Rectangle, you could certainly add DoesHeightAffectsWidth and DoesWidthAffectsHeight to fix the issue, but the team must decide whether that's acceptable, or whether the addition of those methods breaks ISP. Is the addition of AreAllInternalAnglesEqual to support trapezoids too far? To a certain extent, it's up to the engineers writing the code.



Related Topics



Leave a reply



Submit