Это наверно самая сложная часть SOLID , принцип подстановки Барбары Лисков,
для меня он был тоже самым противным в своё время, но стоило задаться вопросом понять и изучить, как стало достаточно просто.
2.3. Liskov Substitution Principle - Принцип подстановки Лискоу
Принцип подстановки Барбары Лисков сформулирован предельно формально и говорит вообще о любых типа, а не только классах ООП.
Если A является подтипом B, то в любом месте программы (функции), где требуется объект типа B, можно подставить объект типа A и поведение программы (функции) при этом не изменится.
В ООП если класс A унаследован от класса B, равно как класс A реализует абстрактный интерфейс B, то A является подтипом B, а B является супертипом A. Кроме того некоторые языки пограммирования поддерживают вариантность типов, для них тоже надо применять LSP, но там помогает компилятор.
Если коротко, то идея состоит в том, что объекты в данной иерархии классов могут быть заменены на их подтипы без нарушения корректности системы. При этом должны соблюдаться следующие условия:
• Предусловия в подтипе должны быть не сильнее, чем в супертипе.
• Постусловия в подтипе должны быть не слабее, чем в супертипе.
• все возбуждаемые исключения должны быть в равной степени заменяемыми
• сигнатуры методов должны быть полностью совместимы
Если более кратко объединить эти условия, можно сказать так, что, поведение наследуемых классов не должно противоречить поведению, заданному базовым классом. Почему? Потому что клиентский код начинает считать производный класс разновидностью базового, и возможно появление кода, явно использующего этот факт.
Вот из-за этих условий применять этот принцип оказывается не так просто, как могло бы показаться. Известный пример нарушения принципа LSP расширение класса квадрата из класса прямоугольника. В то время как в математике квадрат является частным случаем прямоугольника, а именно тот, в котором все стороны равны по длине, для ООП они совершенно разные. Классу прямоугольник могут потребоваться некоторые способы установки ширины и высоты, что не есть правильным для квадрата.
Пример:
class Rectangle
{
public function setHeight($height)
{
$this->height = $height;
}
public function setWidth($width)
{
$this->width = $width;
}
...
}
class Square extends Rectangle
{
/**
* Square has equal length sides so set both
*/
public function setHeight($height)
{
$this->height = $height;
$this->width = $height; // ??
}
public function setWidth($width)
{
$this->height = $width; // ??
$this->width = $width;
}
...
}
class RectangleUser
{
public function doSomething(Rectangle $rectangle)
{
$rectangle->setHeight(2);
$rectangle->setWidth(1);
$rectangle->render();
// Unexpected behavior follows, because the postconditions
// of these methods is that the height is 1 and width is 2
...
}
}
Этот код может работать, но его трудно понять, и ведет себя непредсказуемо. Вместо того чтобы полагаться на конкретную реализацию прямоугольника, методы Прямоугольника и квадрата должны быть извлечены в интерфейсе и, возможно, (частично) реализованы в абстрактном классе. Другие классы, которые используют эти методы класса Rectangle, теперь должны полагаться на новый интерфейс вместо этого.
interface Shape
{
public function render();
}
class Rectangle implements Shape
{
public function setHeight($height)
{
$this->height = $height;
}
public function setWidth($width)
{
$this->width = $width;
}
...
}
class Square implements Shape
{
/**
* Square has equal length sides so set both
*/
public function setLength($length)
{
$this->length = $length;
}
}
class ShapeUser
{
public function doSomething(Shape $shape)
{
// The dimensions of the shape cannot be set in a – размеры формы не //могут быть установлены в общих методах, но метод render общий
// general fashion, but the render method is generic
// $shape should be configured before calling doSomething – фигура //должна быть сконфигурирована перед вызовом метода doSomething
$shape->render();
...
}
}
Либо вот такой пример для показа сложности применения принципа LSP.
class Rectangle {
protected $_height = '';
protected $_width = '';
public function __construct($height, $width) {
$this->_height = $height;
$this->_width = $width;
}
public function getArea() {
return $this->_height*$this->_width;
}
}
Этот класс не делает ничего особенного, но выражает определение прямогугольника. Теперь давайте расширим этот класс и получим класс Square(квадрат).
Т.к. мы знаем, что все квадраты это прямоугольники, но не все прямоугольники это квадраты, то приходим к такой иерархии классов:
class Square extends Rectangle {
public function __construct($side) {
parent::__construct($side, $side);
}
}
К сожалению, у нас разные сигнатуры методов, т.е. мы нарушили требование 4. Сейчас мы это исправим:
class Square extends Rectangle {
public function __construct($height, $width) {
parent::__construct($height, $width);
}
}
Но сейчас мы пришли к тому, что Квадрат может быть неквадратным, т.о. надо добавить валидацию сторон:
class Square extends Rectangle {
public function __construct($height, $width) {
if ($height != $width) {
throw new Exception("That isn't a square knucklehead!");
}
parent::__construct($height, $width);
}
}
Но теперь мы добавили новое исключение и добавили новые предусловия на вход...Нарушение требований 1 и 3.