Мне просто интересно, есть ли разница между IS-A (терминология UML и ООП) и принципом замещения Лискова (LSP)?
Собственно, оба говорят о наследстве. Так в чем же основное отличие на практике?
Мне просто интересно, есть ли разница между IS-A (терминология UML и ООП) и принципом замещения Лискова (LSP)?
Собственно, оба говорят о наследстве. Так в чем же основное отличие на практике?
Оба термина в конечном итоге описывают одну и ту же «концепцию».
Принцип подстановки Лисков говорит вам: отношение наследования между классами B (базовый) и C (дочерний) правильное, когда каждое и любое использование некоторого объекта типа B... может быть заменено объект типа С.
Это означает: B определяет API и публичный контракт, а C также должен поддерживать эти свойства!
И IS-A сводится к тому же: некоторый объект C является также B.
«Разница» в том, что LSP дает вам точные правила, которые вы можете проверить. В то время как IS-A — это скорее «наблюдение» или выражение намерения. Как в: вы выражаете, что вы желаете, чтобы класс C IS-A B.
Другими словами: если вы не знаете, как правильно использовать наследование, IS-A не поможет вам написать правильный код. В то время как LSP ясно говорит вам, что что-то вроде:
class Base { int foo(); }
class Child extends Base { @Override double foo(); }
недействителен. Согласно LSP, вы можете только расширять аргументы метода и ограничивать возвращаемые значения.
int iValue = someBase.foo();
нельзя заменить на
int iValue = someChild.foo();
потому что результат метода foo()
расширен.
И последнее соображение: многие люди думают, что C IS-A B это то же самое, что и просто записать Child extends Base
. Да. Но это только сообщает компилятору, что C расширяет B. Это не означает, что методы, которые вы используете в C, будут следовать LSP и, таким образом, превратят C в настоящего допустимого потомка B.
C IS-A B требует больше, чем "C расширяет B". Чтобы быть действительно действительным, LSP должен поддерживаться!
Is-A/Has-A касается того, следует ли использовать наследование. Является ли LaserCat типом лазера или у него должно быть только поле для лазера? LSP — это особая проблема, на которую следует обратить внимание, если вы используете наследование определенным образом.
Хорошее использование наследования — иметь Animal a1; указывая на кошку или собаку, используя a1.speed() (*). LSP говорит, что функции скорости Cat и Dog должны использовать одни и те же единицы измерения. Точно так же a1.setWeight для Cats не может допускать отрицательных весов, но Dogs изменяет их на 0. LSP — это согласованность, когда вы можете вызывать любую функцию. На самом деле это довольно очевидно, если вы уже знаете Животное a1; трюк, который трудно.
Для контраста предположим, что у вас есть отдельные кошки и собаки. Если на самом деле скорости измеряются по-разному, это нормально, если для кошек используется метрика, а для собак — английский. Если Cats и Dogs наследуют от Animal, но вы никогда не используете трюк «a1 = Cat or Dog», все равно нормально. c1.speed() точно в метрике, d1.speed() явно в милях в час. Но если у вас есть функция animalRace(Animal a1), у вас есть проблема.
Разница также в тоне. Is-a/has-a — простой совет для начинающих. LSP взят из статьи 30-летней давности, написанной для докторов наук. Уравнения, которые он использует, предназначены для выпускников, специализирующихся в области компьютерных наук. В нем используются условия Pre и Post, которые в то время были обычными и хорошо известными терминами. «Подстановка» — хороший математический термин, но сегодня мы бы сказали просто «базовый класс, который будет указывать на любой подкласс».
(*) Более подробно: у нас есть суперкласс Animal и подклассы Cat и Dog. У животных есть функция скорости заглушки, и Кошка и Собака переопределяют ее. a1.speed() ищет правильный. Реальным примером этого является массив Animals, который действительно содержит Cats и Dogs. Или функция с входом Animal, ожидающая Cat или Dog.
(то же *) Часто базовый класс является абстрактным — мы никогда не создадим объект Animal. Но трюк тот же, если у нас есть суперкласс Toaster и подкласс DeluxeToaster. Все, что берет тостер, может брать все, что является тостером.
Наследование является чисто синтаксическим отношением, тогда как Подстановка Лисков также является семантическим отношением.
Синтаксис прост: вы узнаете, как объявить один класс дочерним по отношению к другому, а компилятор сообщит вам, написали ли вы корректный код. Если код компилируется, вы создали отношение наследования (IS-A).
Семантика сложнее: она определяет, что код означает для клиентов. Семантическое значение часто включает такие вещи, как документация. В популярных ООП-языках компилятор не может сказать вам, соответствует ли код своему предполагаемому значению или нарушает его. Тут в дело вступает Лисков.