Каков пример Принципа замещения Лискова?

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

Ответы

Ответ 1

Принцип замещения Лискова (LSP, ) - это понятие в объектно-ориентированном программировании, которое гласит:

Функции, которые используют указатели или ссылки на базовые классы должны быть возможность использования объектов производных классов не зная об этом.

В основе LSP лежит интерфейсы и контракты, а также как решать, когда расширить класс и использовать другую стратегию, такую ​​как композиция для достижения вашей цели.

Самый эффективный способ, который я видел, чтобы проиллюстрировать этот момент, был в Head First OOA & D. Они представляют собой сценарий, в котором вы являетесь разработчиком проекта для создания основы для стратегических игр.

Они представляют класс, представляющий плату, которая выглядит так:

Class Diagram

Все методы принимают координаты X и Y в качестве параметров для определения положения плитки в двумерном массиве Tiles. Это позволит игровому разработчику управлять подразделениями на борту в ходе игры.

В книге говорится об изменении требований к заявлению о том, что работа игрового фрейма также должна поддерживать 3D-игровые платы для размещения игр, которые имеют полет. Таким образом, вводится класс ThreeDBoard, который расширяет Board.

На первый взгляд это кажется хорошим решением. Board предоставляет свойства Height и Width, а ThreeDBoard - ось Z.

Где он ломается, когда вы смотрите на всех остальных членов, унаследованных от Board. Методы для AddUnit, GetTile, GetUnits и т.д. Все принимают как параметры X, так и Y в классе Board, но ThreeDBoard также требуется параметр Z.

Итак, вы должны снова реализовать эти методы с помощью параметра Z. Параметр Z не имеет контекста для класса Board, и унаследованные методы из класса Board теряют смысл. Единице кода, пытающейся использовать класс ThreeDBoard в качестве базового класса Board, было бы очень не повезло.

Возможно, нам стоит найти другой подход. Вместо расширения Board, ThreeDBoard должен состоять из объектов Board. Один объект Board на единицу оси Z.

Это позволяет нам использовать хорошие объектно-ориентированные принципы, такие как инкапсуляция и повторное использование, и не нарушает LSP.

Ответ 2

Отличным примером, иллюстрирующим LSP (данный дядей Бобом в подкасте, который я недавно слышал), было то, как иногда что-то, что звучит правильно на естественном языке, не совсем работает в коде.

В математике Square - это Rectangle. На самом деле это специализация прямоangularьника. "Is" заставляет вас моделировать это с наследованием. Однако если в коде, который вы сделали Square, получен из Rectangle, то Square должен использоваться везде, где вы ожидаете Rectangle. Это делает для некоторого странного поведения.

Представьте, что у вас есть методы SetWidth и SetHeight в вашем базовом классе Rectangle; это кажется совершенно логичным. Однако, если ваша ссылка Rectangle указала на Square, то SetWidth и SetHeight не имеют смысла, поскольку установка одного из них приведет к изменению другого в соответствии с ним. В этом случае Square не проходит тест замены Лискова с помощью Rectangle, и абстракция наличия Square наследуется от Rectangle является плохой.

enter image description here

Вам следует проверить другие бесценные мотивационные плакаты SOLID Принципы.

Ответ 3

LSP относится к инвариантам.

Классический пример представлен следующим объявлением псевдокода (реализация опущена):

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

Теперь у нас есть проблема, хотя интерфейс соответствует. Причина в том, что мы нарушили инварианты, вытекающие из математического определения квадратов и прямоугольников. Как работают геттеры и сеттеры, a Rectangle должен удовлетворять следующему инварианту:

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

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

Ответ 4

Подстановочность - это принцип в объектно-ориентированном программировании, утверждающий, что в компьютерной программе, если S является подтипом T, объекты типа T могут быть заменены объектами типа S

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

Плохой пример

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

Утка может летать, потому что это птица, но как насчет этого?

