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?
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 yield
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 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 raise
s 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 raise
ing 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.