Bas
Bas

Reputation: 2210

Violation of the LSP-principle using extra constructor parameters in subtypes

I've been reading about the Liskov Substitution Principle when I noticed this answer. It has a Circle and a ColoredCircle type where the constructor of ColoredCircle takes one extra argument; the color.

class Circle:
    radius: int

    def __init__(self, radius: int) -> None:
        self.radius = radius

class ColoredCircle(Circle):
    radius: int
    color: str

    def __init__(self, radius: int, color: str) -> None:
        super().__init__(radius)
        self.color = color

Doesn't this violate one of the requirements below? (taken from this answer). The only other option in the case of the ColoredCircle would be a public variable or a set_color method.

Pre-conditions cannot be strengthened: Assume your base class works with a member int. Now your sub-type requires that int to be positive. This is strengthened pre-conditions, and now any code that worked perfectly fine before with negative ints is broken.

If I'm searching in the wrong direction here, please let me know. Also, what if a subtype has many more parameters to handle, how would one normally manage those, is a new abstraction always nessacery?

Upvotes: 3

Views: 680

Answers (4)

S.B
S.B

Reputation: 16536

After seeing your discussions I got the point where the confusion started.

LSP talks about and focuses on replacing the instances, not the "classes" themselves!


Having the code:

class Circle:
    def __init__(self, radius: int) -> None:
        self.radius = radius


class ColoredCircle(Circle):
    def __init__(self, radius: int, color: str) -> None:
        super().__init__(radius)
        self.color = color


circle_instance = Circle(10)
colored_circle_instance = ColoredCircle(20, "blue")


def print_radius(obj: Circle) -> None:
    print(f"The radius is: {obj.radius}")

Wherever you need an instance of the type Circle(which is circle_instance), you can pass an instance of the type ColoredCircle(which is colored_circle_instance). LSP doesn't get violated here. You can do anything possible on circle_instance, on colored_circle_instance as well. If you have the instances, it means the initializers must have called before.

When do the things go wrong? If you have incompatible methods which can be called on the instances:

class Circle:
    def __init__(self, radius: int) -> None:
        self.radius = radius

    def method(self, arg) -> None:
        print(f"calling method with arg: {arg}")


class ColoredCircle(Circle):
    def __init__(self, radius: int, color: str) -> None:
        super().__init__(radius)
        self.color = color

    def method(self, arg, arg2) -> None:
        print(f"calling method with arg: {arg, arg2}")

Even Pylance would tell you that:

Method "method" overrides class "Circle" in an incompatible manner
  Positional parameter count mismatch; base method has 2, but override has 3PylancereportIncompatibleMethodOverride

One could argue that classes are instances themselves. Yes but that's another story. There must be a hierarchy of metaclasses if we want to talk about LSP being violated or not when the actual "classes"(Circle and ColoredCircle) are being passed(for instantiation for example).

Upvotes: 0

Scott Hannen
Scott Hannen

Reputation: 29252

The intent of the Liskov Substitution Principle is that types and their subtypes should be substitutable, which in turn allows decoupling. Consumers shouldn't have to know the implementation of an object, only its declared type. If a class creates its own dependency by calling its constructor, it is coupled to that specific type. In that case the LSP becomes irrelevant. There's no way to substitute another type, so it doesn't matter if the types are substitutable or not.

Put another way - a class that creates an instance of another class generally cannot benefit from the LSP, because it mostly rules out the possibility of substituting one type for another. If it passes that object to other methods which interact with it in ways other than creating it, that's where we benefit from the LSP.

Based on that reasoning, I'd say that varying constructors don't violate the intent of the Liskov Substitution Principle.

In many languages we use dependency injection to decouple classes from the construction of dependencies. That means consumers deal with every aspect of a type except for its constructor. The constructor is out of the equation and subtypes are (or should be) substitutable for the types from which they inherit.

Upvotes: 2

Matt Timmermans
Matt Timmermans

Reputation: 59283

When a class X has a constructor, the constructor is not a method on objects of type X. Since it is not a method on objects of type X, it doesn't have to exist as a method on objects of derived types, either -- it's irrelevant to LSP.

Upvotes: 2

jaco0646
jaco0646

Reputation: 17104

Doesn't this violate one of the requirements below?

It would depend on the language; but at least in Java, constructors are not inherited, so their signatures are not subject to the LSP, which governs inheritance.

Subtypes work best for modifying behavior (of a supertype). This is known as polymorphism, and subtypes do it well (when the LSP is followed). Subtypes work poorly for code reuse, such as sharing variables. This is the idea behind the famous principle, prefer composition over inheritance.

Upvotes: 1

Related Questions