public class Ostrich extends Bird{}

Страус - это птица, но он не может летать, класс Страус - это подтип класса Bird, но он не может использовать метод fly, это означает, что мы нарушаем принцип LSP.

Хороший пример

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

Ответ 5

Роберт Мартин имеет отличную статью о принципе замены Лискова. В нем обсуждаются тонкие и не очень тонкие способы нарушения принципа.

Некоторые соответствующие части статьи (обратите внимание, что второй пример сильно уплотнен):

Простой пример нарушения LSP

Одним из самых вопиющих нарушений этого принципа является использование С++ Информация о времени выполнения (RTTI) для выбора функции, основанной на тип объекта. то есть:.

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

Очевидно, что функция DrawShape плохо сформирована. Он должен знать о каждая возможная производная класса Shape, и ее необходимо изменить при создании новых производных от Shape. Действительно, многие рассматривают структуру этой функции как анафему для объектно-ориентированного проектирования.

Квадрат и прямоугольник, более тонкое нарушение.

Однако существуют и другие, гораздо более тонкие способы нарушения LSP. Рассмотрим приложение, которое использует класс Rectangle, как описано ниже:

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

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

Очевидно, что квадрат является прямоугольником для всех нормальных целей и целей. Поскольку соотношение ISA выполняется, логично моделировать Squareкласс, полученный из Rectangle. [...]

Square наследует функции SetWidth и SetHeight. Эти функции совершенно неуместны для Square, так как ширина и высота квадрата одинакова. Это должно быть важной подсказкой что есть проблема с дизайном. Однако есть способ обойти проблему. Мы могли бы переопределить SetWidth и SetHeight [...]

Но рассмотрим следующую функцию:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

Если мы передадим ссылку на объект Square в эту функцию, Square объект будет поврежден, потому что высота не будет изменена. Это явное нарушение LSP. Функция не работает для производных от его аргументов.

[...]

Ответ 6

LSP необходим, когда какой-то код считает, что он вызывает методы типа T и может неосознанно вызвать методы типа S, где S extends T (т.е. S наследует, выводит из или является подтипом супертипа T).

Например, это происходит там, где функция с входным параметром типа T вызывается (т.е. вызывается) со значением аргумента типа S. Или, где идентификатор типа T, присваивается значение типа S.

val id : T = new S() // id thinks it a T, but is a S

LSP требует, чтобы ожидания (т.е. инварианты) для методов типа T (например, Rectangle) не нарушались, когда вместо этого называются методы типа S (например, Square).

val rect : Rectangle = new Square(5) // thinks it a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

Даже тип с неизменяемыми полями по-прежнему имеет инварианты, например. неизменяемые сеттеры Rectangle ожидают, что размеры будут независимо изменены, но неизменные квадратные сеттеры нарушают это ожидание.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP требует, чтобы каждый метод подтипа S должен иметь контравариантный входной параметр и ковариантный вывод.

Контравариантный означает, что дисперсия противоречит направлению наследования, т.е. тип Si каждого входного параметра каждого метода подтипа S должен быть таким же или супертипом типа Ti соответствующего входного параметра соответствующего метода супертипа T.

Ковариация означает, что дисперсия находится в одном направлении наследования, т.е. тип So вывода каждого метода подтипа S должен быть таким же или подтипом типа To соответствующий выход соответствующего метода супертипа T.

Это связано с тем, что если вызывающий объект имеет тип T, он считает, что он вызывает метод T, тогда он поставляет аргументы типа Ti и назначает вывод типу To. Когда он фактически вызывает соответствующий метод S, каждый входной аргумент Ti присваивается входному параметру Si, а выход So присваивается типу To. Таким образом, если Si не были контравариантными w.r.t. до Ti, тогда подтест Xi, который не был бы подтипом Si, может быть назначен Ti.

