DevGang
Авторизоваться

SOLID «L»: Принцип подстановки Барбары Лисков

Принцип единственной обязанности, открытости-закрытости, подстановки, разделения интерфейса и инвертирования зависимостей. Это – пятерка принципов, которыми следует руководствоваться при написании кода. Принципы подстановки и отделения интерфейса – очень просты сами по себе, а значит их оба можно рассмотреть в одной статье.

Принцип подстановки Барбары Лисков (LSP).

Подклассы не могут замещать поведение базовых классов. Концепция принципа подстановки была предложена Барбарой Лисков в ее докладе на конференции 1987 года, а спустя 7 лет – опубликована в соавторстве с Джаннет Вин. Оригинальное определение принципа, предложенное Барбарой, следующее:

«В том случае, если q(x) – свойство, верное по отношению к объектам х некого типа T, то свойство q(y) тоже будет верным относительно ряда объектов y, которые относятся к типу S, при этом S – подтип некого типа T.»

Некоторое время спустя, после публикации Робертом С. Мартином всей пятерки принципов SOLID в книге о быстрой разработке программ, а затем и после публикации версии книги о быстрой разработке для языка программирования C#, принцип  стал называться принципом подстановки Барбары Лисков.

Это приводит нас к определению, которое дал сам Роберт С. Мартин:

Подтипы должны дополнять базовые типы.

Если это разъяснить, то получится, что подклассы должны переопределять методы базового класса так, чтобы не нарушалась функциональность с точки зрения клиента. Подробно это можно рассмотреть на простом примере:

class Vehicle {
 
    function startEngine() {
        // Default engine start functionality
    }
 
    function accelerate() {
        // Default acceleration functionality
    }
}

Есть существующий класс Vehicle, который может быть и абстрактным в том числе, и две реализации:

class Car extends Vehicle {
 
    function startEngine() {
        $this->engageIgnition();
        parent::startEngine();
    }
 
    private function engageIgnition() {
        // Ignition procedure
    }
 
}
 
class ElectricBus extends Vehicle {
 
    function accelerate() {
        $this->increaseVoltage();
        $this->connectIndividualEngines();
    }
 
    private function increaseVoltage() {
        // Electric logic
    }
 
    private function connectIndividualEngines() {
        // Connection logic
    }
 
}

Клиентский класс должен иметь возможность использовать любой из них, если он может использовать Vehicle.

class Driver {
    function go(Vehicle $v) {
        $v->startEngine();
        $v->accelerate();
    }
}

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

Основываясь на предыдущем опыте с принципом открытости-закрытости, можно сделать вывод, что принцип Барбары Лисков сильно с ним связан. И в самом деле, как сказал Роберт Мартин, нарушение принципа LSP – это скрытое нарушение принципа OCP. Шаблонный метод проектирования – классический пример соблюдения и реализации принципа подстановки, который, в свою очередь, является одним из способов соблюдения OCP.

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

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

class Rectangle {
 
    private $topLeft;
    private $width;
    private $height;
 
    public function setHeight($height) {
        $this->height = $height;
    }
 
    public function getHeight() {
        return $this->height;
    }
 
    public function setWidth($width) {
        $this->width = $width;
    }
 
    public function getWidth() {
        return $this->width;
    }
 
}

Мы начнем с основной геометрической формы – прямоугольника (Rectangle). Это всего лишь простой объект данных с сеттерами и геттерами для ширины (width) и высоты (height). Если представить, что приложение уже работает и даже на нескольких клиентах, которым нужно управлять этим прямоугольником так, чтобы сделать из него квадрат, то придется ввести новые функции.

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

Но будет ли квадрат Square прямоугольником Rectangle уже в программировании?

class Square extends Rectangle {
 
    public function setHeight($value) {
        $this->width = $value;
        $this->height = $value;
    }
 
    public function setWidth($value) {
        $this->width = $value;
        $this->height = $value;
    }
}

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

class Client {
 
    function areaVerifier(Rectangle $r) {
        $r->setWidth(5);
        $r->setHeight(4);
 
        if($r->area() != 20) {
            throw new Exception('Bad area!');
        }
 
        return true;
    }
 
}

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

function area() {
    return $this->width * $this->height;
}

Ну и конечно добавлен метод класса Rectangle.

class LspTest extends PHPUnit_Framework_TestCase {
 
    function testRectangleArea() {
        $r = new Rectangle();
        $c = new Client();
        $this->assertTrue($c->areaVerifier($r));
    }
 
}

С помощью простого теста можно проверить работу: отправим пустой прямоугольный объект для определения его площади. Работает. Если наш класс Square определяется корректно, то его отправка на клиентский areaVerifier() не повредит функциональности. В конце концов, в математическом понимании Square – это все тот же Rectangle. Но наш ли это класс?

function testSquareArea() {
    $r = new Square();
    $c = new Client();
    $this->assertTrue($c->areaVerifier($r));
}

Тестирование проходит легко и много времени не занимает. А уведомление появляется при запуске теста выше.

PHPUnit 3.7.28 by Sebastian Bergmann.
 
Exception : Bad area!
#0 /paht/: /.../.../LspTest.php(18): Client->areaVerifier(Object(Square))
#1 [internal function]: LspTest->testSquareArea()

Итак, в нашем «программном» смысле Square класс - это не Rectangle, иначе бы законы геометрии и принцип подстановки Барбары Лисков нарушались.

Этот пример особенно хорош тем, что он показывает и нарушение LSP, и то, что объектно-ориентированное программирование не может применить правила реальной жизни к объектам. Каждый объект здесь должен быть абстракцией над концепцией. А если мы попытаемся сопоставить реальный объект и программный объект, то у нас никогда это не получится.

Источник: code.tutsplus.com 

Продолжение статьи: Принцип разделения интерфейса

#PHP
Комментарии 2
Kirill Petyushin 26.11.2020 в 02:38

Вроде бы и хороший пример, но насчет неприменимости "правил реальной жизни к ООП"... не то чтобы не согласен, но в данном примере все пошло не так именно потому, что реальные правила геометрии были нарушены при построении модели: Ведь проверяя прямоугольник описанным способом мы явно указываем, что его стороны не равны, а такой прямоугольник по определению не может быть квадратом. Получается ошибка в том, что класс Rectangle не может быть родителем класса квадрата, так как к нему(к Rectangle), видимо, есть дополнительные требования, которые делают в принципе невозможным образование из него квадрата. Получается тут может быть несколько ошибок: 1. клиентская функция неправильно использовала(проверяла) объекты; 2. была совершена ошибка при проектировании иерархии классов, т.к. требовалось разработать именно неравносторонний прямоугольник и квадрат, которые по законам геометрии друг с другом в иерархической связи не состоят. И тогда не совсем понятно, то есть если бы мы не переопределили родительские методы, а как-нибудь выкрутились из ситуации, то ошибки бы не выявились? Или получается, что без переопределения тут не обойтись, что приводит к нарушению LSP и помогает выявить ошибки?

algetar 08.06.2022 в 09:22

Мне кажется тут надо исходить из того, что: любой квадрат является прямоугольником, и не любой прямоугольник является квадратом. И определить сначала класс квадрат, а потом наследовать от него класс прямоугольник.

Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу