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
Продолжение статьи: Принцип разделения интерфейса