Приведи пример когда наследуемый класс не может выполнять поведение класса родителя
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Принцип подстановки Барбары Лисков (LSP) и пример его нарушения
В объектно-ориентированном программировании принцип подстановки Барбары Лисков (LSP) гласит, что объекты наследника должны быть заменяемы на объекты родителя без изменения корректности программы. Классический пример нарушения LSP — иерархия «Прямоугольник-Квадрат», где попытка заставить квадрат вести себя как прямоугольник с независимыми шириной и высотой приводит к логической ошибке.
Подробный пример с кодом
Предположим, у нас есть базовый класс Rectangle, который инкапсулирует логику работы с прямоугольником.
open class Rectangle {
open var width: Double = 0.0
open var height: Double = 0.0
open fun area(): Double {
return width * height
}
open fun setDimensions(newWidth: Double, newHeight: Double) {
width = newWidth
height = newHeight
}
}
Теперь мы создаем подкласс Square, который "является" прямоугольником с математической точки зрения, но накладывает дополнительное ограничение: ширина должна равняться высоте.
class Square : Rectangle() {
override var width: Double
get() = super.width
set(value) {
super.width = value
super.height = value // Нарушение инварианта родителя!
}
override var height: Double
get() = super.height
set(value) {
super.height = value
super.width = value // Нарушение инварианта родителя!
}
override fun setDimensions(newWidth: Double, newHeight: Double) {
// Квадрат не может корректно выполнить этот метод!
// Он должен либо игнорировать одно из значений, либо бросать исключение.
if (newWidth != newHeight) {
throw IllegalArgumentException("Ширина и высота квадрата должны быть равны")
}
super.setDimensions(newWidth, newHeight)
}
}
Почему это нарушает LSP?
-
Изменение постусловий: Метод
setDimensionsбазового класса подразумевает, что после его вызоваwidthиheightбудут установлены в переданные значения независимо. ПодклассSquareнарушает это постусловие, либо изменяя оба свойства (чего клиент не ожидает), либо выбрасывая исключение. -
Нарушение контракта для клиентского кода: Клиент, работающий с интерфейсом
Rectangle, ожидает определенного поведения.
fun testRectangle(rect: Rectangle) {
rect.setDimensions(5.0, 4.0) // Устанавливаем ширину 5, высоту 4
// Клиентский код рассчитывает на независимость свойств.
// Для Rectangle: width=5, height=4 -> area = 20
// Для Square: width=4, height=4 -> area = 16 (НЕОЖИДАННО!)
val expectedArea = 20.0
val actualArea = rect.area()
if (actualArea != expectedArea) {
println("Нарушение LSP! Ожидалось $expectedArea, получено $actualArea")
}
}
// Использование:
val myRect: Rectangle = Square() // Подстановка квадрата вместо прямоугольника
testRectangle(myRect) // Выведет: "Нарушение LSP! Ожидалось 20.0, получено 16.0"
Ключевая проблема: Подкласс Square не может выполнять все поведение своего родителя Rectangle. Он не может гарантировать корректную работу метода setDimensions с разными значениями аргументов, что является фундаментальным требованием к прямоугольнику.
Правильный дизайн
Вместо наследования "is-a" (квадрат является прямоугольником) следует использовать композицию, общий интерфейс или признак (trait).
interface Shape {
fun area(): Double
}
class Rectangle(val width: Double, val height: Double) : Shape {
override fun area() = width * height
}
class Square(val side: Double) : Shape {
override fun area() = side * side
// У квадрата нет методов setWidth/setHeight, нарушающих контракт.
}
Вывод: Наследование должно расширять, а не ограничивать поведение базового класса. Если подкласс не может выполнить какой-либо метод родителя, не изменяя его ожидаемой семантики или не выбрасывая исключений, это явный сигнал о нарушении LSP и необходимости пересмотра архитектуры. В Android-разработке подобные ошибки часто возникают при неправильном наследовании от View, Adapter или Fragment, когда подкласс не может гарантировать выполнение всех контрактов суперкласса.