Последние записи в блоге

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

Автор: Patkos Csaba 
Дата: 24 Jan 2014

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

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

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

Агрегатор фриланс бирж FreelanceGrab, искать заказы на фрилансе стало еще проще.
8 крупных бирж, удобный поиск и фильтрация по проектам,
моментальное обновление ленты без перезагрузки страницы