Кроме того, для языков (например, Scala или Ceylon), которые содержат аннотации вариации на основе определения по параметрам полиморфизма типа (т.е. дженерики), совпадение или противоречие аннотации дисперсии для каждого параметра типа типа T должен быть напротив или в том же направлении соответственно для каждого входного параметра или вывода (каждого метода T), который имеет тип параметра типа.

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


Подтипирование является подходящим, где инварианты могут быть перечислены.

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

Typestate (см. стр. 3) объявляет и применяет инварианты состояния, ортогональные типу. В качестве альтернативы, инварианты могут выполняться с помощью преобразования утверждений в типы. Например, чтобы утверждать, что файл открыт до его закрытия, File.open() может возвращать тип OpenFile, который содержит метод close(), который недоступен в файле. A tic-tac-toe API может быть еще одним примером использования ввода для принудительного применения инвариантов во время компиляции. Система типа может даже быть завершена Тьюрингом, например. Scala. Зависимые языки и теоретические прообразы формализуют модели типизации более высокого порядка.

Из-за необходимости в семантике abstract over extension, я ожидаю, что использование ввода для моделирования инвариантов, т.е. унифицированная денотационная семантика более высокого порядка, превосходит Typestate. "Расширение означает неограниченный, перестановленный состав нескоординированной модульной разработки. Поскольку мне кажется, что это антитеза объединения и, следовательно, степеней свободы, иметь две взаимозависимые модели (например, типы и Typestate) для выражения общей семантики, которые не могут быть унифицированы друг с другом для растяжимого состава, Например, Проблема с выражением -образное расширение было унифицировано в подтипировании, перегрузке функций и параметрических областях ввода.

Моя теоретическая позиция заключается в том, что для знания существуют (см. раздел" Централизация слепа и непригодна "), никогда не будет общего модель, которая может обеспечить 100% охват всех возможных инвариантов на полном компьютерном языке Turing. Для того, чтобы знание существовало, существуют неожиданные возможности, т.е. Беспорядок и энтропия всегда должны возрастать. Это энтропийная сила. Чтобы доказать все возможные вычисления потенциального расширения, необходимо вычислить априори все возможные расширения.

Вот почему существует теорема о прекращении, т.е. неразрешима, прекращается ли всякая возможная программа на языке программирования Turing. Можно доказать, что какая-то конкретная программа завершается (одна из которых все возможности определены и вычислены). Но невозможно доказать, что все возможные расширения этой программы завершаются, если только возможности для расширения этой программы не являются Тьюрингом законченными (например, посредством зависимой типизации). Поскольку фундаментальным требованием для полноты Turing является неограниченная рекурсия, интуитивно понятно, как теоремы о неполноте Гёделя и парадокс Рассела применимы к расширению.

Интерпретация этих теорем включает их в обобщенное концептуальное понимание энтропийной силы:

  • Теоремы о неполноте Гёделя: любая формальная теория, в которой все арифметические истины могут быть доказаны, противоречива.
  • Парадокс Рассела: каждое правило членства для набора, которое может содержать набор, либо перечисляет конкретный тип каждого члена или содержит себя. Таким образом, наборы либо не могут быть расширены, либо они являются неограниченной рекурсией. Например, набор всего, что не является чайником, включает в себя себя, включающий в себя себя, включающий в себя себя и т.д. Таким образом, правило несовместимо, если оно (может содержать набор и) не перечисляет конкретные типы (т.е. Разрешает все неопределенные типы) и не допускает неограниченное расширение. Это набор множеств, которые не являются сами собой. Эта неспособность быть последовательной и полностью перечислимой по всему возможному расширению, является теоремами неполноты Гёделя.
  • Принцип логики Лискова: обычно неразрешимой проблемой является ли любое множество подмножеством другого, т.е. наследование, как правило, неразрешимо.
  • Линские ссылки: невозможно решить, что такое вычисление, когда оно описано или воспринимается, т.е. восприятие (реальность) не имеет абсолютной точки отсчета.
  • Теорема Коуза: нет внешней контрольной точки, поэтому любой барьер для неограниченных внешних возможностей потерпит неудачу.
  • Второй закон термодинамики: целая вселенная (замкнутая система, т.е. все), тенденции к максимальному беспорядку, т.е. максимальные независимые возможности.

