Что такое принцип подстановки Лисков (LSP)?

Ответ

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

Проще говоря: Если у вас есть функция, работающая с классом Родитель, то она должна безошибочно работать и с любым классом Ребёнок, унаследованным от Родителя. Наследник не должен нарушать контракт базового класса.

Ключевые аспекты контракта:

  1. Предусловия (требования к входным данным) не могут быть усилены в подклассе.
  2. Постусловия (гарантии на выходе) не могут быть ослаблены в подклассе.
  3. Инварианты (условия, истинные на протяжении всей жизни объекта) должны сохраняться.

Классический пример нарушения LSP:

// Базовый класс, предполагающий, что все птицы летают.
class Bird {
    public function fly(): int {
        return $this->airSpeedVelocity; // Возвращает скорость полёта
    }
}

// Подкласс, нарушающий контракт. Пингвин не летает.
class Penguin extends Bird {
    public function fly(): int {
        throw new RuntimeException('Penguins cannot fly!'); // Усилено предусловие (теперь нужно НЕ летать)
        // или return 0; // Ослаблено постусловие (скорость полёта не имеет смысла)
    }
}

// Функция, рассчитанная на работу с Bird.
function traverseForest(array $birds) {
    foreach ($birds as $bird) {
        $speed = $bird->fly(); // Для Penguin выбросится исключение, программа сломается.
        // ...
    }
}

Как исправить, следуя LSP: Нужно пересмотреть иерархию наследования, чтобы она отражала реальные возможности объектов.

// Более точная абстракция.
abstract class Bird {
    // Общие для всех птиц методы (есть, пить, размножаться)
}

// Интерфейс для способности, а не для сущности.
interface Flyable {
    public function fly(): int;
}

class Sparrow extends Bird implements Flyable {
    public function fly(): int {
        return 15; // OK
    }
}

class Penguin extends Bird {
    public function swim(): int {
        return 10; // OK, пингвин плавает
    }
}

// Теперь функция работает только с летающими объектами.
function traverseForest(array $flyableBirds) {
    foreach ($flyableBirds as $bird) {
        $speed = $bird->fly(); // Гарантированно безопасно
    }
}

Соблюдение LSP делает систему устойчивой к изменениям и позволяет безопасно использовать полиморфизм.

Ответ 18+ 🔞

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

Говоря человеческим языком: Подставляй наследника вместо родителя — и ничего не должно ебнуться. Если функция ждёт Папу, она должна спокойно принять и Сына, не заметив подмены. А если заметила — значит, архитектура говно, и наследник нарушил негласный договор с предком.

В чём суть этого договора, спросишь ты? А вот в чём:

  1. Предусловия (что нужно для работы метода) — наследник не может быть привередливее родителя. Нельзя требовать больше, чем просил папаша.
  2. Постусловия (что метод гарантирует на выходе) — наследник не может схалтурить и обещать меньше, чем обещал родитель.
  3. Инварианты (что всегда истинно для объекта) — эти священные истины должны оставаться незыблемыми во всей династии.

Классический пиздец, где всё пошло по пизде:

// Допустим, есть класс "Птица". Логично же, птицы летают?
class Bird {
    public function fly(): int {
        return $this->airSpeedVelocity; // Возвращает скорость, всё чётко.
    }
}

// А вот и наш ушастый пингвин, мартышлюшка нелетающая.
class Penguin extends Bird {
    public function fly(): int {
        throw new RuntimeException('А пингвины не летают, мудила!'); // Нахуй сломали контракт!
        // Или вот так: return 0; // Типа летит со скоростью ноль. Это тоже пиздец, ослабили гарантии.
    }
}

// А тут у нас функция, которая гоняет стаю птиц по лесу.
function traverseForest(array $birds) {
    foreach ($birds as $bird) {
        $speed = $bird->fly(); // И тут БАБАХ! Для пингвина — исключение. Всё, пизда программе.
    }
}

Видишь, в чём ядрёна вошь? Мы подсунули наследника (Penguin) вместо родителя (Bird), а программа накрылась медным тазом. Это и есть нарушение LSP в чистом виде.

Как сделать по-человечески, без этих пиздопроебибен? Надо перестать выдумывать иерархии из головы и отражать реальность. Не все птицы летают — значит, и класс Bird не должен про это врать.

// Вот более честная абстракция. Просто птица, без обещаний.
abstract class Bird {
    // Общие для всех птиц штуки: есть, пить, срать.
}

// А способность летать — это отдельный скилл, интерфейс.
interface Flyable {
    public function fly(): int;
}

// Воробей — птица И летающая. Всё ок.
class Sparrow extends Bird implements Flyable {
    public function fly(): int {
        return 15; // Летит себе, красавец.
    }
}

// Пингвин — птица, но не летающая. И не надо ему эту хуйню впихивать.
class Penguin extends Bird {
    public function swim(): int {
        return 10; // Зато плавает, сука, как торпеда!
    }
}

// И теперь наша функция работает ТОЛЬКО с теми, кто реально умеет летать.
// Никаких сюрпризов, доверия — овердохуища.
function traverseForest(array $flyableBirds) {
    foreach ($flyableBirds as $bird) {
        $speed = $bird->fly(); // Тут можно спать спокойно, исключений не будет.
    }
}

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