Do I understand correctly that Liskov Substitution Principle cannot be observed in languages where objects can inspect themselves, like what is usual in duck typed languages?
For example, in Ruby, if a class B
inherits from a class A
, then for every object x
of A
, x.class
is going to return A
, but if x
is an object of B
, x.class
is not going to return A
.
Here is a statement of LSP:
Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
So in Ruby, for example,
class T; end
class S < T; end
violate LSP in this form, as witnessed by the property q(x) = x.class.name == 'T'
Addition. If the answer is “yes” (LSP incompatible with introspection), then my other question would be: is there some modified “weak” form of LSP which can possibly hold for a dynamic language, possibly under some additional conditions and with only special types of properties.
Update. For reference, here is another formulation of LSP that I’ve found on the web:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
And another:
If S is a declared subtype of T, objects of type S should behave as objects of type T are expected to behave, if they are treated as objects of type T.
The last one is annotated with:
Note that the LSP is all about expected behaviour of objects. One can only follow the LSP if one is clear about what the expected behaviour of objects is.
This seems to be weaker than the original one, and might be possible to observe, but I would like to see it formalized, in particular explained who decides what the expected behavior is.
Is then LSP not a property of a pair of classes in a programming language, but of a pair of classes together with a given set of properties, satisfied by the ancestor class? Practically, would this mean that to construct a subclass (descendant class) respecting LSP, all possible uses of the ancestor class have to be known? According to LSP, the ancestor class is supposed to be replaceable with any descendant class, right?
Update. I have already accepted the answer, but i would like to add one more concrete example from Ruby to illustrate the question. In Ruby, each class is a module in the sense that Class
class is a descendant of Module
class. However:
class C; end
C.is_a?(Module) # => true
C.class # => Class
Class.superclass # => Module
module M; end
M.class # => Module
o = Object.new
o.extend(M) # ok
o.extend(C) # => TypeError: wrong argument type Class (expected Module)
14
Here’s the actual principle:
Let
q(x)
be a property provable about objectsx
of typeT
. Thenq(y)
should be provable for objectsy
of typeS
whereS
is a subtype ofT
.
And the excellent wikipedia summary:
It states that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).
And some relevant quotes from the paper:
What is needed is a stronger requirement that constrains the behavior of sub-types: properties that can be proved using the specification of an object’s presumed type should hold even though the object is actually a member of a subtype of that type…
A type specification includes the following information:
– The type’s name;
– A description of the type’s value space;
– For each of the type’s methods:
— Its name;
— Its signature (including signaled exceptions);
— Its behavior in terms of pre-conditions and post-conditions.
So on to the question:
Do I understand correctly that Liskov Substitution Principle cannot be observed in languages where objects can inspect themselves, like what is usual in duck typed languages?
No.
A.class
returns a class.
B.class
returns a class.
Since you can make the same call on the more specific type and get a compatible result, LSP holds. The issue is that with dynamic languages, you can still call things on the result expecting them to be there.
But let’s consider a statically, structural (duck) typed language. In this case, A.class
would return a type with a constraint that it must be A
or a subtype of A
. This provides the static guarantee that any subtype of A
must provide a method T.class
whose result is a type that satisfies that constraint.
This provides a stronger assertion that LSP holds in languages that support duck typing, and that any violation of LSP in something like Ruby occurs more due to normal dynamic misuse than a language design incompatibility.
27
In the context of LSP a “property” is something that can be observed on a type (or an object). In particular it talks about a “provable property”.
Such a “property” could the existence of a foo()
method that has no return value (and follows the contract set in its documentation).
Make sure you don’t confuse this term with “property” as in “class
is a property of every object in Ruby”. Such a “property” can be a “LSP property”, but it’s not automatically the same!
Now the answer to your questions depends a lot on how stringent you define “property”. If you say “the property of class A
is that .class
will return the type of the object”, then B
actually does have that property.
If, however, you define the “property” to be “.class
returns A
“, then obviously B
does not have that property.
However, the second definition isn’t very useful, as you’ve essentially found a round-about way to declare a constant.
11
As I understand it, there’s nothing about introspection that would be incompatible with the LSP. Basically, as long as an object supports the same methods as another, the two should be interchangable. That is, if your code expects an Address
object, then it doesn’t matter if it’s a CustomerAddress
or a WarehouseAddress
, as long as both provide (e.g.) getStreetAddress()
, getCityName()
, getRegion()
and getPostalCode()
. You could certainly create some kind of decorator that takes a different type of object and uses introspection to provide the required methods (e.g., a DestinationAddress
class that takes a Shipment
object and presents the ship-to address as an Address
), but it’s not required and certainly wouldn’t prevent the LSP from being applied.
16
After looking through Barbara Liskov’s original paper, I’ve found how to complete the Wikipedia definition so that LSP indeed could be satisfied in almost any language.
First of all, the word “provable” is important in the definition. It is not explained in the Wikipedia article, and “constraints” are mentioned elsewhere without reference to it.
Here is the first important quote from the paper:
What is needed is a stronger requirement that constrains the behavior of sub-types: properties that can be proved using the specification of an object’s presumed type should hold even though the object is actually a member of a subtype of that type…
And here is the second, explaining what a type specification is:
A type specification includes the following information:
- The type’s name;
- A description of the type’s value space;
- For each of the type’s methods:
- Its name;
- Its signature (including signaled exceptions);
- Its behavior in terms of pre-conditions and post-conditions.
So, LSP only makes sense with respect to given type specifications, and for an appropriate type specification (for the empty one for example), it can be satisfied in probably any language.
I consider Telastyn’s answer to be the closest to what I was looking for because the “constraints” were mentioned explicitly.
5
To quote Wikipedia’s article on LSP, “substitutability is a principle in object-oriented programming.” It is a principle and part of the design of your program. If you write code that depends on x.class == A
, then it is your code that is violating LSP. Note this kind of broken code is also possible in Java, no duck typing necessary.
Nothing in duck typing inherently breaks LSP. Only if you misuse it, as in your example.
Additional thought: doesn’t explicitly checking for an object’s class defeat the purpose of duck typing, anyway?
11