Ответ 7

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

В псевдо-питоне

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

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

Ответ 8

Функции, которые используют указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.

Когда я впервые прочитал о LSP, я предположил, что это подразумевается в очень строгом смысле, по существу, приравнивая его к реализации интерфейса и тис-безопасному литию. Это означало бы, что LSP обеспечивается или нет самим языком. Например, в этом строгом смысле, ThreeDBoard, безусловно, подлежит замене для Board, насколько это касается компилятора.

После ознакомления с концепцией, хотя я обнаружил, что LSP обычно интерпретируется более широко, чем это.

Короче говоря, что означает, что клиентский код "знает", что объект за указателем имеет производный тип, а не тип указателя, не ограничивается безопасностью типа. Соблюдение LSP также можно проверить, исследуя фактическое поведение объектов. То есть, исследуя влияние аргументов состояния объекта и аргументов метода на результаты вызовов метода или типы исключений, выведенных из объекта.

Возвращаясь к примеру еще раз, в теории, можно использовать методы Board, чтобы нормально работать на ThreeDBoard. На практике, однако, будет очень сложно предотвратить различия в поведении, которые клиент может не обрабатывать должным образом, не уклоняясь от функциональности, которую должен добавить ThreeDBoard.

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

Ответ 9

Важным примером использования LSP является тестирование программного обеспечения.

Если у меня есть класс A, который является LSP-совместимым подклассом B, то я могу повторно использовать набор тестов B для тестирования A.

Чтобы полностью проверить подкласс A, мне, вероятно, нужно добавить еще несколько тестовых примеров, но, как минимум, я могу повторно использовать все тестовые примеры суперкласса B.

Способ реализовать это, построив то, что МакГрегор называет "параллельной иерархией для тестирования": класс ATest наследует от BTest. Затем необходима некоторая форма инъекции, чтобы гарантировать, что тестовый пример работает с объектами типа A, а не с типом B (шаблон простой шаблонной модели будет выполнен).

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

См. также ответ на вопрос Stackoverflow "Могу ли я реализовать серию повторных тестов для тестирования реализации интерфейса?

Ответ 10

Существует список проверок, чтобы определить, нарушаете ли вы Лискова.

  • Если вы нарушите один из следующих пунктов → вы нарушаете Лискова.
  • Если вы не нарушаете никаких → не можете ничего сделать.

Контрольный список:

  • Никаких новых исключений не должно быть выбрано в производном классе. Если ваш базовый класс бросил ArgumentNullException, то вашим подклассам разрешалось исключать исключения типа ArgumentNullException или любые исключения, полученные из ArgumentNullException. Бросок IndexOutOfRangeException является нарушением Лискова.
  • Предварительные условия не могут быть усилены.. Предположим, что ваш базовый класс работает с элементом int. Теперь ваш подтип требует, чтобы int был положительным. Это укрепили предварительные условия, и теперь любой код, который отлично работал с отрицательными ints, был нарушен.
  • Пост-условия не могут быть ослаблены.. Предположим, что ваш базовый класс требует, чтобы все соединения с базой данных были закрыты до возвращения метода. В вашем подклассе вы переопределили этот метод и литое соединение для дальнейшего повторного использования. Вы ослабили пост-условия этого метода.
  • Инварианты должны быть сохранены: Самое трудное и болезненное ограничение для выполнения. Инварианты некоторое время скрыты в базовом классе, и единственный способ их раскрыть - прочитать код базового класса. В основном вы должны быть уверены, что когда вы переопределяете метод, любое неизменяемое должно оставаться неизменным после выполнения вашего переопределенного метода. Самое лучшее, что я могу придумать, - это принудительное применение этих инвариантных ограничений в базовом классе, но это будет непросто.
  • Ограничение истории. При переопределении метода вам не разрешается изменять немодифицируемое свойство в базовом классе. Взгляните на этот код, и вы можете видеть, что имя определено как un-modifiable (private set), но SubType вводит новый метод, который позволяет его модифицировать (через отражение):

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

