The Substitution Principle, first defined by Barbara Liskov, says that:
“Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T.”
In other words, an instance of the subclass must always behave like an instance of its superclass, so that in every situation in which an instance of the superclass can be used it could be substituted with an instance of the subclass.
In practice, it is difficult to assure that, because most languages do not provide an explicit mechanism to define the semantics of a method. At most we can define the method’s signature: Its name, the types of its parameters, its return type and the exceptions it may raise.
A special case is the Eiffel programming language, created by Bertrand Meyer, in which there are pre-conditions, post-conditions and class invariants. Together, they define a kind of contract between a class and its users, and hence this approach is known as Design by Contract:
- Pre-conditions are the conditions required by a class before a method is called. The user of the class is responsible for assuring the pre-conditions.
- Post-conditions are the conditions required by the user after a method returns. The class is responsible for assuring the post-conditions.
- Class invariants are the properties that are always true regarding the state of an object. The class assumes that invariants are true before a method is executed and guarantee they remain true after its execution.
The question now is: What can we do if there is no way to define the contract between a class and its users? How can we help the developers of new subclasses to make sure that their classes behave correctly, satisfying the Substitution Principle?
This question is particularly relevant for people using frameworks, when it is common to extend the framework through the definition of new subclasses. In this case, a programmer will write new classes that inherit from existing classes that were developed by someone else, and he must be sure that these new classes will work properly when inserted in the framework.
Fortunately we do have a methodology that allows us to define how a class must behave even before we write this class: Test-Driven Development (TDD).
According to the TDD approach, a developer should write unit tests that check the correct implementation of all important functional aspects of a class. It is common to have multiple tests for each method, checking what happens when there are different parameters or when the object being tested has a different state. In general the unit tests can be executed automatically and they include assertions that either return true on success or false on failure.
Now, if we already have a set of unit tests that check the behavior of some class, we can simply apply the same tests on the new subclass we have created and observe if there is anything broken. This is the Substitution Principle in practice! The set of unit tests defines the semantics of the class being tested. It defines the contract between this class and its users. When we run the unit tests replacing an instance of this class with an instance of the new subclass, we actually are checking if the subclass satisfies the Substitution Principle.
Therefore, a framework should be considered complete only if it includes an extensive collection of unit tests for all the classes intended to be inherited from. This will allow developers extending the framework to easily check that their new subclasses are respecting all the existing contracts.
Very well written…instructive
Not quite sure I follow. For example, consider this simple example:
there’s a SystemTask base class that does one thing—getDueDate().
Different child classes return different due dates.
Unless I’m totally misunderstanding something here, this is a completely sane design.
However, the unit test would be completely meaningless.
How do you test for due date?
The only thing you really care about is that getDueDate() returns *something* and doesn’t throw an exception.
If there is a method setDueDate(), then the unit test must check that the value returned by getDueDate() is the same one that was set. If the due date is the result of some computation, then there should be specialized tests for each subclass that implements this computation differently.