“To build software systems from interchangeable parts, those parts must adhere to a contract that allows those parts to be substituted one for another.”
This is another way of saying that subtypes must be substitutable for their base types.
Suppose we have code designed to send email, and we have a choice of email providers. When writing the code, we should be able to substitute any of the email providers and expect the code to work nonetheless - this is accomplished using OOP interfaces.
Suppose, for example, that we are creating a function that calculates the area of an object:
def get_total_area(shapes: List[Shape]):
return sum(shape.calculate_area() for shape in shapes)The LSP requires that we should be able to pass any Shape (or any subclass of Shape) into the function and expect the result to be correct. This requires careful design based on how the baseclass is defined:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self):
return self.width * self.heightIf we define a Rectangle as the above, and suggest that a Square is a special case of rectangle, we could then subclass as follows, using clever methods to ensure that both width and height are set to the same value:
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
def __setattr__(self, key, value):
super().__setattr__(key, value)
if key in ("width", "height"):
self.__dict__["width"] = value
self.__dict__["height"] = valueThis changes the promise made by the interface itself, which is the Rectangle, since height and width behave differently. Thus the issue is not that a Square is a specific type of Rectangle, but that both of them are instances of a Shape and can be substituted in code using OOP Polymorphism
# shapes_lsp.py
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def calculate_area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side):
self.side = side
def calculate_area(self):
return self.side ** 2