Есть еще 2 пункта: Контравариантность аргументов метода и Ковариация типов возврата. Но это невозможно в С# (я разработчик С#), поэтому я не забочусь о них.

Ссылка:

Ответ 11

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

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

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

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

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

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

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

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

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

Ответ 12

Странно, никто не опубликовал оригинальную статью, которая описывала lsp. Это нелегко читать, как Роберт Мартин, но стоит того.

Ответ 13

Короче говоря, давайте оставим прямоangularьники прямоangularьники и квадраты, практический пример при расширении родительского класса, вы должны либо СОХРАНИТЬ точный родительский 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/

Ответ 14

Я вижу прямоугольники и квадраты в каждом ответе, и как нарушать 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 !
    }
}

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

Ответ 15

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

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

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

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

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

Ответ 16

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

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

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

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

Ответ 17

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

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

Ответ 18

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

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

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

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

Пример:

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

    // Violation of Likov 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 a rectangle.
        // It assumes that he able to set the width and height as for the base
        // class

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

Заключение:

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

Смотрите также: принцип Open Close

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

Ответ 19

Давайте проиллюстрируем на 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() { ... }
}

Ответ 20

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

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

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

Ответ 21

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

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

Ответ 22

Скажем, мы используем прямоугольник в нашем коде

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

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

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

Если мы заменим Rectangle на Square в нашем первом коде, то он сломается:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

Это связано с тем, что Square имеет новое предварительное условие, которое у нас не было в классе Rectangle: width == height. Согласно LSP экземпляры Rectangle должны быть заменены экземплярами подкласса Rectangle. Это происходит потому, что эти экземпляры передают проверку типа для экземпляров Rectangle, и поэтому они будут вызывать непредвиденные ошибки в вашем коде.

Это был пример того, что "предварительные условия не могут быть усилены в подтипе" в статье wiki. Итак, чтобы подвести итог, нарушение LSP, вероятно, вызовет ошибки в вашем коде в какой-то момент.

Ответ 23

Я рекомендую вам прочитать статью: Нарушение принципа замены Лискова (LSP).

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

Ответ 24

Наиболее ясное объяснение LSP, которое я нашел до сих пор, было "Принцип замещения Лискова говорит, что объект производного класса должен иметь возможность заменить объект базового класса без каких-либо ошибок в системе или изменения поведения базовый класс" из здесь. В статье приведен пример кода для нарушения LSP и его фиксации.

Ответ 25

ПРИНЦИП ПРИМЕНЕНИЯ LISKOV (из книги Марка Семанна) гласит, что мы должны иметь возможность заменить одну реализацию интерфейса на другую, не нарушая ни клиент, ни реализацию. Этот принцип позволяет учитывать требования, которые происходят в будущем, даже если мы не можем предвидеть их сегодня.

Если мы отсоединяем компьютер от стены (Реализация), ни сетевая розетка (Интерфейс), ни компьютер (Клиент) не разрушаются (фактически, если его портативный компьютер может даже работать на своих батареях в течение определенного периода времени времени). Однако с программным обеспечением клиент часто ожидает, что услуга будет доступна. Если служба была удалена, мы получаем исключение NullReferenceException. Чтобы справиться с такой ситуацией, мы можем создать реализацию интерфейса, который ничего не делает. Это шаблон проектирования, называемый Null Object, [4], и это примерно соответствует отключению компьютера от стены. Поскольку мы использовали свободную связь, мы можем заменить реальную реализацию тем, что ничего не делает, не вызывая проблем.

