При соблюдении принципа замещения Лискова (LSP) может ли дочерний класс реализовать дополнительный интерфейс?

Рассмотрим этот рубиновый пример

class Animal
  def walk
     # In our universe all animals walk, even whales
     puts "walking"
  end

  def run
    # Implementing to conform to LSP, even though only some animals run
    raise NotImplementedError
  end
end

class Cat < Animal
  def run
    # Dogs run differently, and Whales, can't run at all
    puts "running like a cat"
  end

  def sneer_majesticly
    # Only cats can do this. 
    puts "meh"
  end
end

Нарушает ли метод sneer_majesticly LSP, поскольку он определен только для Cat, поскольку этот интерфейс не реализован и не нужен для Animal?


person konung    schedule 08.06.2017    source источник


Ответы (2)


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

В Ruby (и вообще в ООП) типы — это, по сути, протоколы. Протоколы описывают, на какие сообщения объект отвечает и как он на них отвечает. Например, одним из известных протоколов в Ruby является протокол итераций, который состоит из одного сообщения each, которое принимает блок, но без позиционных или ключевых аргументов и yields элементов последовательно в блок. Обратите внимание, что этому протоколу не соответствует ни класс, ни примесь. Объект, который соответствует этому протоколу, не может объявить об этом.

Существует миксин, который зависит от этого протокола, а именно 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). Итак, вопрос: является ли raiseing исключения частью желаемых свойств или нет? Без документации мы не можем сказать.

Если бы 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
comment
Действительно, действительно, очень хороший ответ. Спасибо, Йорг, что нашли время! Таким образом, SOLID не может быть применен к Ruby OOP, только SOID (из-за вашей точки зрения на типы)? :-) - person konung; 10.06.2017
comment
Нет, LSP применим к Ruby, я привел пример в своем ответе. Кажется, вы думаете, что поскольку в Ruby нет типов, типы не имеют значения. Это неправда. На самом деле вам, вероятно, придется больше думать о типах в Ruby, чем, скажем, в Scala, потому что все происходит в вашей голове: типы не видны в программе, вы должны запомнить их сами , а типы не проверяются языком, их надо проверять самому. Если в документации к методу сказано, что есть определенное постусловие, а вы создаете подтип, который нарушает это постусловие и… - person Jörg W Mittag; 10.06.2017
comment
… попробуйте использовать объект этого подтипа в своей программе, программа сломается во время выполнения, и причина этого сбоя — нарушение LSP. В своей основе LSP сообщает вам, когда можно безопасно заменить объект одного типа на объект другого типа. Игнорирование LSP делает вашу программу ошибочной. - person Jörg W Mittag; 10.06.2017
comment
Зависимость LSP от документации кажется мне слабостью принципа, поскольку количество недокументированного кода и количество неточного задокументированного кода. Требование точной документации для применения LSP представляется необходимым условием, которое редко достигается на практике. Я считаю, что документация уникальна для LSP среди принципов SOLID, что, кажется, делает его гораздо менее практичным, чем другие. - person jaco0646; 11.06.2017
comment
@ jaco0646: LSP не полагается на документацию. LSP сформулирован с точки зрения типов, предусловий, постусловий и инвариантов, а в Ruby они не существуют вне документации. Это единственная связь. В языках с типами, предусловиями, постусловиями и инвариантами все по-другому; документация не нужна. В LSP сказано, что замена объекта супертипа на объект подтипа не должна изменять желательные свойства программы, но как узнать, какие желательные свойства без документации? На некоторых языках вы можете выразить это, … - person Jörg W Mittag; 11.06.2017
comment
…в других нельзя. Посмотрите, например, на количество предусловий, постусловий и инвариантов в Javadocs. И сравните их с Eiffel, который изначально поддерживает их в языке. - person Jörg W Mittag; 11.06.2017
comment
@ JörgWMittag, вы сказали, что принцип подстановки Лисков не имеет ничего общего с классами? Но определение принципа подстановки Лискова гласит, что программа должна иметь возможность заменять экземпляр родительского класса экземпляром одного из своих дочерних классов без отрицательных побочных эффектов. Само определение подразумевает, что мы будем работать с заменой экземпляров классов и будем работать с родительскими и дочерними классами. Это говорит мне, что принцип вращается вокруг объектно-ориентированного наследования. - person Daniel; 11.06.2018
comment
@Daniel: Нет, LSP говорит, что вы должны иметь возможность заменить экземпляр супертипа экземпляром подтипа без изменения наблюдаемых желаемых свойств программы. (Точнее, там говорится, что любое свойство P, которое доказуемо в исходной программе, должно быть доказуемо и в измененной программе.) Существует очень простой способ понять, как работает LSP . не об объектно-ориентированном наследовании, а именно о том, что Барбара Лисков ввела принцип в отношении своего языка CLU, который не является объектно-ориентированным и не имеет наследования. - person Jörg W Mittag; 11.06.2018
comment
@ JörgWMittag, не могли бы вы указать мне какую-нибудь документацию о том, что вы только что поделились со мной? Заранее спасибо. - person Daniel; 11.06.2018
comment
@Daniel: Обратите внимание, например, насколько тщательно написана статья в Википедии, чтобы избежать термина «класс» и говорить только о типе, когда речь идет о LSP. - person Jörg W Mittag; 11.06.2018

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

person drone6502    schedule 08.06.2017
comment
Ну, насмехаясь, это единственная способность Кота. Так почему, по вашему мнению, это не соответствует SRP? Или вы подразумеваете, что я должен использовать принцип разделения интерфейса? Я не думаю, что ISP здесь применим, так как я не применяю насмешки над Животным, а только над кошкой. Я имею в виду, что внутренности насмешки могут вызвать другой класс, но на самом деле он отделен от других классов, унаследованных от Animal. - person konung; 08.06.2017
comment
Если у вас есть код, использующий интерфейс Animal, он не будет знать о насмешках и никогда не вызовет этот метод. Единственная причина, по которой вы бы сделали это, — если бы вы использовали Cat где-то еще, для других целей, что предполагает, что у Cat есть более одной обязанности. - person drone6502; 08.06.2017
comment
Итак, вы предлагаете реализовать класс Sneer, который берет животное, такое как кошка, и заставляет его насмехаться? P.S. Я также обновил свой верхний комментарий - person konung; 08.06.2017
comment
Что касается кода, использующего Animal, я не хочу, чтобы он знал или мог заставить Animal насмехаться. Только кошки должны быть в состоянии сделать это. Я никогда не видел Попугая, который может иронизировать :-) - person konung; 08.06.2017