I have heard that the Liskov Substitution Principle (LSP) is a fundamental principle of object oriented design. What is it and what are some examples of its use?
Would implementing ThreeDBoard in terms of an array of Board be that useful?
Perhaps you may want to treat slices of ThreeDBoard in various planes as a Board. In that case you may want to abstract out an interface (or abstract class) for Board to allow for multiple implementations.
In terms of external interface, you might want to factor out a Board interface for both TwoDBoard and ThreeDBoard (although none of the above methods fit).
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
When I first read about LSP, I assumed that this was meant in a very strict sense, essentially equating it to interface implementation and type-safe casting. Which would mean that LSP is either ensured or not by the language itself. For example, in this strict sense, ThreeDBoard is certainly substitutable for Board, as far as the compiler is concerned.
After reading up more on the concept though I found that LSP is generally interpreted more broadly than that.
In short, what it means for client code to "know" that the object behind the pointer is of a derived type rather than the pointer type is not restricted to type-safety. Adherence to LSP is also testable through probing the objects actual behavior. That is, examining the impact of an object's state and method arguments on the results of the method calls, or the types of exceptions thrown from the object.
Going back to the example again, in theory the Board methods can be made to work just fine on ThreeDBoard. In practice however, it will be very difficult to prevent differences in behavior that client may not handle properly, without hobbling the functionality that ThreeDBoard is intended to add.
With this knowledge in hand, evaluating LSP adherence can be a great tool in determining when composition is the more appropriate mechanism for extending existing functionality, rather than inheritance.
Robert Martin has an excellent paper on the Liskov Substitution Principle here. It discusses subtle and not-so-subtle ways in which the principle may be violated.
LSP concerns invariants. Your board example is broken at the outset because the interfaces simply don't match.
A better example would be the following (implementations omitted):
class Rectangle {
int getHeight() const;
void setHeight(int value);
int getWidth() const;
void setWidth(int value);
};
class Square : public Rectangle { };
Now we have a problem although the interface matches. The reason is that we have violated (implied) invariants. The way getters and setters work, a Rectangle
should satisfy the following invariant:
void invariant(Rectangle& r) {
r.setHeight(200);
r.setWidth(100);
assert(r.getHeight() == 200 and r.getWidth() == 100);
}
However, this invariant must be violated by a correct implementation of Square
, therefore it is not a valid substitute of Rectangle
.
Maybe we should find another approach. Instead of extending Board, ThreeDBoard should be composed of Board objects. One Board object per unit of the Z axis.
Instead, I would consider making the coordinate type itself a type parameter of the Board class. The Board doesn't care how units are positioned, just that they are.
The LSP is a rule about the contract of the clases: if a base class satisfies a contract, then by the LSP derived classes must also satisfy that contract.
In Pseudo-python
class Base:
def Foo(self, arg):
# *... do stuff*
class Derived(Base):
def Foo(self, arg):
# *... do stuff*
satisfies LSP if every time you call Foo on a Derived object, it gives exactly the same results as calling Foo on a Base object, as long as arg is the same.
A great example illustrating LSP (given by Uncle Bob in a podcast I heard recently) was how sometimes something that sounds right in natural language doesn't quite work in code.
In mathematics, a Square is a Rectangle. Indeed it is a specialization of a rectangle. The "is a" makes you want to model this with inheritance. However if in code you made Square derive from Rectangle, then a Square should be usable anywhere you expect a rectangle. This makes for some strange behavior.
Imagine you had SetWidth and SetHeight methods on your Rectangle base class; this seems perfectly logical. However if your Rectangle reference pointed to a Square, then SetWidth and SetHeight doesn't make sense because setting one would change the other to match it. In this case Square fails the Liskov Substitution Test with Rectangle and the abstraction of having Square inherit from Rectangle is a bad one.
Y'all should check out the other priceless SOLID Principles Motivational Posters.
Strangely, no one has posted the original paper that described lsp. It is not an easy read as Robert Martin's one, but worth it.
This formulation of the LSP is way too strong:
If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T.
Which basically means that S is another, completely encapsulated implementation of the exact same thing as T. And I could be bold and decide that performance is part of the behavior of P...
So, basically, any use of late-binding violates the LSP. It's the whole point of OO to to obtain a different behavior when we substitute an object of one kind for one of another kind!
The formulation cited by wikipedia is better since the property depends on the context and does not necessarily include the whole behavior of the program.