Ответ
Принцип подстановки Лисков (LSP) гласит, что объекты в программе должны быть заменяемыми экземплярами их подтипов без изменения корректности программы. Нарушение LSP происходит, когда подкласс изменяет ожидаемое поведение базового класса, делая его несовместимым с кодом, который работает с базовым типом.
Пример нарушения LSP (Python):
Рассмотрим классический пример с Rectangle и Square:
class Rectangle:
def __init__(self, width: int, height: int):
self._width = width
self._height = height
@property
def width(self) -> int:
return self._width
@width.setter
def width(self, value: int):
self._width = value
@property
def height(self) -> int:
return self._height
@height.setter
def height(self, value: int):
self._height = value
def area(self) -> int:
return self._width * self._height
class Square(Rectangle):
def __init__(self, side: int):
super().__init__(side, side)
@Rectangle.width.setter
def width(self, value: int):
# Нарушение: изменение ширины влияет на высоту
self._width = value
self._height = value
@Rectangle.height.setter
def height(self, value: int):
# Нарушение: изменение высоты влияет на ширину
self._width = value
self._height = value
def test_rectangle_area(rect: Rectangle):
# Ожидаем, что изменение ширины не повлияет на высоту
rect.width = 5
rect.height = 4
# Для Rectangle: 5 * 4 = 20
# Для Square: 5 * 5 = 25 (после rect.width = 5), затем 4 * 4 = 16 (после rect.height = 4)
assert rect.area() == 20 # Это утверждение будет ложным для Square!
# Тестирование с Rectangle - работает корректно
rectangle_obj = Rectangle(10, 2)
test_rectangle_area(rectangle_obj) # OK, area = 20
# Тестирование с Square - нарушает LSP
square_obj = Square(10)
try:
test_rectangle_area(square_obj) # AssertionError
except AssertionError:
print("Нарушение LSP: Square не может быть подставлен вместо Rectangle без изменения поведения.")
Объяснение нарушения:
Класс Square наследуется от Rectangle, но изменяет его фундаментальное поведение: при изменении ширины или высоты Square обе стороны устанавливаются одинаковыми. Это нарушает инвариант Rectangle, который предполагает независимое изменение сторон.
Функция test_rectangle_area, разработанная для работы с Rectangle, ожидает, что установка ширины и высоты будет работать независимо. При подстановке Square это ожидание нарушается, что приводит к некорректным результатам (assert rect.area() == 20 становится ложным, так как для Square площадь будет 16).
Почему это проблема и как исправить:
Нарушение LSP приводит к хрупкости кода, затрудняет его поддержку и расширение, так как подклассы не могут быть безопасно использованы там, где ожидается базовый класс. Правильным решением было бы не наследовать Square от Rectangle, поскольку Square не является 'специальным видом' Rectangle в контексте независимых размеров. Вместо этого можно использовать:
- Общий интерфейс (абстрактный базовый класс): Определить общий интерфейс
Shapeс методомarea(), от которого будут наследоваться какRectangle, так иSquare. - Композиция:
Squareможет содержатьRectangleкак внутренний компонент, управляя его размерами.
Ответ 18+ 🔞
Ну, слушай, вот есть у нас принцип, который называется принцип подстановки Лисков. Звучит, конечно, как заклинание из старой книги, но суть проста, как три копейки: если у тебя есть какая-то штука, и ты её заменяешь на её более специализированную версию, то вся программа должна продолжать работать, как ни в чём не бывало. Не должно быть такого, что подсунул ты «улучшенную» деталь, а весь механизм накрылся медным тазом.
А теперь смотри, как его можно, блядь, с треском нарушить на классическом примере с прямоугольником и квадратом. Все думают: «А, квадрат — это же частный случай прямоугольника, логично наследовать!» И вот тут-то и начинается пиздец.
class Rectangle:
def __init__(self, width: int, height: int):
self._width = width
self._height = height
@property
def width(self) -> int:
return self._width
@width.setter
def width(self, value: int):
self._width = value
@property
def height(self) -> int:
return self._height
@height.setter
def height(self, value: int):
self._height = value
def area(self) -> int:
return self._width * self._height
Всё чинно, благородно. Прямоугольник. Ширина и высота живут своей жизнью. А теперь рождается его «сынок» — Квадрат.
class Square(Rectangle):
def __init__(self, side: int):
super().__init__(side, side)
@Rectangle.width.setter
def width(self, value: int):
# Нарушение: изменение ширины влияет на высоту
self._width = value
self._height = value
@Rectangle.height.setter
def height(self, value: int):
# Нарушение: изменение высоты влияет на ширину
self._width = value
self._height = value
С виду вроде всё логично, да? Квадрат, стороны равны. Но ёпта, вот в чём подвох! Мы же переопределили сеттеры. Теперь, когда ты меняешь ширину, у тебя заодно и высота подтягивается. И наоборот. А теперь представь, что у тебя есть функция, которая заточена под работу с прямоугольником.
def test_rectangle_area(rect: Rectangle):
# Ожидаем, что изменение ширины не повлияет на высоту
rect.width = 5
rect.height = 4
# Для Rectangle: 5 * 4 = 20
# Для Square: 5 * 5 = 25 (после rect.width = 5), затем 4 * 4 = 16 (после rect.height = 4)
assert rect.area() == 20 # Это утверждение будет ложным для Square!
Вот она, мать его, ловушка! Для обычного прямоугольника всё ок: поставил ширину 5, высоту 4, площадь 20. А для квадрата получается ебаный цирк: сначала ты ставишь ширину 5, и он, хитрая жопа, автоматом и высоту в 5 меняет. Площадь уже 25. Потом ты ставишь высоту 4, а он и ширину в 4 меняет! И площадь становится 16. И твой assert летит в пизду с громким треском AssertionError. Это и есть нарушение LSP в чистом виде. Код, который работал с родителем, сломался на ребёнке.
Так в чём же, блядь, корень зла? А в том, что квадрат — это не прямоугольник в мире объектов с изменяемыми сторонами. С математической точки зрения — да, квадрат — это прямоугольник. А с точки зрения поведения нашего кода — нихуя! У прямоугольника есть негласный контракт: «ширина и высота независимы». Квадрат этот контракт нарушает, потому что у него они связаны. Подставляя квадрат, мы ломаем все ожидания.
И как тогда жить, спросишь ты? А не надо пытаться впихнуть невпихуемое через наследование! Есть варианты поумнее:
- Сделать общего предка. Например, абстрактный класс
Shapeс методомarea(). И пустьRectangleиSquareнаследуются от него, каждый со своей реализацией. И не лезут друг другу в сеттеры. - Использовать композицию. Сделать так, чтобы
Squareвнутри себя содержалRectangleи управлял им, сохраняя стороны равными, но не нарушая контракт прямоугольника для внешнего мира.
Короче, мораль такая: наследование — это не про «является с точки зрения школьного учебника», а про «ведёт себя как». Если поведение отличается — это верный признак, что наследование тут будет как волк в овечьей шкуре, только проблемы создаст. Думай, прежде чем наследовать, а то получишь такие сюрпризы, что мало не покажется.