Эпизод 5. LSP: The Liskov Substitution Principle

Приветствую вас в 5-ом эпизоде сериала “ООП здорового человека”. Здесь речь пойдет о 3-ем принципе SOLID – принцип подстановки Лисков. С названия просто ничего не понятно… поскольку Лисков – это фамилия ученого-информатика по имени Барбара. В общем, этот принцип(как и многие) придуман не дядей Бобом, а всего-лишь пере озвучен и переосмыслен – за что, в конкретно этом случаи, огромный респект ему…

Теория

Респект потому, что в оригинале формулировка имеет сложное математическое определение. Для сравнения, оригинал:

Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.

Книга Мартина:

Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.

Простыми словами(вывод, которому я пришёл со второй формулировки, если подумать – первая о том же): проектируй код так, чтоб при замене объекта-родителя объектом-наследником система не падала. Вообще, этот принцип мне видится как частный случай The Open Closed Principle(проясните мне ваше виденье в комментах! только без токсика..).

Пример

В качестве примера, иллюстрирующего этот принцип, принято говорить о прямоугольнике и квадрате(частный случай прямоугольника) или метод передвижения птиц. На практике же(вероятно, так делают все промышленные прогеры(или нет), этим стоит заниматься когда точно знаешь направление развития проекта – в общем, у меня еще нет своих примеров под этот принцип(будут – допишу).

Практика

Коль собственных примеров нет(та и, в принципе – он будет аналогичен этим) – опишу фигуры и птиц в виде классов. Начнем с прямоугольника и квадрата. В прямоугольнике есть высота h, ширина w и добавим метод по вычислению площади area()

<?php

class Rectangle
{
    public int $h;
    public int $w;

    public function setH(int $h): void
    {
        $this->h = $h;
    }

    public function setW(int $w): void
    {
        $this->w = $w;
    }
    
    public function getArea(): int
    {
        return $this->w * $this->h;
    }
}

$rectangle  = new Rectangle();
$rectangle->setH(8);
$rectangle->setW(5);
echo $rectangle->getArea();

/*
Вывод:
40

и, поскольку квадрат это частный случай прямоугольника, кажется логичным квадрат наследовать от прямоугольника – но: задав высоту – как быть с шириной и наоборот?

Вот мой вариант решения этой задачи(уверен, не лучший, ты то точно знаешь лучший)

<?php

abstract class Figure
{
    protected $params;

    public function setParams(array|int $params): void
    {
        $this->params = $params;
    }

    abstract public function getArea(): int;
}

class Rectangle extends Figure
{
    public function getArea(): int
    {
        return (count($this->params) > 1) ?
            $this->params[0] * $this->params[1] :
            throw new Exception('Set all params');
    }
}

class Square extends Figure
{
    public function getArea(): int
    {
        return pow($this->params,2);
    }
}

$rectangle  = new Rectangle();
$rectangle->setParams([5,8]);
echo $rectangle->getArea().PHP_EOL;

$square = new Square();
$square->setParams(6);
echo $square->getArea();

/*
Вывод:
40
36

Он заключается в том, чтоб прямоугольник и квадрат не были в единой ветке наследования а произрастали от общего предка – абстрактной фигуры.

С птицами аналогична: одни летают, вторые плавают, третьи – ножками перемещаются. При работе с такими объектами нужно дергать за какой-то абстрактный метод(к примеру, move()), где , по возможности, выбирается нужный метод. В целом, смысл следующий: класс-наследник должен уметь работать со всеми методами родителя.