LSP - принцип подстановки Лисков

Описание
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.

Классы-наследники не должны противоречить базовому классу. Например, они не могут предоставлять интерфейс ýже базового.

Поведение наследников должно быть ожидаемым для функций, которые используют базовый класс.
Пример
Опишем пример наследования в геометрических фигурах: прямоугольник и квадрат. Квадрат является частным случаем прямоугольника, поэтому Вам захочется выстроить наследования класса квадрат от прямоугольника.

Рассмотрим, что получится:
Класс прямоугольник

from dataclasses import dataclass


@dataclass
class Rectangle:
    _width: int
    _height: int

    @property
    def width(self):
        return self._width

    @width.setter
    def widht(self, value):
        if value <= 0:
            raise Exception
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value <= 0:
            raise Exception
        self._height = value

    @property
    def area(self):
        return self._width * self._height

    def __str__(self):
        return f'Width: {self.width}, height: {self.height}'

Класс квадрат

@dataclass
class Square(Rectangle):
    def __init__(self, size: int):
        Rectangle.__init__(self, size, size)

    @Rectangle.widht.setter
    def widht(self, value):
        self._widht = self._height = value

    @Rectangle.height.setter
    def height(self, value):
        self._widht = self._height = value
Код создания объектов

if __name__ == '__main__':

    print('Прямоугольник')
    rs = Rectangle(5, 10)
    print(rs.area)
    rs.height = 20
    print(rs.area)

    print('Квадрат')
    sq = Square(5)
    print(sq.area)   #вывод 25
    sq.height = 10
    print(sq.area)  #вывод ошибочный 50
Проблема
Изменение параметров не приводит к изменению второго свойства класса.

Возможное решение
Согласно LSP нам необходимо использовать общий интерфейс для обоих классов и не наследовать Square от Rectangle. Этот общий интерфейс должен быть таким, чтобы в классах, реализующих его, предусловия не были более сильными, а постусловия не были более слабыми.

Первый способ — переделать иерархию так, чтобы Square не наследовался от Rectangle. Мы можем ввести новый класс, чтобы и квадрат, и прямоугольник наследовались от него.

Создадим абстрактный класс RightAngleShape, чтобы описать фигуры с прямым углом:

Класс RightAngleShape

from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class RightAngleShape(ABC):

    @abstractmethod
    def area(self):
        pass


@dataclass
class Rectangle(RightAngleShape):
    _width: int
    _height: int

    @property
    def width(self):
        return self._width

    @width.setter
    def widht(self, value):
        if value <= 0:
            raise Exception
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value <= 0:
            raise Exception
        self._height = value

    @property
    def area(self):
        return self._width * self._height

    def __str__(self):
        return f'Width: {self.width}, height: {self.height}'


@dataclass
class Square(RightAngleShape):
    _size: int

    @property
    def size(self):
        return self._width

    @size.setter
    def size(self, value):
        if value <= 0:
            raise Exception
        self._size = value

    @property
    def area(self):
        return self._size * self._size


if __name__ == '__main__':

    print('Прямоугольник')
    rs = Rectangle(5, 10)
    print(rs.area)
    rs.height = 20
    print(rs.area)

    print('Квадрат')
    sq = Square(5)
    print(sq.area)
    sq.size = 10
    print(sq.area)

Длинные цепочки наследования
Старайтесь не строить больших и глубоких иерархий.

Длинные цепочки иерархий типов — хрупкие. Вместо иерархий типов лучше использовать композицию, чтобы собирать сущности из необходимой функциональности.

Например, наследование предполагает проектирование от общего к частному в виде иерархии:

Животные -> Млекопитающие -> Человек

Такие иерархии не всегда правильно отражают связи сущностей в проектируемой системе. Иногда иерархий вовсе может быть несколько одновременно.

Композиция же подразумевает проектирование от частного к общему в виде совокупности нескольких обособленных наборов.

Например:

Человек состоит из: нервной системы, скелета, иммунной системы и т. д.


Вывод
Принцип подстановки Лисков требует использовать общий интерфейс для обоих классов и не наследовать Square от Rectangle.

Общий интерфейс должен быть таким, чтобы в классах, реализующих его, предусловия не были более сильными, а постусловия не были более слабыми.

Принцип подстановки Барбары Лисков:

  • помогает проектировать систему, опираясь на поведение модулей;
  • вводит ограничения и правила наследования объектов, чтобы их потомки не противоречили базовому поведению;
  • делает поведение модулей последовательным и предсказуемым;
  • помогает избегать дублирования, выделять общую для нескольких модулей функциональность в общий интерфейс;
  • позволяет выявлять при проектировании проблемные абстракции и скрытые связи между сущностями.