Что является примером принципа подстановки Лискова? - PullRequest
783 голосов
/ 11 сентября 2008

Я слышал, что принцип замещения Лискова (LSP) является фундаментальным принципом объектно-ориентированного проектирования. Что это такое и какие примеры его использования?

Ответы [ 28 ]

15 голосов
/ 18 декабря 2016

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

Итак, у Лискова есть 3 базовых правила:

  1. Правило подписи: Должна быть допустимая реализация каждой операции надтипа в подтипе синтаксически. Что-то, что компилятор сможет проверить для вас. Существует небольшое правило о том, что нужно генерировать меньше исключений и быть по крайней мере таким же доступным, как и методы супертипа.

  2. Правило методов: реализация этих операций семантически обоснована.

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

    • Инварианты: вещи, которые всегда верны, должны оставаться верными. Например. Размер набора никогда не бывает отрицательным.
    • Эволюционные свойства: обычно что-то связанное с неизменяемостью или типом состояний, в которых может находиться объект. Или, возможно, объект только растет и никогда не сжимается, поэтому методы подтипа не должны его создавать.

Все эти свойства должны быть сохранены, и дополнительная функциональность подтипа не должна нарушать свойства супертипа.

Если об этих трех вещах позаботятся, вы абстрагировались от базового материала и пишете слабосвязанный код.

Источник: Разработка программ на Java - Барбара Лисков

13 голосов
/ 29 октября 2017

Длинная история короткая, давайте оставим прямоугольники прямоугольники и квадраты квадратов, практический пример при расширении родительского класса, вы должны либо СОХРАНИТЬ точный родительский API, либо РАСШИРИТЬ.

Допустим, у вас есть база ItemsRepository.

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

И подкласс, расширяющий его:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

Тогда у вас может быть Клиент , работающий с API Base ItemsRepository и использующий его.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

LSP прерывается, когда замена родительского класса подклассом нарушает контракт API .

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

Вы можете узнать больше о написании поддерживаемого программного обеспечения в моем курсе: https://www.udemy.com/enterprise-php/

9 голосов
/ 03 апреля 2009

Эта формулировка LSP слишком сильна:

Если для каждого объекта o1 типа S существует объект o2 типа T такой, что для всех программ P, определенных в терминах T, поведение P остается неизменным, когда o1 заменяется на o2, тогда S является подтипом Т.

Что в основном означает, что S - это другая, полностью инкапсулированная реализация того же самого, что и T. И я мог бы быть смелым и решить, что производительность является частью поведения P ...

Таким образом, любое использование позднего связывания нарушает LSP. Весь смысл ОО в том, чтобы получить другое поведение, когда мы заменяем объект одного вида другим видом!

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

7 голосов
/ 18 февраля 2018

Я вижу прямоугольники и квадраты в каждом ответе и как нарушать LSP.

Я бы хотел показать, как LSP может быть согласован с реальным примером:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

Этот дизайн соответствует LSP, потому что поведение остается неизменным независимо от реализации, которую мы решили использовать.

И да, вы можете нарушить LSP в этой конфигурации, сделав одно простое изменение следующим образом:

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

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

7 голосов
/ 16 августа 2017

В очень простом предложении мы можем сказать:

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

7 голосов
/ 27 декабря 2013

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

  • Варианты базового класса должны сохраняться производным классом
  • Предварительные условия базового класса не должны быть усилены производным классом
  • Постусловия базового класса не должны быть ослаблены производным классом.

Таким образом, производный должен знать о трех вышеупомянутых условиях, наложенных базовым классом. Следовательно, правила подтипирования заранее определены. Это означает, что отношения «IS A» должны соблюдаться только тогда, когда подтип подчиняется определенным правилам. Эти правила в форме инвариантов, предварительных условий и постусловий должны определяться официальным « контрактом на проектирование ».

Дальнейшие обсуждения по этому вопросу доступны в моем блоге: Принцип подстановки Лискова

5 голосов
/ 21 мая 2018

Принцип замещения Лискова (LSP)

Все время мы разрабатываем программный модуль и создаем некоторый класс Иерархии. Затем мы расширяем некоторые классы, создавая некоторые производные классы.

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

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

Пример:

Ниже приведен классический пример нарушения принципа подстановки Лискова. В примере используются 2 класса: Rectangle и Square. Давайте предположим, что объект Rectangle используется где-то в приложении. Расширяем приложение и добавляем класс Square. Квадратный класс возвращается фабричным шаблоном, основанным на некоторых условиях, и мы не знаем точно, какой тип объекта будет возвращен. Но мы знаем, что это прямоугольник. Мы получаем объект прямоугольника, устанавливаем ширину 5 и высоту 10 и получаем площадь. Для прямоугольника с шириной 5 и высотой 10 площадь должна быть 50. Вместо этого результат будет 100

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

Вывод:

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

См. Также: Принцип открытого закрытия

Некоторые похожие концепции для лучшей структуры: Соглашение по конфигурации

4 голосов
/ 28 июля 2015

Квадрат - это прямоугольник, ширина которого равна высоте. Если квадрат устанавливает два разных размера для ширины и высоты, он нарушает инвариант квадрата. Это обходится путем введения побочных эффектов. Но если у прямоугольника есть setSize (высота, ширина) с предварительным условием 0 <высота и 0 <ширина. Метод производного подтипа требует height == width; более сильное предварительное условие (и это нарушает LSP). Это показывает, что хотя квадрат является прямоугольником, он не является допустимым подтипом, поскольку предварительное условие усиливается. Обход (вообще плохая вещь) вызывает побочный эффект, и это ослабляет почтовое условие (которое нарушает lsp). У setWidth на базе есть условие post 0 <width. Производная ослабляет его с высотой == ширина. </p>

Поэтому квадрат с изменяемым размером не является прямоугольником с изменяемым размером.

4 голосов
/ 11 сентября 2008

Будет ли реализация ThreeDBoard с точки зрения массива Board настолько полезной?

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

Что касается внешнего интерфейса, вы можете выделить интерфейс Board для TwoDBoard и ThreeDBoard (хотя ни один из перечисленных методов не подходит).

3 голосов
/ 10 февраля 2019

Давайте проиллюстрируем на Java:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

Здесь нет проблем, верно? Автомобиль определенно является транспортным устройством, и здесь мы видим, что он переопределяет метод startEngine () своего суперкласса.

Давайте добавим еще одно транспортное средство:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

Сейчас все идет не так, как планировалось! Да, велосипед - это транспортное средство, однако у него нет двигателя, и поэтому метод startEngine () не может быть реализован.

Это те виды проблем, которые нарушают подстановку Лискова Принцип приводит к, и они чаще всего могут быть признаны метод, который ничего не делает или даже не может быть реализован.

Решением этих проблем является правильная иерархия наследования, и в нашем случае мы решили бы эту проблему путем дифференциации классов транспортных устройств с двигателями и без них. Даже если велосипед - это транспортное средство, у него нет двигателя. В этом примере наше определение транспортного устройства неверно. У него не должно быть двигателя.

Мы можем реорганизовать наш класс TransportationDevice следующим образом:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

Теперь мы можем расширить TransportationDevice для немоторизованных устройств.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

и расширение службы транспорта для моторизованных устройств. Здесь более уместно добавить объект Engine.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

Таким образом, наш класс автомобилей становится более специализированным, придерживаясь принципа подстановки Лискова.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

И наш класс велосипедов также соответствует принципу замещения Лискова.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...