Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why Liskov Substitution Principle needs the argument to be contravariant?

One of the rules that Liskov Substitution Principle imposes on method signature in derived class is:

Contravariance of method arguments in the subtype.

If I understood correctly, it is saying that the derived class's overridding function should allow contravariant arguments(Supertype arguments). But, I could'nt understand the reason behind this rule. Since LSP talks mostly about dynamically binding the types with there subtypes(rather than supertypes) in order to achieve abstraction, so allowing supertypes as the method arguments in derived class is quite confusing to me. My questions are :

  • Why LSP allows/requires Contravariant arguments in derived class's overridding funtion?
  • How Contravariance rule is helpful in achieving data/procedure abstraction?
  • Is there any real world example where we need to pass contravariant parameter to the derived class's overridden method?
like image 629
Mac Avatar asked Oct 31 '16 10:10

Mac


People also ask

Why do we need Liskov Substitution Principle?

The Liskov Substitution Principle helps us model good inheritance hierarchies. It helps us prevent model hierarchies that don't conform to the Open/Closed principle. Any inheritance model that adheres to the Liskov Substitution Principle will implicitly follow the Open/Closed principle.

What violates Liskov Substitution Principle?

A very common violation of this principle is the partial implementation of interfaces or base class functionality, leaving unimplemented methods or properties to throw an exception (e.g. NotImplementedException).

Which OOP principle is absolutely important for the Liskov Substitution Principle?

The Liskov Substitution Principle (LSP, lsp) is a concept in Object Oriented Programming that states: Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

What is the most accurate example of the Liskov Substitution Principle What is the most accurate example of the Liskov Substitution Principle?

A good example here is that of a bird and a penguin; I will call this dove-penguin problem. The below is a Java code snippet showing an example that violates the LSP principle. Here, the Dove can fly because it is a Bird. In this inheritance, much as technically a penguin is a bird, penguins do not fly.


3 Answers

Here, following what LSP says, a "derived object" should be usable as a replacement of the "base object".

Let's say your base object has a method:

class BasicAdder
{
    Anything Add(Number x, Number y);
}

// example of usage
adder = new BasicAdder

// elsewhere
Anything res = adder.Add( integer1, float2 );

Here, "Number" is an idea of base type for a number-like data types, integers, floats, doubles, etc. No such thing exists in i.e. C++, but then, we're not discussing a specific language here. Similarly, just for the purpose of example, "Anything" depicts an unrestricted value of any type.

Let's consider a derived object that is "specialized" to use Complex:

class ComplexAdder
{
    Complex Add(Complex x, Complex y);
}

// example of usage
adder = new ComplexAdder

// elsewhere
Anything res = adder.Add( integer1, float2 ); // FAIL

hence, we just broke LSP: it is NOT usable as a replacement for original object, because it is not able to accept integer1, float2 parameters, because it actually requires complex parameters.

On the other hand, please note that covariant return type is OK: Complex as return type will fit Anything.

Now, let's consider the other case:

class SupersetComplexAdder
{
    Anything Add(ComplexOrNumberOrShoes x, ComplexOrNumberOrShoes y);
}

// example of usage
adder = new SupersetComplexAdder

// elsewhere
Anything res = adder.Add( integer1, float2 ); // WIN

now everything is OK, because whoever was using the old object, now is also able to use the new object as well, with no change impact on the point of use.

Of course, it is not always possible to create such "union" or "superset" type, especially in terms of numbers, or in terms of some automatic type conversions. But then, we are not talking about specific programming language. The overall idea matters.

It's also worth noting that you can adhere or break LSP at various "levels"

class SmartAdder
{
    Anything Add(Anything x, Anything y)
    {
        if(x is not really Complex) throw error;
        if(y is not really Complex) throw error;

        return complex-add(x,y)
    }
}

It surely looks like conforming to LSP at the class/method signature level. But is it? Often not, but that depends on many things.

How Contravariance rule is helpful in achieving data/procedure abstraction?

it is well.. obvious for me. If you create say, components, that are meant to be exchangeable/swappable/replaceable:

  • BASE: compute sum of invoices naively
  • DER-1: compute sum of invoices on multiple cores in parallel
  • DER-2: compute sum of invoices with detailed logging

and then add a new one:

  • compute sum of invoices in different currency

and lets say it handles EUR and GBP input values. What about inputs in old currency, say USD? If you omit that, then new component is not a replacement of old ones. You cannot just take out the old component and plug the new one in and hope everything is fine. All other things in the system may still send a USD values as inputs.

If we create the new component as derived from BASE, then everyone should be safe to assume that they can use it wherever a BASE was required earlier. If some place required a BASE, but a DER-2 was used, then we should be able to plug the new compoenent there. This is LSP. If we cannot, then something is broken:

  • either place of use did't require just BASE but in fact required more
  • or our component indeed is not a BASE (please note the is-a wording)

Now, if nothing is broken, we can take one and replace with another, regardless of whether USDs or GBPs or single core or multicore is out there. Now, looking at the big picture at one-level-above, if no longer need to care about specific types of currency, then we successfully abstracted it away the big picture will be simpler, while of course, components will need to internally handle that somehow.

If that does not feel like helping in data/procedure abstraction then look at opposite case:

If component derived from BASE didn't adhere to LSP, then it may raise errors when values legitimate in USDs arrive. Or worse, it will not notice and will process them as GBP. We have a problem. To fix that we need to either fix the new component (to adhere to all requirements from BASE), or change other neighbour components to follow new rules like "now use EUR not USD, or the Adder will throw exceptions", or we need to add things to the big picture to work it around i.e. add some branches that will detect old-style data and redirect them to old components. We just "leaked" the complexity to neighbours (and maybe we forced them to break SRP) or we made the "big picture" more complex (more adapters, conditions, branches, ..).

like image 180
quetzalcoatl Avatar answered Oct 24 '22 06:10

quetzalcoatl


I know it is a pretty old question, but I think a more real life use could help:

 class BasicTester
    {
       TestDrive(Car f)

    }

    class ExpensiveTester:BasicTester
    {
       TestDrive(Vehicle v)
    }

The old class can only work with Car type, while the derived one is better and can handle any Vehicle. In addition, those who use the new class with the "old" Car type will be served too.

However, you cannot override like that in C#. You could implement that indirectly using delegates:

protected delegate void TestDrive(Car c)

which can be then assigned a method that accepts Vehicle. THanks to contravariance it will work.

like image 26
John V Avatar answered Oct 24 '22 06:10

John V


The phrase "contravariance of method arguments" may be concise, but it's ambiguous. Let's use this as an example:

class Base {
  abstract void add(Banana b);
}

class Derived {
  abstract void add(Xxx? x);
}

Now, "contravariance of method argument" could mean that Derived.add must accept any object that has the type Banana or a supertype, something like ? super Banana. This is an incorrect interpretation of the LSP rule.

The actual interpretation is: "Derived.add must be declared either with the type Banana, just as in Base, or some supertype of Banana such as Fruit." Which supertype you choose is up to you.

I believe that using this interpretation it is not hard to see that the rule makes perfect sense. Your subclass is compatible with the parent API, but it also, optionally, covers extra cases which the base class doesn't. Therefore it's LSP-substitutable for the base class.

In practice there aren't many examples where this widening of type in the subclass is useful. I assume this is why most languages don't bother to implement it. Requiring strictly the same type preserves LSP as well, just doesn't give you the full flexibility you could have while still achieving LSP.

like image 21
Marko Topolnik Avatar answered Oct 24 '22 07:10

Marko Topolnik