By konung


2017-06-08 17:47:44 8 Comments

Consider this ruby example

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

Does method sneer_majesticly violate LSP, being defined only on Cat, since this interfaces is not implemented nor needed on Animal?

2 comments

@Jörg W Mittag 2017-06-08 20:53:07

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.

@konung 2017-06-09 21:37:00

Really, really , really good answer. Thank you,Jorg for taking the time! So SOLID then can't be really applied to Ruby OOP, only SOID (because of your point about types) ? :-)

@Jörg W Mittag 2017-06-10 09:08:20

No, the LSP is applicable to Ruby, I gave an example of that in my answer. You seem to think that because Ruby doesn't have types, types don't matter. That is not true. In fact, you will probably have to think more about types in Ruby than, say, in Scala, because everything happens in your head: the types are not visible in the program, you have to remember them yourself, and the types are not checked by the language, you have to check them yourself. If the documentation for a method says that there is a certain postcondition, and you create a subtype which violates that postcondition and …

@Jörg W Mittag 2017-06-10 09:09:57

… try to use an object of that subtype in your program, the program will blow up at runtime, and the reason for it blowing up is violating the LSP. At its base, the LSP tells you when it is safe to substitute an object of one type for an object of another type. Ignoring the LSP makes your program buggy.

@jaco0646 2017-06-10 21:25:35

The reliance of LSP on documentation strikes me as a weakness of the principle insofar as the amount of code which is undocumented and the amount of documented code which is inaccurate. Requiring accurate documentation to apply LSP appears to be a prerequisite which is rarely achieved in practice. I believe documentation is unique to LSP among the SOLID principles, which seems to make it far less practical than the others.

@Jörg W Mittag 2017-06-11 00:34:45

@jaco0646: The LSP doesn't rely on documentation. The LSP is phrased in terms of types, preconditions, postconditions, and invariants, and in Ruby, those don't exist outside of documentation. That's the only connection. In languages with types, preconditions, postconditions, and invariants, it's different; no documentation is needed. The LSP says that exchanging an object of a supertype for an object of a subtype should not change the desirable properties of the program – but how do you know what the "desirable properties" are without documentation? In some languages you can express it, …

@Jörg W Mittag 2017-06-11 00:38:49

… in others, you can't. Look at the amount of preconditions, postconditions, and invariants in Javadocs, for example. And compare them to Eiffel, which supports them natively in the language.

@Daniel 2018-06-11 11:01:21

@JörgWMittag, you said that the Liskov Substitution Principle has nothing to do with classes? But the definition of Liskov substitution principle says that a program should have the ability to replace an instance of a parent class with an instance of one of its child classes without negative side effects. The definition itself implies we will be working with replacing instances of classes and that we're going to be working with parent and child classes. This tells me the principle revolves around object oriented inheritance.

@Jörg W Mittag 2018-06-11 15:13:50

@Daniel: No, the LSP says that you should be able to replace an instance of a supertype with an instance of a subtype without changing the observable desirable properties of the program. (More precisely, it says that any property P that is provable about the original program should also be provable about the changed program.) There is a very simple way how to how that the LSP is not about object-oriented inheritance, namely that Barbara Liskov coined the principle in relationship to her CLU language which is neither OO nor has inheritance.

@Daniel 2018-06-11 17:49:43

@JörgWMittag, can you point me to some documentation concerning about you just shared with me? Thank you in advance.

@Jörg W Mittag 2018-06-11 20:30:23

@Daniel: Barbara Liskov formulated the Principle in her Keynote Address at OOPSLA 1987 titled "Data Abstraction and Hierarchy". (By the way, that is the same OOPSLA where also the famous "Treaty of Orlando" happened; this seems to have been a very productive conference!) Like pretty much anything related to Software Engineering, some of the best sources can be found on the Wiki. As it turns out, the Wikpedia article is also not half bad.

@Jörg W Mittag 2018-06-11 20:35:36

@Daniel: Note, for example, how carefully the Wikipedia article is written to avoid the term "class" and only talk about "type" when talking about the LSP.

@drone6502 2017-06-08 17:58:25

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?

@konung 2017-06-08 18:04:33

Well sneering, is a Cat only ability. So why is it not SRP compliant, in your opinion? Or are you implying that I should used Interface segregation principle ? I don't think ISP applies here, since I don't implement sneering on the Animal, just the cat. I mean internals of sneering could call up another class, but its is actually segregated out from other classes inhering from Animal

@drone6502 2017-06-08 18:07:37

If you have some code that uses the Animal interface, it will have no knowledge of sneering, and never call that method. The only reason you would do this is if you used Cat elsewhere, for other purposes, which would suggest that Cat has more than one responsibility.

@konung 2017-06-08 18:09:22

So you Suggesting to Implement a class Sneer, that takes an Animal, such as Cat, and makes it sneer? P.S. I updated my top comment as well

@konung 2017-06-08 18:12:14

As far as code that uses Animal - I don't want it to know or be able to make an Animal sneer. Only Cats should be able to do that. I've never seen Parrot, that can sneer :-)

Related Questions

Sponsored Content

3 Answered Questions

29 Answered Questions

2 Answered Questions

[SOLVED] SOLID Liskov Substitution Principle

2 Answered Questions

3 Answered Questions

[SOLVED] Liskov substitution principle violation

5 Answered Questions

3 Answered Questions

[SOLVED] liskov substitution principle violations

Sponsored Content