Я слышал, что принцип замещения Лискова (LSP) является фундаментальным принципом объектно-ориентированного проектирования. Что это такое и каковы примеры его использования?
Каков пример принципа замещения Лискова?
Ответы (33)
Отличный пример, иллюстрирующий LSP (приведенный дядей Бобом в подкасте, который я недавно слышал), - это то, как иногда что-то, что звучит правильно на естественном языке, не совсем работает в коде.
В математике Square
- это Rectangle
. На самом деле это специализация прямоугольника. «Это» заставляет вас смоделировать это с помощью наследования. Однако если в коде вы сделали Square
производным от Rectangle
, тогда Square
можно использовать везде, где вы ожидаете Rectangle
. Это вызывает странное поведение.
Представьте, что у вас есть методы SetWidth
и SetHeight
в вашем Rectangle
базовом классе; это кажется совершенно логичным. Однако, если ваша Rectangle
ссылка указывает на Square
, тогда SetWidth
и SetHeight
не имеют смысла, потому что установка одного изменит другое, чтобы оно соответствовало ему. В этом случае Square
не проходит тест подстановки Лискова с Rectangle
, и абстракция о том, что Square
наследуется от Rectangle
, является плохой.
Вы все должны проверить другие бесценные Мотивационные плакаты с принципами SOLID.
Square.setWidth(int width)
был реализован так: this.width = width; this.height = width;
? В этом случае гарантируется, что ширина равна высоте.
- person MC Emperor; 28.10.2015
setHeight()
и setWidth()
в Square
, поэтому те места в вашем коде, где вы используете Rectangule
, больше не будут работать, если вы передадите Square
, и это основной момент в LSP;
- person sdlins; 26.12.2016
h1
с ошибкой при установлении соединения с базой данных. Я нашел последний снимок WayBack Machine, и в нем четко указано, что изображения лицензированы CC BY-SA. Я предлагаю вам связать снимок WayBack Machine, а также исходный URL-адрес и загрузить изображение на серверы Stack Overflow (через редактор). Таким образом, перебои в работе почты не повлияют.
- person Palec; 10.06.2017
o.setDimensions(width, height)
решит эту проблему, но LSP все равно будет нарушен, потому что квадрат имеет более строгие предварительные условия (width == height
), чем прямоугольник. Я не думаю, что ваш пост что-то отвечает о LSP.
- person inf3rno; 09.10.2017
GetHeight
и GetWidth
, если они собираются делать то же самое. Это может быть крошечной проблемой, но все же причина сделать это по-другому.
- person AustinWBryan; 09.05.2018
SetLength
, который может внутренне вызывать SetWidth
и SetHeight
- person alancalvitti; 29.03.2019
Принцип замены Лискова (LSP, lsp) - это концепция объектно-ориентированного программирования, которая гласит:
Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.
По сути, LSP касается интерфейсов и контрактов, а также того, как решить, когда расширять класс или использовать другую стратегию, например композицию, для достижения вашей цели.
Самый эффективный способ проиллюстрировать этот момент, который я видел, - это Head First OOA & D. Они представляют собой сценарий, в котором вы являетесь разработчиком проекта по созданию основы для стратегических игр.
Они представляют собой класс, представляющий доску, которая выглядит следующим образом:
Все методы принимают координаты X и Y в качестве параметров для определения положения плитки в двумерном массиве Tiles
. Это позволит разработчику игры управлять юнитами на доске во время игры.
В книге также изменяются требования и говорится, что игровая структура также должна поддерживать трехмерные игровые доски, чтобы соответствовать играм, в которых есть полет. Итак, представлен 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.
GetUnits(int x, int y)
на GetUnits(Position pos)
, и то же самое касается других функций. Таким образом, это не нарушит LSP. Поправьте меня если я ошибаюсь.
- person du369; 22.04.2020
Заменяемость - это принцип объектно-ориентированного программирования, согласно которому в компьютерной программе, если S является подтипом T, тогда объекты типа T могут быть заменены объектами типа S.
Давайте сделаем простой пример на Java:
Плохой пример
public class Bird{
public void fly(){}
}
public class Duck extends Bird{}
Утка умеет летать, потому что это птица, а как насчет этого:
public class Ostrich extends Bird{}
Страус - это птица, но он не может летать, класс Ostrich является подтипом класса Bird, но он не должен иметь возможность использовать метод fly, это означает, что мы нарушаем принцип LSP.
Хороший пример
public class Bird{}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
Bird bird
. Вы должны передать объект в FlyingBirds, чтобы использовать fly, что нехорошо, правда?
- person Moody; 20.11.2017
Bird bird
, это означает, что он не может использовать fly()
. Вот и все. Передача Duck
этого факта не меняет. Если клиент имеет FlyingBirds bird
, то даже если он получит Duck
, он всегда должен работать одинаково.
- person Steve Chamaillard; 18.02.2018
LSP касается инвариантов.
Классический пример дается следующим объявлением псевдокода (реализации опущены):
class Rectangle {
int getHeight()
void setHeight(int value) {
postcondition: width didn’t change
}
int getWidth()
void setWidth(int value) {
postcondition: height didn’t change
}
}
class Square extends Rectangle { }
Теперь у нас проблема, хотя интерфейс совпадает. Причина в том, что мы нарушили инварианты, вытекающие из математического определения квадратов и прямоугольников. Как работают геттеры и сеттеры, Rectangle
должен удовлетворять следующему инварианту:
void invariant(Rectangle r) {
r.setHeight(200)
r.setWidth(100)
assert(r.getHeight() == 200 and r.getWidth() == 100)
}
Однако этот инвариант (а также явные постусловия) должен нарушаться правильной реализацией Square
, поэтому он не является допустимой заменой Rectangle
.
Square
?
- person ca9163d9; 24.01.2012
setHeight()
и setWidth()
, вероятно, не должны существовать, потому что это делает класс изменяемым. Однако, если они существуют, можно было бы просто throw NotSupportedException() / NotImplementedException()
или что-то подобное.
- person Leonid; 03.07.2012
square
это не всегда будет истинным, оба установщика должны сбросить и ширину, и высоту, иначе они не сохранят (подразумеваемые) инварианты квадрата. Другими словами: у вас несовместимая система типов, и вы никогда этого не захотите.
- person Konrad Rudolph; 06.06.2013
SetArea(4); SetPerimeter(10);
сделает модель обычного прямоугольника прямоугольником 1x4. Вызовы сделают модель класса Square
сначала 2x2, а затем либо изменится на квадрат sqrt (10) xsqrt (10), либо выбросят InvalidOperationException
или что-то в этом роде.
- person Wolfzoon; 07.08.2016
SetDepth(int d) => throw exception
. Но мне интересно ... прямоугольник - это особый тип квадрата, который не требует H = W? В качестве альтернативы .. Четырехугольник ⇒ Трапеция ⇒ Параллелограмм.
- person Paulustrious; 05.08.2017
Shape
или Parallelogram
или чего-то такого?
- person AustinWBryan; 09.05.2018
Square
из Rectangle
, если полиморфный подход не является обязательным, я бы поддержал SquareDecorator
, который принимает объект Rectangle
. Что вы или кто-либо другой думаете об этом?
- person Reuel Ribeiro; 11.01.2019
Shape
. Тем не менее, ваш подход, безусловно, является правильным решением во многих реальных случаях использования.
- person Konrad Rudolph; 11.01.2019
Rectangle
базового класса утверждения, которые гарантируют, что другое значение не будет изменено. Но тот факт, что у квадратов есть другой неявный контракт, именно поэтому это нарушение LSP: оба действительны изолированно, но квадрат не является допустимым подтипом прямоугольника, потому что их неявные контракты не т совместим.
- person Konrad Rudolph; 13.10.2020
Rectangle
. Но справедливое предупреждение: я могу в конечном итоге удалить их снова, потому что они искусственные и, во всяком случае, уже зафиксированы invariant
методом примера. Более того, LSP по своему первоначальному определению также касается неявных инвариантов, а не только явных. Исходный пример без постусловий очень соответствовал исходному формальному определению LSP.
- person Konrad Rudolph; 13.10.2020
setHeight()
и setWidth()
являются независимыми операциями, каждая сама по себе не является нарушением какого-либо инварианта. Только когда вы пытаетесь заставить независимые операции быть одной операцией, можно увидеть ваше представление о том, что вы считаете неявным инвариантом. Так что в лучшем случае это слабый случай нарушения LSP.
- person wired_in; 19.10.2020
У Роберта Мартина есть отличная статья о принцип замещения Лискова. В нем обсуждаются тонкие и не очень тонкие способы нарушения этого принципа.
Некоторые важные части статьи (обратите внимание, что второй пример сильно сжат):
Простой пример нарушения LSP
Одним из наиболее вопиющих нарушений этого принципа является использование информации о типе времени выполнения (RTTI) C ++ для выбора функции в зависимости от типа объекта. то есть:
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. Функция не работает для производных от своих аргументов.[...]
Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one.
Если предварительное условие дочернего класса сильнее, чем предварительное условие родительского класса, вы не сможете заменить родительского ребенка дочерним без нарушения предварительного условия . Следовательно, LSP.
- person user2023861; 11.02.2015
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
Даже тип с неизменяемыми полями все еще имеет инварианты, например Установщики immutable Rectangle ожидают, что размеры будут изменены независимо, но установщики immutable Square нарушают это ожидание.
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
не были контравариантными по отношению к к Ti
, тогда подтип Xi
- который не будет подтипом Si
- может быть назначен Ti
.
Кроме того, для языков (например, Scala или Ceylon), которые имеют аннотации вариации сайта определения для параметров полиморфизма типов (т. Е. Универсальных), совместное или противоположное направление аннотации вариации для каждого параметра типа типа T
должно быть противоположное или то же направление, соответственно, для каждого входного параметра или выхода (каждого метода T
), имеющего тип параметра типа.
Кроме того, для каждого входного параметра или выхода, имеющего тип функции, требуемое направление отклонения меняется на противоположное. Это правило применяется рекурсивно.
Подходящее выделение подтипов, где можно перечислить инварианты.
В настоящее время ведется много исследований о том, как моделировать инварианты, чтобы они выполнялись компилятором.
Typestate (см. стр. 3) объявляет и применяет инварианты состояний, ортогональные типу. Кроме того, инварианты можно принудительно применить путем преобразования утверждений в типы. Например, чтобы утверждать, что файл открыт перед его закрытием, File.open () может вернуть тип OpenFile, который содержит метод close (), который недоступен в File. Еще одним примером может служить tic-tac-toe API. использования типизации для обеспечения соблюдения инвариантов во время компиляции. Система типов может быть даже полной по Тьюрингу, например Scala. Языки с зависимой типизацией и средства доказательства теорем формализуют модели типизации более высокого порядка.
Из-за необходимости в семантике абстрагироваться над расширением, я ожидаю, что использование типизации для моделирования инвариантов, то есть унифицированной денотационной семантики более высокого порядка, превосходит Typestate. «Расширение» означает неограниченную, постоянно меняющуюся композицию нескоординированного модульного развития. Потому что мне кажется антитезой унификации и, следовательно, степеней свободы, иметь две взаимозависимые модели (например, типы и Typestate) для выражения общей семантики, которые не могут быть объединены друг с другом для расширяемой композиции. . Например, Expression Problem -подобное расширение было объединено в подтипах, перегрузке функций и параметрической типизации. домены.
Моя теоретическая позиция состоит в том, что для существования знания ( см. раздел «Централизация слепая и непригодная»), никогда не будет общей модели, которая могла бы обеспечить 100% покрытие всех возможных инвариантов на полном по Тьюрингу компьютерном языке. Для существования знания существует много неожиданных возможностей, то есть беспорядок и энтропия всегда должны возрастать. Это энтропийная сила. Доказать все возможные вычисления потенциального расширения - значит вычислить априори все возможные расширения.
Вот почему существует теорема об остановке, то есть невозможно решить, завершается ли всякая возможная программа на языке программирования, полном по Тьюрингу. Можно доказать, что завершается некоторая конкретная программа (все возможности которой определены и вычислены). Но невозможно доказать, что все возможные расширения этой программы прекращаются, если только возможности расширения этой программы не являются полными по Тьюрингу (например, через зависимую типизацию). Поскольку фундаментальным требованием для полноты по Тьюрингу является неограниченная рекурсия, интуитивно понятно, как теоремы Гёделя о неполноте и парадокс Рассела применимы к расширению.
Интерпретация этих теорем включает их в общее концептуальное понимание энтропийной силы:
- Теоремы Гёделя о неполноте: любая формальная теория, в которой могут быть доказаны все арифметические истины, несовместима.
- парадокс Рассела: каждое правило членства для набор, который может содержать набор, либо перечисляет определенный тип каждого члена, либо содержит сам себя. Таким образом, множества либо не могут быть расширены, либо являются неограниченной рекурсией. Например, набор всего, что не является чайником, включает себя самого, которое включает себя, которое включает себя и т. Д. Таким образом, правило несовместимо, если оно (может содержать набор и) не перечисляет определенные типы (т. Е. Разрешает все неуказанные типы) и не допускает неограниченного расширения. Это набор наборов, которые не являются членами сами по себе. Эта неспособность быть одновременно непротиворечивой и полностью перечисленной по всем возможным расширениям является теоремой Гёделя о неполноте.
- Принцип подстановки Лискова: как правило, неразрешимая проблема, является ли какой-либо набор подмножеством другого, т.е. наследование, как правило, неразрешимо.
- Ссылка Лински: непонятно, что такое вычисление чего-либо, когда оно описывается или воспринимается, то есть восприятие (реальность) не имеет абсолютной точки отсчета.
- Теорема Коуза: нет внешней точки отсчета, поэтому любой барьер для неограниченных внешних возможностей потерпит неудачу.
- Второй закон термодинамики: вся Вселенная (замкнутая система, то есть все) стремится к максимальному беспорядку, то есть к максимальным независимым возможностям.
Я вижу прямоугольники и квадраты в каждом ответе, а также о том, как нарушить 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 !
}
}
Теперь подтипы нельзя использовать одинаково, поскольку они больше не дают одинакового результата.
Database::selectQuery
для поддержки только подмножества SQL, поддерживаемого всеми механизмами БД. Это вряд ли практично ... Тем не менее, пример все же легче понять, чем большинство других, используемых здесь.
- person Palec; 25.02.2018
Есть контрольный список, чтобы определить, нарушаете ли вы Лисков.
- Если вы нарушаете один из следующих пунктов - ›вы нарушаете Лисков.
- Если ничего не нарушишь - ›ничего не могу сделать.
Контрольный список:
Никакие новые исключения не должны создаваться в производном классе: если ваш базовый класс выдал исключение ArgumentNullException, тогда вашим подклассам было разрешено генерировать исключения только типа ArgumentNullException или любые исключения, производные от ArgumentNullException. Выброс IndexOutOfRangeException является нарушением Лискова.
Невозможно усилить предварительные условия: предположим, что ваш базовый класс работает с членом типа int. Теперь ваш подтип требует, чтобы int был положительным. Это усиленные предварительные условия, и теперь любой код, который раньше отлично работал с отрицательными целыми числами, сломан.
Постусловия не могут быть ослаблены: предположим, что ваш базовый класс требует, чтобы все соединения с базой данных были закрыты перед возвратом метода. В своем подклассе вы переопределили этот метод и оставили соединение открытым для дальнейшего использования. Вы ослабили пост-условия этого метода.
Инварианты должны быть сохранены: самое сложное и болезненное ограничение для выполнения. Инварианты иногда скрыты в базовом классе, и единственный способ их раскрыть - это прочитать код базового класса. По сути, вы должны быть уверены, что при переопределении метода что-либо неизменное должно оставаться неизменным после выполнения вашего переопределенного метода. Лучшее, что я могу придумать, - это применить эти инвариантные ограничения в базовом классе, но это будет непросто.
Ограничение истории: при переопределении метода вам не разрешается изменять неизменяемое свойство в базовом классе. Взгляните на этот код, и вы увидите, что Name определен как неизменяемый (частный набор), но 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); } }
Есть еще два элемента: Контравариантность аргументов метода и Ковариация возвращаемых типов. Но это невозможно в C # (я разработчик C #), поэтому меня это не волнует.
Короче говоря, давайте оставим прямоугольники, прямоугольники и квадраты, квадраты. Практический пример расширения родительского класса, вы должны СОХРАНИТЬ точный родительский 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/
LSP - это правило о контракте классов: если базовый класс удовлетворяет контракту, то производные классы LSP также должны удовлетворять этому контракту.
В псевдо-питоне
class Base:
def Foo(self, arg):
# *... do stuff*
class Derived(Base):
def Foo(self, arg):
# *... do stuff*
удовлетворяет LSP, если каждый раз, когда вы вызываете Foo для производного объекта, он дает точно такие же результаты, как и вызов Foo для базового объекта, если arg остается таким же.
2 + "2"
). Возможно, вы путаете строго типизированный со статическим типом?
- person asmeurer; 20.01.2013
u'hello'
с 'world'
), а иногда и означать одну из многих других вещей. en.wikipedia.org/wiki/Strong_and_weak_typing
- person Mark Amery; 27.12.2013
2 + '2'
, он все равно не будет поддерживать 2 + 'two'
.
- person asmeurer; 27.12.2013
Я предполагаю, что все как бы понимали, что такое LSP технически: вы в основном хотите иметь возможность абстрагироваться от деталей подтипа и безопасно использовать супертипы.
Итак, у Лискова есть 3 основных правила:
Правило подписи: синтаксически должна существовать допустимая реализация каждой операции супертипа в подтипе. Что-то компилятор сможет проверить за вас. Есть небольшое правило о том, чтобы генерировать меньше исключений и быть по крайней мере таким же доступным, как методы супертипа.
Правило методов: реализация этих операций семантически корректна.
- Weaker Preconditions : The subtype functions should take at least what the supertype took as input, if not more.
- Более строгие постусловия: они должны производить подмножество вывода, созданного методами супертипа.
Правило свойств: это выходит за рамки отдельных вызовов функций.
- Invariants : Things that are always true must remain true. Eg. a Set's size is never negative.
- Эволюционные свойства: обычно что-то связано с неизменяемостью или типом состояний, в которых может находиться объект. Или, может быть, объект только растет и никогда не сжимается, поэтому методы подтипа не должны этого делать.
Все эти свойства необходимо сохранить, а дополнительные функции подтипа не должны нарушать свойства супертипа.
Если позаботиться об этих трех вещах, вы абстрагировались от лежащих в основе вещей и пишете слабо связанный код.
Источник: Разработка программ на Java - Барбара Лисков.
Проиллюстрируем на 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() { ... }
}
И расширите TransportationDevice для моторизованных устройств. Здесь более уместно добавить объект Engine.
class DevicesWithEngines extends TransportationDevice
{
Engine engine;
Engine getEngine() { ... }
void setEngine(Engine e) { ... }
void startEngine() { ... }
}
Таким образом, наш класс Car становится более специализированным, при этом соблюдая принцип замещения Лискова.
class Car extends DevicesWithEngines
{
@Override
void startEngine() { ... }
}
И наш класс велосипедов также соответствует принципу замены Лискова.
class Bicycle extends DevicesWithoutEngines
{
@Override
void startMoving() { ... }
}
Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом.
Когда я впервые прочитал о LSP, я предположил, что это имелось в виду в очень строгом смысле, по существу приравнивая его к реализации интерфейса и типобезопасному приведению типов. Это означало бы, что LSP либо обеспечивается самим языком, либо нет. Например, в этом строгом смысле ThreeDBoard, безусловно, может быть заменен на Board в том, что касается компилятора.
Прочитав больше о концепции, я обнаружил, что LSP обычно интерпретируется шире.
Короче говоря, то, что означает для клиентского кода «знать», что объект, стоящий за указателем, имеет производный тип, а не тип указателя, не ограничивается типобезопасностью. Соблюдение LSP также можно проверить, исследуя фактическое поведение объектов. То есть изучение влияния состояния объекта и аргументов метода на результаты вызовов методов или типы исключений, создаваемых объектом.
Возвращаясь снова к примеру, теоретически методы Board можно заставить нормально работать на ThreeDBoard. Однако на практике будет очень сложно предотвратить различия в поведении, которые клиент может неправильно обработать, не ограничивая функциональные возможности, которые должен добавить ThreeDBoard.
Обладая этими знаниями, оценка соблюдения LSP может стать отличным инструментом для определения того, когда композиция является более подходящим механизмом для расширения существующей функциональности, а не наследования.
Важным примером использования LSP является тестирование программного обеспечения.
Если у меня есть класс A, который является LSP-совместимым подклассом B, то я могу повторно использовать набор тестов B для тестирования A.
Чтобы полностью протестировать подкласс A, мне, вероятно, нужно добавить еще несколько тестовых примеров, но как минимум я могу повторно использовать все тестовые примеры суперкласса B.
Один из способов реализовать это - построить то, что МакГрегор называет «параллельной иерархией для тестирования»: мой ATest
класс унаследует от BTest
. Затем потребуется некоторая форма внедрения, чтобы убедиться, что тестовый пример работает с объектами типа A, а не типа B (подойдет простой шаблон метода).
Обратите внимание, что повторное использование набора супертестов для всех реализаций подкласса на самом деле является способом проверить, что эти реализации подкласса совместимы с LSP. Таким образом, можно также утверждать, что каждый должен запускать набор тестов суперкласса в контексте любого подкласса.
См. Также ответ на вопрос Stackoverflow «Могу ли я реализовать серию многоразовых тестов для проверки реализации интерфейса?»
В очень простом предложении мы можем сказать:
Дочерний класс не должен нарушать характеристики своего базового класса. Он должен уметь с этим справляться. Можно сказать, что это то же самое, что и подтипирование.
Принцип замещения Лискова
- Переопределенный метод не должен оставаться пустым.
- Переопределенный метод не должен вызывать ошибку
- Поведение базового класса или интерфейса не должно подвергаться модификации (переработке) из-за поведения производного класса.
Эта формулировка LSP слишком сильна:
Если для каждого объекта o1 типа S существует объект o2 типа T такой, что для всех программ P, определенных в терминах T, поведение P не меняется при замене o1 на o2, тогда S является подтипом T.
По сути, это означает, что S - это еще одна, полностью инкапсулированная реализация того же самого, что и T. И я мог бы быть смелым и решить, что производительность является частью поведения P ...
Таким образом, любое использование позднего связывания нарушает LSP. Весь смысл объектно-ориентированного подхода - получить другое поведение, когда мы заменяем объект одного вида одним другим!
Формулировка, процитированная в wikipedia, лучше, поскольку свойство зависит от контекста и не обязательно включает весь поведение программы.
Принцип замещения Лискова (LSP)
Все время мы проектируем программный модуль и создаем какие-то иерархии классов. Затем мы расширяем некоторые классы, создавая производные классы.
Мы должны убедиться, что новые производные классы просто расширяются, не заменяя функциональность старых классов. В противном случае новые классы могут вызвать нежелательные эффекты при использовании в существующих программных модулях.
Принцип замены Лискова гласит, что если программный модуль использует базовый класс, то ссылка на базовый класс может быть заменена на производный класс, не влияя на функциональность программного модуля.
Пример:
Ниже приведен классический пример нарушения принципа замещения Лискова. В примере используются 2 класса: прямоугольник и квадрат. Предположим, что объект 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.
}
}
Вывод:
Этот принцип является просто расширением принципа открытого и закрытого типа, и это означает, что мы должны убедиться, что новые производные классы расширяют базовые классы без изменения их поведения.
См. Также: Принцип открытого закрытия
Некоторые похожие концепции для лучшей структуры: Соглашение важнее конфигурации
Простыми словами, LSP утверждает, что объекты одного и того же суперклассы должны иметь возможность обмениваться друг с другом, ничего не нарушая.
Например, если у нас есть классы Cat
и Dog
, производные от класса Animal
, любые функции, использующие класс Animal, должны иметь возможность использовать Cat
или Dog
и вести себя нормально.
Этот принцип был введен Барбарой Лисков в 1987 году и расширяет принцип открытости-закрытости, фокусируясь на поведении суперкласса и его подтипов.
Его важность становится очевидной, если мы рассмотрим последствия его нарушения. Рассмотрим приложение, в котором используется следующий класс.
public class Rectangle
{
private double width;
private double height;
public double Width
{
get
{
return width;
}
set
{
width = value;
}
}
public double Height
{
get
{
return height;
}
set
{
height = value;
}
}
}
Представьте, что однажды клиенту потребуется умение манипулировать квадратами в дополнение к прямоугольникам. Поскольку квадрат является прямоугольником, класс Square должен быть производным от класса Rectangle.
public class Square : Rectangle
{
}
Однако при этом мы столкнемся с двумя проблемами:
Квадрату не нужны переменные высоты и ширины, унаследованные от прямоугольника, и это может привести к значительным потерям памяти, если нам придется создавать сотни тысяч квадратных объектов. Свойства установщика ширины и высоты, унаследованные от прямоугольника, не подходят для квадрата, поскольку ширина и высота квадрата идентичны. Чтобы установить одинаковое значение для высоты и ширины, мы можем создать два новых свойства следующим образом:
public class Square : Rectangle
{
public double SetWidth
{
set
{
base.Width = value;
base.Height = value;
}
}
public double SetHeight
{
set
{
base.Height = value;
base.Width = value;
}
}
}
Теперь, когда кто-то будет устанавливать ширину квадратного объекта, его высота соответственно изменится и наоборот.
Square s = new Square();
s.SetWidth(1); // Sets width and height to 1.
s.SetHeight(2); // sets width and height to 2.
Давайте продвинемся вперед и рассмотрим еще одну функцию:
public void A(Rectangle r)
{
r.SetWidth(32); // calls Rectangle.SetWidth
}
Если мы передадим в эту функцию ссылку на квадратный объект, мы нарушим LSP, потому что функция не работает для производных от своих аргументов. Свойства width и height не являются полиморфными, потому что они не объявлены виртуальными в прямоугольнике (квадратный объект будет поврежден, потому что высота не будет изменена).
Однако, объявив свойства установщика виртуальными, мы столкнемся с другим нарушением - OCP. Фактически, создание квадрата производного класса вызывает изменения в прямоугольнике базового класса.
A()
я думаю, вы имели в виду r.Width = 32;
, поскольку Rectangle
не имеет SetWidth()
метода.
- person wired_in; 13.10.2020
Некоторое дополнение:
Интересно, почему никто не написал об инварианте, предварительных условиях и условиях публикации базового класса, которым должны подчиняться производные классы. Чтобы производный класс D был полностью совместим с базовым классом B, класс D должен подчиняться определенным условиям:
- In-варианты базового класса должны быть сохранены производным классом
- Предварительные условия базового класса не должны усиливаться производным классом.
- Пост-условия базового класса не должны ослабляться производным классом.
Таким образом, производные должны знать о трех вышеуказанных условиях, налагаемых базовым классом. Следовательно, правила выделения подтипов определены заранее. Это означает, что отношение «IS A» должно соблюдаться только тогда, когда подтип соблюдает определенные правила. Эти правила в форме инвариантов, предварительных условий и постусловий должны быть определены в формальном 'контракте на проектирование '.
Дальнейшее обсуждение этого доступно в моем блоге: Принцип замены Лискова < / а>
Квадрат - это прямоугольник, ширина которого равна высоте. Если квадрат устанавливает два разных размера для ширины и высоты, он нарушает инвариант квадрата. Это обходится путем введения побочных эффектов. Но если у прямоугольника есть setSize (высота, ширина) с предварительным условием 0 ‹высоты и 0‹ ширины. Для метода производного подтипа требуется высота == ширина; более сильное предусловие (нарушающее lsp). Это показывает, что, хотя квадрат и является прямоугольником, он не является допустимым подтипом, поскольку предусловие усилено. Обход (в общем, плохая вещь) вызывает побочный эффект, и это ослабляет условие публикации (что нарушает lsp). setWidth на базе имеет условие поста 0 ‹ширину. Производное ослабляет его с высотой == шириной.
Следовательно, квадрат изменяемого размера не является прямоугольником изменяемого размера.
Будет ли такая полезная реализация ThreeDBoard в виде массива Board?
Возможно, вы захотите рассматривать срезы ThreeDBoard в различных плоскостях как доску. В этом случае вы можете абстрагироваться от интерфейса (или абстрактного класса) для Board, чтобы можно было реализовать несколько реализаций.
Что касается внешнего интерфейса, вы можете выделить интерфейс Board как для TwoDBoard, так и для ThreeDBoard (хотя ни один из вышеперечисленных методов не подходит).
Самым ясным объяснением LSP, которое я нашел до сих пор, было: «Принцип замены Лискова гласит, что объект производного класса должен иметь возможность заменять объект базового класса без каких-либо ошибок в системе или изменения поведения базового класса. "from здесь. В статье приводится пример кода нарушения LSP и его исправления.
Допустим, мы используем прямоугольник в нашем коде
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 говорит, что «объекты должны заменяться их подтипами». С другой стороны, этот принцип указывает на
Дочерние классы никогда не должны нарушать определения типов родительского класса.
и следующий пример помогает лучше понять 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();
}
Я рекомендую вам прочитать статью: Нарушение принципа замены Лискова (LSP).
Вы можете найти там объяснение, что такое принцип замещения Лискова, общие подсказки, которые помогут вам угадать, нарушили ли вы его, и пример подхода, который поможет вам сделать вашу иерархию классов более безопасной.
ПРИНЦИП ЗАМЕНЫ ЛИСКОВА (из книги Марка Земанна) утверждает, что мы должны иметь возможность заменить одну реализацию интерфейса другой, не нарушая ни клиента, ни реализации. Именно этот принцип позволяет удовлетворить требования, которые возникнут в будущем, даже если мы сможем » Я предвижу их сегодня.
Если мы отключим компьютер от сети (реализация), ни розетка (интерфейс), ни компьютер (клиент) не выйдут из строя (на самом деле, если это портативный компьютер, он может даже работать от батарей в течение определенного периода времени) . Однако в случае программного обеспечения клиент часто ожидает, что услуга будет доступна. Если сервис был удален, мы получаем исключение NullReferenceException. Чтобы справиться с ситуацией такого типа, мы можем создать реализацию интерфейса, которая «ничего не делает». Это шаблон проектирования, известный как «Нулевой объект» [4], и примерно соответствует отключению компьютера от стены. Поскольку мы используем слабую связь, мы можем заменить реальную реализацию чем-то, что ничего не делает, не вызывая проблем.
Принцип замены Ликова гласит, что , если программный модуль использует базовый класс, тогда ссылка на базовый класс может быть заменена на производный класс, не влияя на функциональность программного модуля.
Намерение - производные типы должны полностью заменять свои базовые типы.
Пример - Ковариантные возвращаемые типы в 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);
}
}
И это автоматически нарушает принцип открытого-закрытого [то есть требования о снятии денег. Потому что никогда не знаешь, что произойдет, если у объекта, нарушающего договор, не хватит денег. Возможно, он просто ничего не вернет, возможно, будет выброшено исключение. Поэтому вам нужно проверить hasEnoughMoney()
, не является ли он частью интерфейса. Таким образом, эта принудительная проверка, зависящая от конкретного класса, является нарушением OCP].
Этот пункт также устраняет заблуждение, с которым я довольно часто сталкиваюсь, о нарушении LSP. В нем говорится, что «если поведение родителей изменилось в ребенке, значит, это нарушит LSP». Однако это не так - до тех пор, пока ребенок не нарушает договор со своим родителем.
В нем говорится, что если C является подтипом E, то E можно заменить объектами типа C без изменения или нарушения поведения программы. Проще говоря, производные классы должны заменять свои родительские классы. Например, если сын фермера - фермер, то он может работать вместо своего отца, но если сын фермера играет в крикет, он не может работать вместо своего отца. отец.
Пример нарушения:
public class Plane{
public void startEngine(){}
}
public class FighterJet extends Plane{}
public class PaperPlane extends Plane{}
В данном примере классы FighterPlane
и PaperPlane
расширяют класс Plane
, который содержит метод startEngine()
. Итак, ясно, что FighterPlane
может запустить двигатель, но PaperPlane
не может, поэтому он ломается LSP
.
PaperPlane
класс, хотя и является расширением класса Plane
и должен быть заменяемым вместо него, но не является подходящей сущностью, которой можно было бы заменить экземпляр Plane, потому что бумажный самолетик не может запустить двигатель, поскольку у него его нет. Итак, хороший пример:
Уважаемый пример:
public class Plane{
}
public class RealPlane{
public void startEngine(){}
}
public class FighterJet extends RealPlane{}
public class PaperPlane extends Plane{}
Принцип замещения Лискова вики (LSP)
В подтипе нельзя усилить предусловия.
Постусловия нельзя ослабить в подтипе.
Инварианты супертипа должны сохраняться в подтипе.
* Предусловие и постусловие: function (method) types
[Тип функции Swift. Функция Swift и метод]
//C1 <- C2 <- C3
class C1 {}
class C2: C1 {}
class C3: C2 {}
Предварительные условия (например, функция
parameter type
) могут быть такими же или более слабыми. (стремится к - ›C1)Постусловия (например, функция
returned type
) могут быть такими же или более сильными (стремится к - ›C3)Инвариантная переменная супертипа [About] должна оставаться неизменной.
Быстрый
class A {
func foo(a: C2) -> C2 {
return C2()
}
}
class B: A {
override func foo(a: C1) -> C3 {
return C3()
}
}
Джава
class A {
public C2 foo(C2 a) {
return new C2();
}
}
class B extends A {
@Override
public C3 foo(C2 a) { //You are available pass only C2 as parameter
return new C3();
}
}
Подтип
- Sybtype не должен требовать от вызывающего больше, чем супертип
- Sybtype не должен открывать для вызывающего абонента меньше супертипа
Контравариантность типов аргументов и ковариация возвращаемого типа.
- Контравариантность аргументов метода в подтипе.
- Ковариация возвращаемых типов в подтипе.
- Никакие новые исключения не должны генерироваться методами подтипа, за исключением случаев, когда эти исключения сами являются подтипами исключений, генерируемых методами супертипа.
[Дисперсия, ковариация, контравариантность]
Попробую, рассмотрим интерфейс:
interface Planet{
}
Это реализовано классом:
class Earth implements Planet {
public $radius;
public function construct($radius) {
$this->radius = $radius;
}
}
Вы будете использовать Землю как:
$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();
Теперь рассмотрим еще один класс, расширяющий Землю:
class LiveablePlanet extends Earth{
public function color(){
}
}
Теперь, согласно LSP, вы должны иметь возможность использовать LiveablePlanet вместо Земли, и это не должно нарушать вашу систему. Нравиться:
$planet = new LiveablePlanet(6371); // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();
Примеры взяты здесь
Earth
является экземпляром plant
, почему он должен быть производным от него?
- person zar; 03.05.2019