When adhering to Liskov Substitution Principle (LSP) can a child class implement additional interface?

前端 未结 2 2093
清酒与你
清酒与你 2021-02-11 02:22

Consider this ruby example

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

  def run
    # Implementing         


        
相关标签:
2条回答
  • 2021-02-11 02:54

    The Liskov Substitution Principle has nothing to do with classes. It is about types. Ruby doesn't have types as a language feature, so it doesn't really make sense to talk about them in terms of language features.

    In Ruby (and OO in general), types are basically protocols. Protocols describe which messages an object responds to, and how it responds to them. For example, one well-known protocol in Ruby is the iteration protocol, which consists of a single message each which takes a block, but no positional or keyword arguments and yields elements sequentially to the block. Note that there is no class or mixin corresponding to this protocol. There is no way for an object which conforms to this protocol to declare so.

    There is a mixin which depends on this protocol, namely Enumerable. Again, since there is no Ruby construct which corresponds to the notion of "protocol", there is no way for Enumerable to declare this dependency. It is only mentioned in the introductory paragraph of the documentation (bold emphasis mine):

    The Enumerable mixin provides collection classes with several traversal and searching methods, and with the ability to sort. The class must provide a method each, which yields successive members of the collection.

    That's it.

    Protocols and types don't exist in Ruby. They do exist in Ruby documentation, in the Ruby community, in the heads of Ruby programmers, and in implicit assumptions in Ruby code, but they are never manifest in the code.

    So, talking about the LSP in terms of Ruby classes makes no sense (because classes aren't types), but talking about the LSP in terms of Ruby types makes little sense either (because there are no types). You can only talk about the LSP in terms of the types in your head (because there aren't any in your code).

    Okay, rant over. But that is really, really, really, REALLY important. The LSP is about types. Classes aren't types. There are languages like C++, Java, or C♯, where all classes are also automatically types, but even in those languages it is important to separate the notion of a type (which is a specification of rules and constraints) from the notion of a class (which is a template for the state and behavior of objects), if only because there are other things besides classes which are types in those languages as well (e.g. interfaces in Java and C♯ and primitives in Java). In fact, the interface in Java is a direct port of the protocol from Objective-C, which in turn comes from the Smalltalk community.

    Phew. So, unfortunately none of this answers your question :-D

    What, exactly, does the LSP mean? The LSP talks about subtyping. More precisely, it defines a (at the time it was invented) new notion of subtyping which is based on behaviorial substitutability. Very simply, the LSP says:

    I can replace objects of type T with objects of type S <: T without changing the desirable properties of the program.

    For example, "the program does not crash" is a desirable property, so I should not be able to make a program crash by replacing objects of a supertype with objects of a subtype. Or you can also view it from the other direction: if I can violate a desirable property of a program (e.g. make the program crash) by replacing an object of type T with an object of type S, then S is not a subtype of T.

    There are a couple of rules we can follow to make sure that we don't violate the LSP:

    • Method parameter types are contravariant, i.e. if you override a method, the overriding method in the subtype must accept parameters of the same types or more general types as the overridden method.
    • Method return types are covariant, i.e. the overriding method in a subtype must return the same type or a more specific type as the overridden method.

    These two rules are just the standard subtyping rules for functions, they were known long before Liskov.

    • Methods in subtypes must not raise any new exceptions that are not only raised by the overridden method in the supertype, except for exceptions whose types are themselves subtypes of the exceptions raised by the overridden method.

    These three rules are static rules restricting the signature of methods. The key innovation of Liskov were the four behavioral rules, in particular the fourth rule ("History Rule"):

    • Preconditions cannot be strengthened in a subtype, i.e. if you replace an object with a subtype, you cannot impose additional restrictions on the caller, since the caller doesn't know about them.
    • Postconditions cannot be weakened in a subtype, i.e. you cannot relax guarantees that the supertype makes, since the caller may rely on them.
    • Invariants must be preserved, i.e. if the supertype guarantees that something will always be true, then it must also always be true in the subtype.
    • History Rule: Manipulating the object of a subtype must not create a history that is impossible to observe from objects of the supertype. (This one is a bit tricky, it means the following: if I observe an object of type S only through methods of type T, I should not be able to put the object in a state such that the observer sees a state that would not be possible with an object of type T, even if I use methods of S to manipulate it.)

    The first three rules were known before Liskov, but they were formulated in a proof-theoretical manner which didn't take aliasing into account. The behavioral formulation of the rules, and the addition of the History Rule make the LSP applicable to modern OO languages.

    Here is another way to look at the LSP: if I have an inspector who only knows and cares about T, and I hand him an object of type S, will he be able to spot that it is a "counterfeit" or can I fool him?

    Okay, finally to your question: does adding the sneer_majesticly method violate the LSP? And the answer is: No. The only way that adding a new method can violate LSP is if this new method manipulates old state in such a way that is impossible to happen using only old methods. Since sneer_majesticly doesn't manipulate any state, adding it cannot possibly violate LSP. Remember: our inspector only knows about Animal, i.e. he only knows about walk and run. He doesn't know or care about sneer_majesticly.

    If, OTOH, you were adding a method bite_off_foot after which the cat can no longer walk, then you violate LSP, because by calling bite_off_foot, the inspector can, by only using the methods he knows about (walk and run) observe a situation that is impossible to observe with an animal: animals can always walk, but our cat suddenly can't!

    However! run could theoretically violate LSP. Remember: objects of a subtype cannot change desirable properties of the supertype. Now, the question is: what are the desirable properties of Animal? The problem is that you have not provided any documentation for Animal, so we have no idea what its desirable properties are. The only thing we can look at, is the code, which always raises a NotImplementedError (which BTW will actually raise a NameError, since there is no constant named NotImplementedError in the Ruby core library). So, the question is: is the raiseing of the exception part of the desirable properties or not? Without documentation, we cannot tell.

    If Animal were defined like this:

    class Animal
      # …
    
      # Makes the animal run.
      #
      # @return [void]
      # @raise [NotImplementedError] if the animal can't run
      def run
        raise NotImplementedError
      end
    end
    

    Then it would not be an LSP violation.

    However, if Animal were defined like this:

    class Animal
      # …
    
      # Animals can't run.
      #
      # @return [never]
      # @raise [NotImplementedError] because animals never run
      def run
        raise NotImplementedError
      end
    end
    

    Then it would be an LSP violation.

    In other words: if the specification for run is "always raises an exception", then our inspector can spot a cat by calling run and observing that it doesn't raise an exception. However, if the specification for run is "makes the animal run or else raises an exception", then our inspector can not differentiate a cat from an animal.

    You will note that whether or not Cat violates the LSP in this example is actually completely independent of Cat! And it is in fact also completely independent of the code inside Animal! It only depends on the documentation. That is because of what I tried to make clear in the very beginning: the LSP is about types. Ruby doesn't have types, so the types only exist in the programmer's head. Or in this example: in documentation comments.

    0 讨论(0)
  • 2021-02-11 03:00

    LSP says you can drop in any implementation of the base type/interface, and it should continue to work. So no reason why it should violate that, although it raises interesting questions about why you need to implement that additional interface in one implementation and not others. Are you following the single responsibility principle?

    0 讨论(0)
提交回复
热议问题