← Назад к вопросам

Приведи пример когда наследуемый класс не может выполнять поведение класса родителя

3.0 Senior🔥 122 комментариев
#Архитектура и паттерны

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Принцип подстановки Барбары Лисков (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?

  1. Изменение постусловий: Метод setDimensions базового класса подразумевает, что после его вызова width и height будут установлены в переданные значения независимо. Подкласс Square нарушает это постусловие, либо изменяя оба свойства (чего клиент не ожидает), либо выбрасывая исключение.

  2. Нарушение контракта для клиентского кода: Клиент, работающий с интерфейсом 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, когда подкласс не может гарантировать выполнение всех контрактов суперкласса.

Приведи пример когда наследуемый класс не может выполнять поведение класса родителя | PrepBro