Каков пример принципа подстановки Лискова?

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

26 ответов


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

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

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

вы все должны проверить другой бесценный твердые принципы, Мотивационные плакаты.


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

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

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

самый эффективный способ я видел, чтобы проиллюстрировать этот момент был в голова первая 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.


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.


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

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

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

один из самых вопиющих нарушений этого принципа является использование C++ Сведения о типе времени выполнения (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 класс, и его необходимо изменить всякий раз, когда новые производные есть. Действительно, многие рассматривают структуру этой функции как анафему объектно-ориентированному дизайну.

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

однако, есть и другие, гораздо более тонкие способы нарушения 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 объект в эту функцию,


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

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

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

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

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

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

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 должен иметь контравариантный входной параметр(Ы) и ковариантный выход.

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

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

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

дополнительно, для языков (например, Scala или Цейлон), которые имеют аннотации дисперсии определения сайта по параметрам полиморфизма типа (т. е. дженерики), co - или contra - направление аннотации дисперсии для каждого параметра типа типа T должно быть напротив или же направление соответственно для каждого входного параметра или выхода (каждого метода T), который имеет тип параметра type.

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


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

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

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

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

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

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

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

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

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

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

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

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

утка может летать из-за своей птицы, но как насчет этого:

public class Ostrich extends Bird{}

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

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

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

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

в псевдо-python

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

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

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


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

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

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

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

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

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


странно, никто не выложил оригинал статьи это описало lsp. Это не так легко читать, как Роберта Мартина, но стоит того.


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

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

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

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

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

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


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

  • если вы нарушаете один из следующих пунктов -> вы нарушаете Лисков.
  • если вы не нарушаете какие-либо -> не могу сделать вывод.

проверяем список:

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

    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 элемента:контравариантность аргументов метода и ковариантность возвращаемых типов. Но это невозможно в C# (я разработчик C#), поэтому я не забочусь о них.

ссылка:


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

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

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

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

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

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

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

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

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


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

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

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

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

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


долго история короче говоря, давайте оставим прямоугольники прямоугольники и квадраты квадраты, практический пример при расширении родительского класса, вы должны либо сохранить точный Родительский 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 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 :(
    }
}

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

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

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

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


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

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


квадрат-это прямоугольник, ширина которого равна высоте. Если квадрат устанавливает два разных размера для ширины и высоты, он нарушает инвариант квадрата. Это работает вокруг путем вводить побочные эффекты. Но если прямоугольник имел setSize (height, width) с предварительным условием 0

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


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

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


была бы полезна реализация ThreeDBoard с точки зрения массива платы?

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

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


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

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


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


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

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


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

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 экземпляры, и поэтому они вызовут непредвиденные ошибки в вашем коде.

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


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

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

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

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

пример:

Ниже приведен классический пример, для которого нарушается принцип подстановки Лискова. В примере используются 2 класса: Rectangle и Square. Предположим, что используется объект Rectangle где-то в приложении. Мы расширяем приложение и добавляем класс Square. Класс 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.
    }
}

вывод:

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

Читайте также: Открыть-Закрыть Принципа

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


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

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

Example-Co-variant возвращаемые типы в java.


вот выдержка из этот пост это хорошо проясняет ситуацию:

[..] чтобы понять некоторые принципы, важно понять, когда они были нарушены. Вот что я сейчас сделаю.

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

рассмотрим следующий пример:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

это нарушение LSP? Да. Это потому, что контракт счета говорит нам, что счет будет снята, но это не всегда так. Итак, что я должен сделать, чтобы исправить это? Я просто изменяю контракт:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

вуаля, теперь контракт довольны.

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

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

и это автоматически нарушает принцип open-closed [то есть требование вывода денег. Потому что никогда не знаешь, что случится, если у объекта, нарушающего договор, не будет достаточно денег. Вероятно, он просто ничего не возвращает, вероятно, будет создано исключение. Поэтому вы должны проверить, если это hasEnoughMoney() - это не часть интерфейса. Так это заставило проверка, зависящая от конкретного класса, является нарушением OCP].

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