2

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?

Bas
  • 2,106
  • 5
  • 21
  • 59

3 Answers3

2

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.

Matt Timmermans
  • 53,709
  • 3
  • 46
  • 87
  • I was tempted to make a broader statement like this, about constructors in general; but I wasn't confident that some esoteric language doesn't support overriding constructors. – jaco0646 Mar 16 '22 at 02:51
  • What would that even mean? How would you call an overridden constructor? It would have to be a completely different kind of thing... and then yes, anything I say about constructors could become incorrect if that word is redefined to mean something entirely different :) – Matt Timmermans Mar 16 '22 at 03:22
  • Tell me if I'm taking to literal, but when I replace an object of type `Color` with a type of `ColoredProvider` - or vica versa - the `color` variable is missing, which breaks the program? – Bas Mar 16 '22 at 06:54
  • @Bas, most languages don't actually have a way to make a subtype of `Color` that is missing one of its variables, but if you could, and the program broke as you say, then the `color` variable must have been exposed -- it was part of `Color`'s interface and needs to be preserved in all subtypes. – Matt Timmermans Mar 16 '22 at 12:01
  • @MattTimmermans I added my example in Python, without errors. Does that violate the principle then, and would the only option to be to set the `color` param in the `Color` type too? – Bas Mar 16 '22 at 12:19
  • 1
    @Bas No, that doesn't violate LSP. A `ColoredCircle` can do everything a `Circle` can. – Matt Timmermans Mar 16 '22 at 12:39
  • But as you said, it's missing one variable right? If you'd have a `Circle` obj, and you'd replace it with a `ColoredCircle`? – Bas Mar 16 '22 at 12:41
  • 1
    You can provide a `ColoredCircle` to anything that needs a `Circle`. There is no requirement that you can provide a `Circle` to something that needs a `ColoredCircle`. The subtype relationship goes one-way. – Matt Timmermans Mar 16 '22 at 12:43
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/242985/discussion-between-bas-and-matt-timmermans). – Bas Mar 16 '22 at 12:56
2

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.

Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
1

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.

jaco0646
  • 15,303
  • 7
  • 59
  • 83
  • How would this be solved on a language where the constructor is inherited? TypeScript for example. – Bas Mar 15 '22 at 20:21
  • 1
    A quick search of TypeScript indicates that constructors are only "inherited" when a subclass provides none. This is only syntax sugar and not a polymorphic relationship such as the LSP is concerned with. You cannot invoke the constructor of a superclass and have it overridden by a subclass. – jaco0646 Mar 15 '22 at 21:29
  • I don't think I understand the core of it. As my comment on the answer from Matt, it breaks the program on a different constructor doesn't it? – Bas Mar 16 '22 at 06:55