Ответ 26

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

Класс ребенка не должен:

  • Удалить поведение базового класса
  • Нарушить инварианты базового класса

Я хотел бы добавить пример кода для решения Liskov Substitution Principal

Проблема

В математике a Square есть a Rectangle. Действительно, это специализация прямоугольника. "Is a" заставляет вас хотеть моделировать это с наследованием. Однако если в коде вы сделали Square вывод из Rectangle, тогда a Square должен использоваться везде, где вы ожидаете Rectangle. Это вызывает какое-то странное поведение.

Представьте, что у вас были SetWidth и SetHeight методы в базовом классе Rectangle; это кажется совершенно логичным. Однако, если ваша ссылка Rectangle указала на Square, то SetWidth и SetHeight не имеет смысла, потому что установка одна изменит другую, чтобы соответствовать ей. В этом случае Square не выполняется тест замены Лискова с помощью Rectangle, а абстракция наличия Square наследуемого от Rectangle является плохим.

Для решения этого вопроса мы имеем два подхода

  • Используя If условие
  • Используя ключевое слово abstract и override

Первый подход с использованием условия If

  • Этот подход решает Liskov Substitution Principal, но нарушает Open closed principle

    [TestMethod]
    public void TwentyFor4X5ShapeFromRectangleAnd9For3X3Square()
    {
        var shapes = new List<Shape>
                         {
                             new Rectangle {Height = 4, Width = 5},
                             new Square {SideLength = 3}
                         };
        var areas = new List<int>();
        foreach (Shape shape in shapes)
        {
            if (shape.GetType() == typeof(Rectangle))
            {
                areas.Add(((Rectangle)shape).Area());
            }
            if (shape.GetType() == typeof(Square))
            {
                areas.Add(((Square)shape).Area());
            }
        }
        Assert.AreEqual(20, areas[0]);
        Assert.AreEqual(9, areas[1]);
    }
    
    public class Rectangle : Shape
    {
        public int Height { get; set; }
        public int Width { get; set; }
    
        public int Area()
        {
            return Height * Width;
        }
    }
    
    public abstract class Shape
    {
    }
    
    public class Square : Shape
    {
        public int SideLength;
    
        public int Area()
        {
            return SideLength * SideLength;
        }
    }
    

Второй и лучший подход с использованием abstract и override

    [TestMethod]
    public void TwentyFor4X5ShapeFromRectangleAnd9For3X3Square()
    {
        var shapes = new List<Shape>
                         {
                             new Rectangle {Height = 4, Width = 5},
                             new Square {SideLength = 3}
                         };
        var areas = new List<int>();
        foreach (Shape shape in shapes)
        {
            areas.Add(shape.Area());
        }
        Assert.AreEqual(20, areas[0]);
        Assert.AreEqual(9, areas[1]);
    }

    public abstract class Shape
    {
        public abstract int Area();
    }

    public class Rectangle : Shape
    {
        public int Height { get; set; }
        public int Width { get; set; }

        public override int Area()
        {
            return Height*Width;
        }
    }
    public class Square : Shape
    {
        public int SideLength;

        public override int Area()
        {
            return SideLength*SideLength;
        }
    }

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

Ответ 27

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

Intent - Производные типы должны быть полностью заменены, чтобы их базовые типы.

Пример. Ковариантные типы возврата в java.

Ответ 28

LSP говорит, что "объекты должны быть заменены их подтипами". С другой стороны, этот принцип указывает на

Дочерние классы никогда не должны нарушать определения типов родительских классов.

и следующий пример помогает лучше понять LSP.

Без LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn't rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Фиксация по LSP:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Ответ 29

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

  • Переопределенный метод не должен оставаться пустым
  • Переопределенный метод не должен выдавать ошибку
  • Поведение базового класса или интерфейса не должно изменяться (переделываться), как из-за поведения производного класса.