В чем суть принципа подстановки Лисков (LSP) и приведи пример его соблюдения и нарушения?

Ответ

Принцип подстановки Лисков (LSP) гласит: объекты в программе должны быть заменяемыми экземплярами их базовых типов без изменения корректности этой программы. Наследующий класс должен:

  1. Выполнять все контракты (обещания) родительского класса.
  2. Не накладывать более строгих предусловий (требований к входным данным).
  3. Не ослаблять постусловий (гарантий на выходные данные или состояние).

Пример нарушения LSP

class Rectangle {
    var width: Double = 0
    var height: Double = 0

    var area: Double {
        return width * height
    }

    func setDimensions(width: Double, height: Double) {
        self.width = width
        self.height = height
    }
}

class Square: Rectangle {
    // Нарушение LSP: Квадрат изменяет ожидаемое поведение родителя.
    // Для Rectangle width и height независимы, для Square — связаны.
    override func setDimensions(width: Double, height: Double) {
        let side = min(width, height) // Неожиданное поведение!
        super.setDimensions(width: side, height: side)
    }
}

// Функция, работающая с базовым классом
func doubleWidth(of rectangle: Rectangle) {
    rectangle.setDimensions(width: rectangle.width * 2, 
                           height: rectangle.height) // Ожидается, что изменится только ширина.
}

let rect = Rectangle()
rect.setDimensions(width: 5, height: 4)
doubleWidth(of: rect)
print("Rectangle area: (rect.area)") // Ожидаемо: 5*2 * 4 = 40

let sq: Rectangle = Square() // Подстановка Square вместо Rectangle
sq.setDimensions(width: 5, height: 4)
doubleWidth(of: sq) // Внутри вызовется переопределенный метод Square
print("Square area (treated as Rectangle): (sq.area)") // Неожиданно: 4*4 = 16 (а не 40)!

Почему это нарушение: Код, использующий Rectangle, ожидает, что width и height изменяются независимо. Square ломает это предположение, что приводит к ошибкам. С точки зрения LSP, Square не является подтипом Rectangle в поведенческом смысле.

Пример соблюдения LSP

// Абстракция через протокол
protocol Shape {
    var area: Double { get }
}

// Конкретные реализации, не связанные наследованием
struct RectangleShape: Shape {
    let width, height: Double
    var area: Double { width * height }
}

struct SquareShape: Shape {
    let side: Double
    var area: Double { side * side }
}

struct CircleShape: Shape {
    let radius: Double
    var area: Double { .pi * radius * radius }
}

// Функция работает с абстракцией Shape и корректна для любой подстановки.
func printArea(of shape: Shape) {
    print("The area is (shape.area)")
}

let shapes: [Shape] = [RectangleShape(width: 5, height: 4),
                       SquareShape(side: 5),
                       CircleShape(radius: 3)]

for shape in shapes {
    printArea(of: shape) // Всегда корректно, LSP соблюден.
}

Ключевой вывод: LSP поощряет проектирование через абстракции (протоколы/интерфейсы) и композицию, а не через глубокие иерархии наследования, которые могут нарушать поведенческие контракты.

Ответ 18+ 🔞

Да ты посмотри, на что они нас, программистов, подписывают! Принцип подстановки Лисков, блядь. Звучит-то как научно, а по сути — элементарная вещь, которую любой мудак, накосячивший с наследованием, потом осознаёт через боль и страдания.

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

А теперь реальность, блядь! Берут классический пример — прямоугольник и квадрат. Ну, в геометрии-то квадрат — частный случай прямоугольника, да? А в коде — пиздец и развод.

class Rectangle {
    var width: Double = 0
    var height: Double = 0

    var area: Double {
        return width * height
    }

    func setDimensions(width: Double, height: Double) {
        self.width = width
        self.height = height
    }
}

Всё чинно, благородно. Прямоугольник. Ширину и высоту задал — площадь посчитал. Контракт простой: width и height живут своей жизнью.

А теперь какой-то умник думает: «А давайте я сделаю класс Square, который наследуется от Rectangle. Это же логично!». И начинает творить дичь:

class Square: Rectangle {
    override func setDimensions(width: Double, height: Double) {
        let side = min(width, height) // Неожиданное поведение!
        super.setDimensions(width: side, height: side)
    }
}

Вот тут-то и начинается, ёпта! Родитель-то обещал, что setDimensions установит ровно то, что ему передали. А этот выродок-квадрат берёт и сам решает, какую сторону использовать! Он контракт родителя в пизду выкинул.

И вот представь, есть у тебя функция, которая работает с прямоугольником:

func doubleWidth(of rectangle: Rectangle) {
    rectangle.setDimensions(width: rectangle.width * 2,
                           height: rectangle.height) // Ожидается, что изменится только ширина.
}

Всё ок, работает. Берём прямоугольник 5x4, удваиваем ширину — площадь становится 40. Красота.

А теперь подсовываем квадрат, замаскированный под прямоугольника:

let sq: Rectangle = Square() // Подстановка Square вместо Rectangle
sq.setDimensions(width: 5, height: 4) // Установится сторона 4, ёбана!
doubleWidth(of: sq) // Вызовется переопределённый метод Square! Он опять возьмёт минимум!
print("Square area (treated as Rectangle): (sq.area)") // Блядь, 16! А где 40?!

Вот тебе и «частный случай прямоугольника». Нахуй он никому не сдался в таком виде. Программа ебнулась, потому что квадрат нарушил все ожидания. Он не является (в поведенческом смысле!) подтипом прямоугольника. Он — хитрая жопа, которая притворяется.

Так как же правильно, блядь? Да не лепить иерархию наследования там, где она не работает! Использовать абстракцию, ёпта!

protocol Shape {
    var area: Double { get } // Единственный честный контракт.
}

struct RectangleShape: Shape {
    let width, height: Double
    var area: Double { width * height }
}

struct SquareShape: Shape {
    let side: Double
    var area: Double { side * side }
}

struct CircleShape: Shape {
    let radius: Double
    var area: Double { .pi * radius * radius }
}

Смотри, какая красота! Никакого наследования, только протокол. У каждой фигуры есть площадь, и каждая сама решает, как её считать. Никаких скрытых пиздецов.

func printArea(of shape: Shape) {
    print("The area is (shape.area)")
}

let shapes: [Shape] = [RectangleShape(width: 5, height: 4),
                       SquareShape(side: 5),
                       CircleShape(radius: 3)]

for shape in shapes {
    printArea(of: shape) // Всё работает, все довольны. LSP не просто соблюдён, он ликует!
}

Вывод, сука: Этот принцип — не про геометрию или бизнес-логику. Он про то, чтобы твои абстракции не были пиздопроебибными. Проектируй через интерфейсы (протоколы), а не через кривое наследование. И тогда подставляй что угодно — будет работать, а не ебать мозг тебе и всем, кто будет читать твой код после.