Liskov Substitution in Dynamic Languages
Of all the class design principles, the one I enjoy discussing the most is the Liskov Substution Principle (LSP). I can, literally, talk about it for hours. Fortunately for people with normal interests, I don't. But, I was teaching it briefly today and a programmer in the room mentioned that he had been taught LSP in school last year.
Finally!
As much as I enjoy discussing LSP, I feel that it is nearly criminal that it isn't taught more often in school. In fact, whenever I teach it, I pensively ask the class whether any of them learned LSP in school. 99% of the time, the answer has been "no."
LSP, in a nutshell, is the idea that when you subclass, your subclass should be usable any place where you have a reference to its superclass. Why? Well, it prevents surprises. The classic example is subclassing a Square class class from a Rectangle class. If you provide methods on Rectangle, you have to make sure that Square can implement those methods in a way which makes sense to people who think they have a Rectangle. If you don't, you can have all sorts of surprises. In some domains it can be disastrous.
LSP is a handy principle. It tells us how we can use inheritance well. The notion of LSP came from the work of Dr. Barbara Liskov at MIT. She was researching safe object substitution. It just turns out that the way that substitution comes up most often in statically typed languages is via subclassing and interface implementation. If I have a class or interface named A, statically typed languages enforce the notion that only something which derives from or implements A can be assigned to a reference to A. That's very convenient, because it means that our class and interface hierarchies can be used to communicate substitutability. When we say that something is an A, and we adhere to LSP, we are saying that it is substitutable for A.
What about dynamic languages, then?
One of the nice features of dynamic languages is that we can substitute anything for anything.. until we get a runtime error. But, if we are careful, and we make sure that, say, an object of class Y handles all of the messages of class X, we can pass that object of Y into any context that expects an X and expect it to work. Y doesn't have to inherit from X; we don't need to use inheritance to get substitutability in dynamically typed languages. What does this mean for LSP?
It's tempting to say that it makes no difference at all, that we should use inheritance any time we want substitutability, but that's terribly impractical and people who use dynamic languages just don't work that way. My experience in dynamic languages isn't very extensive, but the thing I notice is that you get a little bit of slack in Ruby, Python, and Smalltalk. You start out with a piece of code that seems to expect an object of a particular class, and then you look at it and realize that it only expects a few methods. At that point, you've noticed a generalization.. you can pass in other objects that obey the same protocol. In fact, some of the design methods that grew up around Smalltalk recognize this explicitly. Trygve Reenskaug (best known for inventing MVC) wrote a book called 'Working With Objects' in the 1990s which described 'role modeling', a way of looking at design in a class-independent way. You look, instead, at the expectations of a context.
If LSP falls down in dynamic languages, is there a substitute? I think there is. I remember reading something written a long time ago by Tom Love. I can't find the original reference, but (paraphrasing) he said that when you are working with objects every message in a system should mean the same thing to all of its callers. Here's an example:
class Frame
def draw(context)
end
...
end
class Widget
def draw(context)
end
...
end
If we wanted to add a DeckOfCards class to our system we should really think twice before giving it a draw() method that returns a card. If we pass a deck of cards to the windowing system, we might be confused by its behavior. It won't display and eventually it will have far fewer cards than we expect.
Does this uniform semantics principle for methods give us the same benefit LSP did? It might be a little rigid, but I think it does. A method can accept any object, and as long as its intentions are mapped to this one large vocabulary of verb phrases (method names), it can deal with the object appropriately.
I wonder if it is a good substitute for LSP? Excuse the pun.
!commentForm
While interface classes may be strictly unnecessary in a dynamic language, I wonder if they still provide a net benefit of enhanced clarity of substitutability and intent. It seems quicker for future developers to read that a context method that takes a Drawable item, than to examine the full context of use, to determine if one concrete class is substitutable for another. The behavioral difference could manifest many method layers later.
I guess a dynamic developer has the choice of starting with concrete classes, and then refactoring to interfaces, if it clarifies the code.
Yes, depending on the language. Most of them don't have an interface construct. More languages now are introducing optional typing, which seems like the best of both worlds.. you can start dynamic and fill in types as they stabilize. -- MichaelFeathers
I guess a dynamic developer has the choice of starting with concrete classes, and then refactoring to interfaces, if it clarifies the code.
Yes, depending on the language. Most of them don't have an interface construct. More languages now are introducing optional typing, which seems like the best of both worlds.. you can start dynamic and fill in types as they stabilize. -- MichaelFeathers
The original LSP is defined in terms of types and subtypes, not classes and subclasses. In Ruby, we try to remind people that type is not related to class. It seems to me that LSP still applies to dynamic languages, even if there is no direct language construct for type.
I wonder about the genesis of it. The earliest reference I've seen is in Jim Coplien's Advanced C++ Idioms book and if I remember correctly, he speaks in terms of classes rather than types. Dr. Liskov didn't coin the term LSP, but her definition of behavioral subtyping is starting to supercede the the class-based definition that is still floating around in many books and papers. The thing I wonder is whether it is as useful as Tom Love's naming rule, especially since, as you say, dynamic languages have no direct construct for type. -- MichaelFeathers
I wonder about the genesis of it. The earliest reference I've seen is in Jim Coplien's Advanced C++ Idioms book and if I remember correctly, he speaks in terms of classes rather than types. Dr. Liskov didn't coin the term LSP, but her definition of behavioral subtyping is starting to supercede the the class-based definition that is still floating around in many books and papers. The thing I wonder is whether it is as useful as Tom Love's naming rule, especially since, as you say, dynamic languages have no direct construct for type. -- MichaelFeathers
I understand your explanation of the uniform semantics principle, as methods with the same name/params must mean the same thing regardless of the objects they belong to. I think it violates the encapsulation principle of OO. The object on which a method operates is as important as the method itself. From a practical perspective, this suggestion will lead to method names that contain their class name; eg: drawFrame, drawDeckOfCards, ...
I do agree with Jim’s point to distinguish between type and class, I think LSP can still be applied on types.
Yes, I think it can too. Re unform semantics, I hear you. I wonder though, what it's like in large Smalltalk programs, for instance. Do people unconsciously disambiguate names as they go? In the draw example, to me, it might make sense to say drawCard rather than just draw, and that really doesn't duplicate the class name. I'm going to have to ask around. Anyone see this uniform semantics thing occuring in larger dynamic code bases? -- MichaelFeathers
I do agree with Jim’s point to distinguish between type and class, I think LSP can still be applied on types.
Yes, I think it can too. Re unform semantics, I hear you. I wonder though, what it's like in large Smalltalk programs, for instance. Do people unconsciously disambiguate names as they go? In the draw example, to me, it might make sense to say drawCard rather than just draw, and that really doesn't duplicate the class name. I'm going to have to ask around. Anyone see this uniform semantics thing occuring in larger dynamic code bases? -- MichaelFeathers
Michael,
Eiffel follows a strict uniform semantic.
For example, the stack class offers put and item methods rather than the usual push and pop, so that the method names for useing structures of all types are uniform.
It also has a rename facilitiy, so that a descendant within a different domain might use a different name.
So, using the card example, if I had a descendant of both the CARD and DRAWABLE classes that both introduced a 'draw' method (or feature, as they say in paris) then they could both be renamed to draw_image and draw_card in the context of that class. However, any client that thinks it has a CARD or DRAWABLE instance will get the correct result of 'draw.'
ps. I had to google what L is in roman numerals!
Ged, I forgot about that. It's a library convention, isn't it? -- MichaelFeathers
Eiffel follows a strict uniform semantic.
For example, the stack class offers put and item methods rather than the usual push and pop, so that the method names for useing structures of all types are uniform.
It also has a rename facilitiy, so that a descendant within a different domain might use a different name.
So, using the card example, if I had a descendant of both the CARD and DRAWABLE classes that both introduced a 'draw' method (or feature, as they say in paris) then they could both be renamed to draw_image and draw_card in the context of that class. However, any client that thinks it has a CARD or DRAWABLE instance will get the correct result of 'draw.'
ps. I had to google what L is in roman numerals!
Ged, I forgot about that. It's a library convention, isn't it? -- MichaelFeathers
Add Child Page to LiskovSubstitutionInDynamicLanguages