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?
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?
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 andyield
s 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 forEnumerable
to declare this dependency. It is only mentioned in the introductory paragraph of the documentation (bold emphasis mine):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 theprotocol
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:
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:
These two rules are just the standard subtyping rules for functions, they were known long before Liskov.
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"):
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 typeS
, 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. Sincesneer_majesticly
doesn't manipulate any state, adding it cannot possibly violate LSP. Remember: our inspector only knows aboutAnimal
, i.e. he only knows aboutwalk
andrun
. He doesn't know or care aboutsneer_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 callingbite_off_foot
, the inspector can, by only using the methods he knows about (walk
andrun
) 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 ofAnimal
? The problem is that you have not provided any documentation forAnimal
, so we have no idea what its desirable properties are. The only thing we can look at, is the code, which alwaysraise
s aNotImplementedError
(which BTW will actuallyraise
aNameError
, since there is no constant namedNotImplementedError
in the Ruby core library). So, the question is: is theraise
ing of the exception part of the desirable properties or not? Without documentation, we cannot tell.If
Animal
were defined like this:Then it would not be an LSP violation.
However, if
Animal
were defined like this: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 callingrun
and observing that it doesn't raise an exception. However, if the specification forrun
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 ofCat
! And it is in fact also completely independent of the code insideAnimal
! 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.