Принцип подстановки Лисков не имеет ничего общего с классами. Речь идет о типах. Ruby не имеет типов в качестве языковой функции, поэтому не имеет смысла говорить о них с точки зрения языковых функций.
В Ruby (и вообще в ООП) типы — это, по сути, протоколы. Протоколы описывают, на какие сообщения объект отвечает и как он на них отвечает. Например, одним из известных протоколов в Ruby является протокол итераций, который состоит из одного сообщения each
, которое принимает блок, но без позиционных или ключевых аргументов и yield
s элементов последовательно в блок. Обратите внимание, что этому протоколу не соответствует ни класс, ни примесь. Объект, который соответствует этому протоколу, не может объявить об этом.
Существует миксин, который зависит от этого протокола, а именно Enumerable
. Опять же, поскольку нет конструкции Ruby, соответствующей понятию «протокол», Enumerable
не может объявить эту зависимость. Он упоминается только во вступительном абзаце документации (жирный шрифт выделение мое):
Миксин Enumerable
предоставляет классы коллекций с несколькими методами обхода и поиска, а также с возможностью сортировки. Класс должен предоставить метод each
, который возвращает последовательные элементы коллекции.
Вот и все.
Протоколов и типов в Ruby не существует. Они действительно существуют в документации Ruby, в сообществе Ruby, в головах программистов Ruby и в неявных предположениях кода Ruby, но никогда не проявляются в коде.
Таким образом, говорить о LSP с точки зрения классов Ruby не имеет смысла (поскольку классы не являются типами), но и говорить о LSP с точки зрения типов Ruby также мало смысла (поскольку типов нет). Вы можете говорить о LSP только с точки зрения типов в вашей голове (потому что в вашем коде их нет).
Ладно, ругайся. Но это действительно, действительно, действительно, ДЕЙСТВИТЕЛЬНО важно. LSP касается типов. Классы — это не типы. Существуют такие языки, как C++, Java или C♯, где все классы также автоматически являются типами, но даже в этих языках важно отделить понятие типа (которое является спецификацией правил и ограничений) от понятия объекта. class (который является шаблоном для состояния и поведения объектов), хотя бы потому, что существуют другие вещи, кроме классов, которые также являются типами в этих языках (например, интерфейсы в Java и C♯ и примитивы в Ява). Фактически, interface
в Java является прямым портом protocol
из Objective-C. , который, в свою очередь, принадлежит сообществу Smalltalk.
Фу. Так что, к сожалению, ни один из них не отвечает на ваш вопрос :-D
Что именно означает LSP? LSP говорит о подтипах. Точнее, он определяет новое (на момент его изобретения) понятие подтипирования, основанное на поведенческой взаимозаменяемости. Очень просто, LSP говорит:
Я могу заменить объекты типа T объектами типа S‹:T без изменения желаемых свойств программы.
Например, «программа не дает сбоев» — желательное свойство, поэтому я не должен быть в состоянии вызвать сбой программы, заменив объекты супертипа объектами подтипа. Или вы также можете посмотреть на это с другой стороны: если я могу нарушить желаемое свойство программы (например, вызвать сбой программы), заменив объект типа T на объект типа S, то S не является подтипом T.
Есть несколько правил, которым мы можем следовать, чтобы убедиться, что мы не нарушаем LSP:
- Типы параметров метода контравариантны, т. е. если вы переопределяете метод, переопределяющий метод в подтипе должен принимать параметры того же или более общего типа, что и переопределенный метод.
- Типы, возвращаемые методом, являются ковариантными, т. е. переопределяющий метод в подтипе должен возвращать тот же или более конкретный тип, что и переопределенный метод.
Эти два правила — стандартные правила подтипирования функций, они были известны задолго до Лискова.
- Методы в подтипах не должны вызывать никаких новых исключений, не только вызванных переопределенным методом в супертипе, за исключением исключений, типы которых сами являются подтипами исключений, вызванных переопределенным методом.
Эти три правила являются статическими правилами, ограничивающими сигнатуру методов. Ключевым нововведением Лискова стали четыре поведенческих правила, в частности четвертое правило («Правило истории»):
- Предусловия нельзя усиливать в подтипе, т.е. если вы заменяете объект подтипом, вы не можете накладывать дополнительные ограничения на вызывающую сторону, так как вызывающая сторона о них не знает.
- Постусловия не могут быть ослаблены в подтипе, т. е. вы не можете ослабить гарантии, которые дает супертип, поскольку вызывающая сторона может полагаться на них.
- Инварианты должны сохраняться, т. е. если супертип гарантирует, что что-то всегда будет истинным, то это должно быть всегда истинным и в подтипе.
- Правило истории: Манипулирование объектом подтипа не должно создавать историю, которую невозможно наблюдать из объектов супертипа. (Это немного хитро, это означает следующее: если я наблюдаю объект типа S только через методы типа T, я не должен иметь возможность поставить объект в таком состоянии, что наблюдатель видит состояние, которое было бы невозможно с объектом типа T, даже если я использую методы S для управления им.)
Первые три правила были известны до Лискова, но они были сформулированы в теоретико-доказательной манере, не учитывавшей наложение имен. Поведенческая формулировка правил и добавление правила истории делают LSP применимым к современным объектно-ориентированным языкам.
Вот еще один способ посмотреть на LSP: если у меня есть инспектор, который знает и заботится только о T
, и я передаю ему объект типа S
, сможет ли он определить, что это «подделка», или я могу обмануть его?
Хорошо, наконец, на ваш вопрос: нарушает ли добавление метода sneer_majesticly
LSP? И ответ: нет. Единственный способ, которым добавление нового метода может нарушить LSP, — это если этот новый метод манипулирует старым состоянием в такой способом, которого невозможно добиться, используя только старые методы. Поскольку sneer_majesticly
не манипулирует никаким состоянием, его добавление не может нарушить LSP. Помните: наш инспектор знает только о Animal
, т.е. он знает только о walk
и run
. Он не знает и не заботится о sneer_majesticly
.
Если, OTOH, вы добавляли метод bite_off_foot
, после которого кошка больше не может ходить, то вы нарушаете LSP, потому что, вызывая bite_off_foot
, инспектор может, используя только методы, о которых он знает (walk
и run
) наблюдаем ситуацию, которую нельзя наблюдать у животного: животные всегда могут ходить, а наш кот вдруг не может!
Однако! run
могло теоретически нарушить LSP. Помните: объекты подтипа не могут изменять желаемые свойства супертипа. Теперь вопрос: каковы являются желательными свойствами Animal
? Проблема в том, что вы не предоставили никакой документации для Animal
, поэтому мы понятия не имеем, каковы его желательные свойства. Единственное, на что мы можем обратить внимание, это код, который всегда raise
соответствует NotImplementedError
(что, кстати, на самом деле будет raise
вместо NameError
, поскольку в базовой библиотеке Ruby нет константы с именем NotImplementedError
). Итак, вопрос: является ли raise
ing исключения частью желаемых свойств или нет? Без документации мы не можем сказать.
Если бы Animal
было определено так:
class Animal
# …
# Makes the animal run.
#
# @return [void]
# @raise [NotImplementedError] if the animal can't run
def run
raise NotImplementedError
end
end
Тогда это не будет нарушением LSP.
Однако, если бы Animal
было определено так:
class Animal
# …
# Animals can't run.
#
# @return [never]
# @raise [NotImplementedError] because animals never run
def run
raise NotImplementedError
end
end
Тогда это было бы нарушением LSP.
Другими словами: если спецификация для run
"всегда вызывает исключение", то наш инспектор может обнаружить кошку, вызвав run
и наблюдая, что она не вызывает исключение. Однако если спецификация для run
"заставляет животное бежать или возбуждает исключение", то наш инспектор не может отличить кошку от животного.
Вы заметите, что то, нарушает ли Cat
LSP в этом примере, на самом деле совершенно не зависит от Cat
! И это на самом деле также совершенно не зависит от кода внутри Animal
! Это только зависит от документации. Это из-за того, что я пытался прояснить в самом начале: LSP касается типов. В Ruby нет типов, поэтому типы существуют только в голове программиста. Или в этом примере: в комментариях к документации.
person
Jörg W Mittag
schedule
08.06.2017