Does overloading (not overriding) break the Liskov subsitution principle?

Viewed 342

There are tons of discussions out there about the LSP, but all of them seem to be excessively blurred.
AFAIK, LSP states that to properly override (not overload) a superclass method in a child, one should assure that the child method:

  1. does not yield a new type of exception that in no scenario is raised by the parent method
  2. has the same signature as the parent method (in case of strongly typed languages)
  3. has the same semantic MEANING of the signature
  4. returns value of the same type/types
  5. returns value of the same semantic meaning

By semantic meaning I mean that if base class method implies that it returns int and this int means, say, USD or EUR, then the overloaded method should also imply that the returned value is either USD or EUR, and returning 'RUB' even in one scenario would violate LSP.

But what if my base class looks like this (example is in Python):

class A:

    func(x: int) -> int
        return x*2

class B(A):

    func(x: int, y: string) -> int
        return x*y

So there are two sub-questions in my question:

  1. What does the term contract mean in more practical terms ? Is it synonimous to interface ?
  2. Does overloading with a signature (in part of list of arguments) that is NOT present in the base class violate LSP?
3 Answers

The answer depends on the language.

In typical stronly-typed OO languages like Java, C++, C#, etc., when you write a method call like a.func(b,c), the actual method to call is determined based on the method name, as well as the receiving type (type of a in this example), and the number and types of arguments.

In a language like that, methods with different numbers of arguments, or different types of arguments, are entirely different methods. Having a different number of arguments is exactly like having a different name. When you "overload" a method, it's just like making a method with a different name, so you're not going to violate LSP by overloading a method from a base class.

Your question appears to be about python, though, and in typical dynamically typed languages like python, JavaScript, etc., when you write a method call like a.func(b,c), the method to call is looked up by name only (looked up in a table associated with the receiving object). In languages like that, there is no such thing as overloading for methods or functions.

In your example, you have overridden the single-argument definition of func with a two-argument function. That means that consumers of your derived class can no longer call func with one parameter, and that is indeed an LSP violation.

In real life a contract is what party A and party B can expect from each other, the guarantees that they provide. Same in programming:

  • What range of arguments does this method accept?
  • What behavior can you expect from it?
  • When does in throw errors?

It's a combination of Interface and Documentation to the method.

When we say that a child should follow the same contract what we mean is that if we replace implementation with another ChildC, then same constraints are followed as described in the parent, same behavior.

E.g. in Java we have a notion of Map (similar to Dict in Python) and there're 2 different implementations: HashMap and LinkedHashMap. One doesn't preserve order of addition, the other one - does. But in the contract (in the interface and its docs) no one said that the order should be preserved. So if our code used HashMap and then we replaced it with LinkedHashMap - it still does everything what was required by the parent's docs and signatures.

There's yet another implementation: ConcurrentHashMap. Now that one violates LSP. Because HashMap, LinkedHashMap could have null keys while ConcurrentHashMap can't. So we can't just replace one implementation with another - it may break things if client actually put null's there.

The issue is mitigated by documentation to Map saying that some implementations may not accept null's, but it's more of a hack.

As for overloading - you're creating a separate method with the same name, it doesn't have to follow the contract of the 1st one. These methods may have nothing in common at all. Though as noted in other answers - there's no overloading of methods in Python, you're simply replacing the old method with the new one, so it's a kind of overriding when done in a sub-class.

PS: if you working with a Strategy or a Command - the implementation would quite different things. So LSP doesn't apply here. But they still need to follow the generic contract described in the parent.

To understand the implications of the LSP, the slogan "demand no more, promise no less" helps a lot. The overriding method cannot demand more arguments, or arguments of more specific types (i.e., they must be contravariant). It must promise to return a value of the same, or more specific, type as the overridden method (i.e., it must be covariant).

The semantic constraint is that the overriding method must do the "same thing" in the subclass as it did in the superclass. This notion of doing the same thing cannot be formalized easily and depends the meaning of the classes themselves. However if the overriding method calls the overridden method, it's a strong sign of semantic consistency.

Overloading is irrelevant to LSP. It's just a compile-time trick to allow different methods have the same name.

Related