Reputation: 2210
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
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
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
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
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