Inheritance and contracts

We mostly use Java for the examples and Java is an object-oriented language, so I must discuss what happens when we use inheritance. Figure 4.2 shows that the TaxCalculator class has many children (TaxCalculatorBrazil which calculates taxes in Brazil, TaxCalculatorNL, which calculates taxes in the Netherlands, and so on). These child classes all override calculateTax() and change the pre- or post-conditions one way or another. Are these contract changes breaking changes?

Figure 4.2 A base class and its child classes. The client depends on the base class, which means any of its children may be used at run time.

We can apply the same reasoning as when we discussed changing contracts. Let’s start by focusing on the client class rather than the child classes. Suppose the client class receives a TaxCalculator in its constructor and later uses it in its methods. Due to polymorphism, we know that any of the child classes can also be passed to the client: for example, we can pass a TaxCalculatorBrazil or a TaxCalculatorUS, and it will be accepted because they are all children of the base class.

Since the client class does not know which tax calculator was given to it, it can only assume that whatever class it received will respect the pre- and post-conditions of the base class (the only class the client knows). In this case, value must be greater than or equal to 0 and should return a value greater than or equal to 0. Let’s explore what will happen if each of the child classes is given to the client class:

  • TaxCalculatorBrazil has the same pre-conditions as the base class. This means there is no way the client class will observe strange behavior regarding the pre-conditions if it is given TaxCalculatorBrazil. On the other hand, the TaxCalculatorBrazil class has a post-condition that the returned value is any number. This is bad. The client class expects only values that are greater than or equal to zero; it does not expect negative numbers. So if TaxCalculatorBrazil returns a negative number to the client, this may surprise the client and lead to a failure.
  • TaxCalculatorUS has the following pre-condition: “value greater than or equal to 100.” This pre-condition is stronger than the pre-condition of the base class (value >= 0), and the client class does not know that. Thus the client may call the tax calculator with a value that is acceptable for the base class but not acceptable for TaxCalculatorUS. We can expect a failure to happen. The post-condition of TaxCalculatorUS is the same as that of the base class, so we do not expect problems there.
  • TaxCalculatorNL has a different pre-condition from the base class: it accepts any value. In other words, the pre-condition is weaker than that of the base class. So although the client is not aware of this pre-condition, we do not expect failures, as TaxCalculatorNL can handle all of the client’s inputs.

If we generalize what we observe in this example, we arrive at the following rules whenever a subclass S (for example, TaxCalculatorBrazil) inherits from a base class B (for example, TaxCalculator):

  1. The pre-conditions of subclass S should be the same as or weaker (accept more values) than the pre-conditions of base class B.
  2. The post-conditions of subclass S should be the same as or stronger (return fewer values) than the post-conditions of base class B.

This idea that a subclass may be used as a substitution for a base class without breaking the expected behavior of the system is known as the Liskov substitution principle (LSP). This principle was introduced by Barbara Liskov in a 1987 keynote and later refined by her and Jeannette Wing in the famous “A behavioral notion of subtyping” paper (1994). The LSP became even more popular among software developers when Robert Martin popularized the SOLID principles, where the “L” stands for LSP.

NOTE A well-known best practice is to avoid inheritance whenever possible (see Effective Java’s item 16, “favor compostion over inheritance”). If you avoid inheritance, you naturally avoid all the problems just discussed. But it is not the goal of this discuss best practices in object-oriented design. If you ever need to use inheritance, you now know what to pay attention to.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *