The Liskov Substitution Principle
- 10/15/2014
Contracts
It is often said that developers should program to interfaces, and a related idiom is to program to a contract. However, beyond the apparent method signatures, interfaces convey a very loose notion of a contract. A method signature reveals little about the actual requirements and guarantees of the method’s implementation, as Figure 7-1 shows. In a strongly typed language like C#, there is at least a notion of passing the correct type for an argument, but this is largely where the interface ends and the concept of the contract must begin.
FIGURE 7-1 Method signatures reveal little about the expectations of the implementation.
All methods have at least an optional return type, a name, and an optional list of formal parameters. Each parameter consists of a type specifier and a name. When calling the method shown in Figure 7-1, you know—from only looking at the signature—that you need to pass in three parameters, one of type float, one of type Size<float>, and another of type RegionInfo. You also know that you can save the return value, of type decimal, in a variable or otherwise operate on this value after the call has been made.
As a method writer, you can control the names given to parameters and methods. Take extra care to ensure that the method name truly represents the method’s purpose and that the parameter names are as descriptive as possible. The CalculateShippingCost function’s name uses a verb-noun form. Here the verb—the action performed by the method—is Calculate, and the noun—the object of the verb—is ShippingCost. This noun is, in a sense, the name of the return value. Descriptive names have also been chosen for the parameters: packageDimensionsInInches and packageWeightIn-Kilograms are self-explanatory parameter names, especially in the context of the method. They form a starting point for documenting the method.
What is missing, though, is the contract of the method. For example, the packageWeightIn-Kilograms parameter is of type float. What clients of this method might infer is that any float value is valid, including a negative value. Because the parameter represents a weight, a negative value should not be valid. The contract of this method should enforce a weight of greater than zero. For this, the method must implement a precondition.
Preconditions
Preconditions are defined as all of the conditions necessary for a method to run reliably and without fault. Every method requires some preconditions to be true before it should be called. By default, interfaces force no guarantees on any of the implementers of their methods. Listing 7-1 shows how you can implement a precondition by using a guard clause at the start of a method.
LISTING 7-1 Throwing an exception is an effective way of enforcing precondition contracts.
public decimal CalculateShippingCost( float packageWeightInKilograms, Size<float> packageDimensionsInInches, RegionInfo destination) { if (packageWeightInKilograms <= 0f) throw new Exception(); return decimal.MinusOne; }
The if statement at the very start of the method is one way to enforce a precondition, such as the requirement for a positive weight. If the condition packageWeightInKilograms <= 0f is met, an exception is thrown and the method stops executing immediately. This certainly prevents a method from being executed unless all parameters have valid values. By using a more descriptive exception, you can provide more context to the caller, as shown in Listing 7-2.
LISTING 7-2 It is important to provide as much context as possible about why the precondition caused a failure.
public decimal CalculateShippingCost( float packageWeightInKilograms, Size<float> packageDimensionsInInches, RegionInfo destination) { if (packageWeightInKilograms <= 0f) throw new ArgumentOutOfRangeException("packageWeightInKilograms", "Package weight must be positive and non-zero"); return decimal.MinusOne; }
This is an improvement on the first exception that was thrown. In addition to using an exception specifically for the purpose of out-of-range arguments, the client is also informed which parameter is errant and a description of the problem is provided.
By chaining more guard clauses like this together, you can add more conditions that must be fulfilled in order to call the method without generating an exception. The changes shown in Listing 7-3 include exceptions that are thrown when the package dimensions are out of range, too.
LISTING 7-3 As many preconditions as necessary can be added to prevent the method from being called with invalid parameters.
public decimal CalculateShippingCost( float packageWeightInKilograms, Size<float> packageDimensionsInInches, RegionInfo destination) { if (packageWeightInKilograms <= 0f) throw new ArgumentOutOfRangeException("packageWeightInKilograms", "Package weight must be positive and non-zero"); if (packageDimensionsInInches.X <= 0f || packageDimensionsInInches.Y <= 0f) throw new ArgumentOutOfRangeException("packageDimensionsInInches", "Package dimensions must be positive and non-zero"); return decimal.MinusOne; }
With these preconditions in place, clients must ensure that the parameters that they provide are within valid ranges before calling. One corollary from this is that all of the state that is checked in a precondition must be publically accessible by clients. If the client is unable to verify that the method they are about to call will throw an error due to an invalid precondition, the client won’t be able to ensure that the call will succeed. Therefore, private state should not be the target of a precondition; only method parameters and the class’s public properties should have preconditions.
Postconditions
Postconditions check whether an object is being left in a valid state as a method is exited. Whenever state is mutated in a method, it is possible for the state to be invalid due to logic errors.
Postconditions are implemented in the same manner as preconditions, through guard clauses. However, rather than placing the clauses at the start of the method, postcondition guard clauses must be placed at the end of the method after all edits to state have been made, as Listing 7-4 shows.
LISTING 7-4 The guard clause at the end of the method is a postcondition that ensures that the return value is in range.
public virtual decimal CalculateShippingCost(float packageWeightInKilograms, Size<float> packageDimensionsInInches, RegionInfo destination) { if (packageWeightInKilograms <= 0f) throw new ArgumentOutOfRangeException("packageWeightInKilograms", "Package weight must be positive and non-zero"); if (packageDimensionsInInches.X <= 0f || packageDimensionsInInches.Y <= 0f) throw new ArgumentOutOfRangeException("packageDimensionsInInches", "Package dimensions must be positive and non-zero"); // shipping cost calculation var shippingCost = decimal.One; if(shippingCost <= decimal.Zero) throw new ArgumentOutOfRangeException("return", "The return value is out of range"); return shippingCost; }
By testing state against a predetermined valid range—and throwing an exception if the value falls outside of that range—you can enforce a postcondition on the method. The postcondition here relates not to the state of the object but to the return value. Much like method argument values are tested against preconditions for validity, so are method return values tested against postconditions for validity. If, at any point during the method, the return value is set to zero or a negative value, the postcondition will detect this and halt execution at the end of the method. This way, clients of this method will never inadvertently receive an invalid value and they can continue to assume that it will always be valid. Note that the interface of the method does not communicate that the return value will always be non-zero and positive—that is a feature of the interface’s contract with clients.
Data invariants
A third type of contract is the data invariant. A data invariant is a predicate that remains true for the lifetime of an object; it is true after construction and must remain true until the object is out of scope. Data invariants relate to the expected internal state of the object. An example of a data invariant for the ShippingStrategy call is that the flat rate provided is positive and non-zero. If, as shown in Listing 7-5, the flat rate is set on construction, a simple guard clause in the constructor will prevent an invalid value from being set.
LISTING 7-5 Adding a precondition to a constructor can help protect a data invariant.
public class ShippingStrategy { public ShippingStrategy(decimal flatRate) { if (flatRate <= decimal.Zero) throw new ArgumentOutOfRangeException("flatRate", "Flat rate must be positive and non-zero"); this.flatRate = flatRate; } protected decimal flatRate; }
Because the flatRate value is a protected member variable, the only opportunity that clients have for setting the value is through the constructor. If flatRate is set to a valid value at this point, it is guaranteed to be valid for the rest of the lifetime of the object because clients have no way of changing this value.
However, if the flatRate variable is instead a publically settable property, the guard clause would have to be moved to the setter block in order to protect the data invariant. Listing 7-6 shows the flat rate refactored as a public property, with an accompanying guard clause.
LISTING 7-6 When a data invariant is a public property, the guard clause moves to the setter.
public class ShippingStrategy { public ShippingStrategy(decimal flatRate) { FlatRate = flatRate; } public decimal FlatRate { get { return flatRate; } set { if (value <= decimal.Zero) throw new ArgumentOutOfRangeException("value", "Flat rate must be positive and non-zero"); flatRate = value; } } }
Now clients might be able to change the value of the FlatRate property but, because of the if statement and exception, the invariant cannot be broken.
Liskov contract rules
All of this method contract discussion is merely preamble to some of the tenets of the Liskov substitution principle. The LSP sets rules by which types must inherit contracts. A reminder of the definition of the LSP is shown here:
If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program.
Where contracts are concerned, this leads to the guidelines that were stated earlier:
- Preconditions cannot be strengthened in a subtype.
- Postconditions cannot be weakened in a subtype.
- Invariants of the supertype must be preserved in a subtype.
If you follow all of these rules when creating subclasses of existing classes, substitutability will be retained when you are dealing with contracts.
Whenever a subclass is created, it brings with it all of the methods, properties, and fields that make up the parent class. This also includes the contracts inside the methods and property setters. Preconditions, postconditions, and data invariants are all expected to be maintained in the same way that they were in the parent class. Subclasses are, where applicable, allowed to override method implementations, which includes the possibility for changing the contracts. Liskov substitution stipulates that some changes are not allowed, because they could break existing clients that must be able to use the new subclass as if it were an instance of the superclass.
Preconditions cannot be strengthened
Whenever a subclass overrides an existing method that contains preconditions, it must never strengthen the existing preconditions. Doing so would potentially break any client code that already assumes that the subclass defines the strongest possible precondition contracts for any method.
Listing 7-7 shows the addition of a new WorldWideShippingStrategy. Due to the large number of similarities in how the classes behave, this new class is implemented as a subclass of the ShippingStrategy class. The CalculateShippingCost method is overridden to provide a new value that takes into account the destination of the package being sent via the RegionInfo parameter. Although the ShippingStrategy class did not make any guarantees that the destination of the package would be provided, WorldWideShippingStrategy now requires this parameter to be provided, otherwise it cannot correctly calculate how much it would cost to send the package to that location.
LISTING 7-7 This subclass adds a new guard clause, thus strengthening the preconditions.
public class WorldWideShippingStrategy : ShippingStrategy { public override decimal CalculateShippingCost( float packageWeightInKilograms, Size<float> packageDimensionsInInches, RegionInfo destination) { if (packageWeightInKilograms <= 0f) throw new ArgumentOutOfRangeException("packageWeightInKilograms", "Package weight must be positive and non-zero"); if (packageDimensionsInInches.X <= 0f || packageDimensionsInInches.Y <= 0f) throw new ArgumentOutOfRangeException("packageDimensionsInInches", "Package dimensions must be positive and non-zero"); if (destination == null) throw new ArgumentNullException("destination", "Destination must be provided"); return decimal.One; } }
The temptation is to strengthen the preconditions so that you can guarantee that the destination parameter is provided. This creates a conflict that calling code is unable to solve. If a class calls the CalculateShippingCost method of the ShippingStrategy class, it is free to pass in a null value for the destination parameter without experiencing a side effect. But if it is calling the CalculateShippingCost method of the WorldWideShippingStrategy class, it must not pass in a null value for the destination parameter. Doing so would violate a precondition and cause an exception to be thrown. As earlier chapters have demonstrated, client code must never make assumptions about what type it is acting on. Doing so only leads to strong coupling between classes and an inability to adapt to changes in requirements.
To demonstrate the problem, examine the unit test shown in Listing 7-8.
LISTING 7-8 When the precondition is strengthened, clients cannot reliably use a WorldWideShippingStrategy where a ShippingStrategy is required.
[Test] public void ShippingRegionMustBeProvided() { strategy.Invoking(s => s.CalculateShippingCost(1f, ValidDimensions, null)) .ShouldThrow<ArgumentNullException>("Destination must be provided") .And.ParamName.Should().Be("destination"); }
If the strategy used by this test is of type WorldWideShippingStrategy, the test will pass; no destination is provided but one is required, thus an exception meeting the specification is thrown. If a ShippingStrategy is used instead, this test will fail because no precondition exists to prevent the null value for the destination and no exception will be thrown.
Listing 7-9 shows a refactored set of unit tests that do not attempt to test the same preconditions on both strategy types. A test asserting that the shipping region must be provided is only valid for the WorldWideShippingStrategy. However, regardless of shipping strategy, the precondition that the shipping weight must be positive is always valid, so this is included in a base class of tests that will be run for each shipping strategy class.
LISTING 7-9 These refactored unit tests separately target the two shipping strategy classes.
[TestFixture] public class WorldWideShippingStrategyTests : ShippingStrategyTestsBase { [Test] public void ShippingRegionMustBeProvided() { strategy.Invoking(s => s.CalculateShippingCost(1f, ValidSize, null)) .ShouldThrow<ArgumentNullException>("Destination must be provided") .And.ParamName.Should().Be("destination"); } protected override ShippingStrategy CreateShippingStrategy() { return new WorldWideShippingStrategy(decimal.One); } } // . . . public abstract class ShippingStrategyTestsBase { [Test] public void ShippingWeightMustBePositive() { strategy.Invoking(s => s.CalculateShippingCost(-1f, ValidSize, null)) .ShouldThrow<ArgumentOutOfRangeException>("Package weight must be positive and non-zero") .And.ParamName.Should().Be("packageWeightInKilograms"); } }
Postconditions cannot be weakened
When applying postconditions to subclasses, the opposite rule applies. Instead of not being able to strengthen postconditions, you cannot weaken them. As for all of the Liskov substitution rules relating to contracts, the reason that you cannot weaken postconditions is because existing clients might break when presented with the new subclass. Theoretically, if you comply with the LSP, any subclass you create should be usable by all existing clients without causing them to fail in unexpected ways.
One such example of causing an unexpected failure in an existing client is explored in Listing 7-10. The unit test and implementation relate to the WorldWideShippingStrategy, the ShippingStrategy subclass for international packages.
LISTING 7-10 The new implementation requires a weakening of the postcondition.
[Test] public void ShippingDomesticallyIsFree() { strategy.CalculateShippingCost(1f, ValidDimensions, RegionInfo.CurrentRegion) .Should().Be(decimal.Zero); } // . . . public override decimal CalculateShippingCost(float packageWeightInKilograms, Size<float> packageDimensionsInInches, RegionInfo destination) { if (destination == null) throw new ArgumentNullException("destination", "Destination must be provided"); if (packageWeightInKilograms <= 0f) throw new ArgumentOutOfRangeException("packageWeightInKilograms", "Package weight must be positive and non-zero"); if (packageDimensionsInInches.X <= 0f || packageDimensionsInInches.Y <= 0f) throw new ArgumentOutOfRangeException("packageDimensionsInInches", "Package dimensions must be positive and non-zero"); var shippingCost = decimal.One; if(destination == RegionInfo.CurrentRegion) { shippingCost = decimal.Zero; } return shippingCost; }
The unit test asserts that, when the current region is used for the destination—that is, the shipping is domestic—the WorldWideShippingStrategy does not charge for shipping at all. This is reflected in the accompanying implementation. This assertion is, again, in conflict with an existing unit test for the base class that asserts the original postcondition: that the result is always positive and non-zero, as shown in Listing 7-11.
LISTING 7-11 This unit test shows the original unit test, which fails when the strategy is a WorldWideShipping-Strategy.
[Test] public void ShippingCostMustBePositiveAndNonZero() { strategy.CalculateShippingCost(1f, ValidDimensions, RegionInfo.CurrentRegion) .Should().BeGreaterThan(0m); }
A client could easily be broken by this change in behavior due to its assumption of the value of the shipping cost. For example, the client assumes that the shipping cost is always positive and non-zero, as indicated by the postcondition contract of the ShippingStrategy. This client then uses the shipping cost as the denominator in a subsequent calculation. When a switch is made to use the new WorldWideShippingStrategy, the client unexpectedly starts throwing DivideByZeroException errors for all domestic orders.
Had the LSP been honored and the postcondition never weakened, this defect would never have been introduced.
Invariants must be maintained
Whenever a new subclass is created, it must continue to honor all of the data invariants that were part of the base class. This is an easy problem to introduce because subclasses have a lot of freedom to introduce new ways of changing previously private data.
Listing 7-12 returns to the previous data invariant example from earlier in the chapter. However, in this instance, the ShippingStrategy accepts the flat rate value as a constructor parameter and maintains this value as a read-only data invariant. The new WorldWideShippingStrategy is introduced, and the means to change the flat rate value is made public through a property.
LISTING 7-12 The subclass breaks the data invariant of the superclass, violating the LSP.
[Test] public void ShippingFlatRateCanBeChanged() { strategy.FlatRate = decimal.MinusOne; strategy.FlatRate.Should().Be(decimal.MinusOne); } // . . . public class WorldWideShippingStrategy : ShippingStrategy { public WorldWideShippingStrategy(decimal flatRate) : base(flatRate) { } public decimal FlatRate { get { return flatRate; } set { flatRate = value; } } }
Although the subclass reuses the base class’s constructor and guard clause, it does not maintain the data invariant and therefore breaks the Liskov substitution principle. The unit test proves that clients are able to set the value to a negative number, which should be disallowed by the class if it is to correctly protect its data invariants.
Listing 7-13 shows that when the base class is reworked to disallow direct write access to the flat rate field, the invariant is properly honored by the subclass. This is a very common pattern whereby fields are private but have protected or public properties that contain guard clauses to protect the invariants.
LISTING 7-13 The base class allows the subclass write access to the field only through the guarded property setter.
public class WorldWideShippingStrategy : ShippingStrategy { public WorldWideShippingStrategy(decimal flatRate) : base(flatRate) { } public new decimal FlatRate { get { return base.FlatRate; } set { base.FlatRate = value; } } } // . . . public class ShippingStrategy { public ShippingStrategy(decimal flatRate) { if (flatRate <= decimal.Zero) throw new ArgumentOutOfRangeException("flatRate", "Flat rate must be positive and non-zero"); this.flatRate = flatRate; } protected decimal FlatRate { get { return flatRate; } set { if (value <= decimal.Zero) throw new ArgumentOutOfRangeException("value", "Flat rate must be positive and non-zero"); flatRate = value; } } }
Tightening the visibility of the field and instead providing access only through the property setter protects the invariant with a guard clause. Doing this at subclass level is also preferable because it means that all future subclasses are absolved of this responsibility and simply cannot directly write to the field at all.
A new unit test can be created that asserts this new behavior, as shown in Listing 7-14.
LISTING 7-14 With the invariant maintained, this unit test passes.
[Test] public void ShippingFlatRateCannotBeSetToNegativeNumber() { strategy.Invoking(s => s.FlatRate = decimal.MinusOne) .ShouldThrow<ArgumentOutOfRangeException>("Flat rate must be positive and non- zero") .And.ParamName.Should().Be("value"); }
If a client tries to set the FlatRate property to a negative value, or even to zero, the guard clause prevents the assignment and an ArgumentOutOfRangeException is thrown.
Code contracts
Throughout the previous section, the guard clauses that formed the basis of the contracts were all written in long form, using if statements and exceptions. It is worth exploring an alternative to these manual guard clauses: code contracts.
Previously a separate library, code contracts were integrated into the .NET Framework 4.0 main libraries. In addition to being easier to read, write, and comprehend than manual guard clauses, code contracts bring with them the possibility of using static verification and automatic generation of reference documentation.
With static contract verification, code contracts are able to check for contract violations without executing the application. This helps expose implicit contracts such as null dereferences and problems with array bounds, in addition to the explicitly coded contracts shown throughout this section.
Generating reference documentation relating to the contract of a method or class is important because client code has no other way of knowing the exp ectations. When more detail is included in the XML comments that form the documentation to methods and classes, clients can view the expectations via IntelliSense. This makes working with classes that use contracts a bit easier.
Preconditions
Preconditions can be written succinctly by using code contracts. You will need to include the System.Diagnostics.Contracts namespace, which is part of the mscorlib.dll and so should not need an additional assembly reference. The static Contract class provides the majority of the functionality that is required to implement contracts.
Listing 7-15 shows the declarative nature of a code contract precondition.
LISTING 7-15 The System.Diagnostics.Contracts namespace can provide guard clauses to methods.
using System.Diagnostics.Contracts; public class ShippingStrategy { public decimal CalculateShippingCost(float packageWeightInKilograms, Size<float> packageDimensionsInInches, RegionInfo destination) { Contract.Requires(packageWeightInKilograms > 0f); Contract.Requires(packageDimensionsInInches.X > 0f && packageDimensionsInInches.Y > 0f); return decimal.MinusOne; } }
The Contract.Requires method accepts a Boolean predicate value. This represents the state that the method requires in order to proceed. Note that this is the exact opposite of the predicate used in an if statement in manual guard clauses. In that case, the clauses were checking for state that was invalid before throwing an exception. With code contracts, the predicate is closer to an assertion: that the Boolean value must return true, otherwise the contract fails. This example requires that the packageWeightInKilograms parameter is non-zero and positive and that the packageDimensions-InInches parameter is non-zero and positive for both its X and Y properties.
This version of the Contract.Requires method throws an exception when the contract predicate is not met, but the type of exception is a ContractException, which does not match the expected exception in the existing unit tests. Therefore, they fail.
Expected System.ArgumentOutOfRangeException because Package dimension must be positive and non- zero, but found System.Diagnostics.Contracts.__ContractsRuntime+ContractException with message "Precondition failed: packageDimensionsInInches.X > 0f && packageDimensionsInInches.Y > 0f"
Furthermore, if you run this example while passing in an invalid value for one of the parameters, you will get the message shown in Figure 7-2. This informs you that you have not properly configured code contracts for use.
FIGURE 7-2 Code contracts must be configured before use.
The property pages of each project include a Code Contracts tab on which you can configure code contracts. A minimal working setup is shown in Figure 7-3.
FIGURE 7-3 The property pages for code contracts contain a lot of settings.
When they are configured correctly, the contract preconditions can be rewritten to use an alternative version of the Contract.Requires method. Listing 7-16 shows this version.
LISTING 7-16 This version of the Requires method accepts the type of the exception to be thrown.
public class ShippingStrategy { public decimal CalculateShippingCost(float packageWeightInKilograms, Size<float> packageDimensionsInInches, RegionInfo destination) { Contract.Requires<ArgumentOutOfRangeException>(packageWeightInKilograms > 0f, "Package weight must be positive and non-zero"); Contract.Requires<ArgumentOutOfRangeException>(packageDimensionsInInches.X > 0f && packageDimensionsInInches.Y > 0f, "Package dimensions must be positive and non-zero"); return decimal.MinusOne; } }
This generic version of the Requires method accepts the type of exception that you would like the contract to throw when the predicate fails. This, along with the exception message included in a subsequent method parameter, will cause the existing unit tests to pass.
Postconditions
Code contracts can similarly provide a shortcut to defining postconditions. The Contract static class contains an Ensures method that is the postcondition complement to the precondition’s Requires method. This method also accepts a Boolean predicate that must be true in order to progress through to the return statement. It is worth noting that the return statement must be the only line that follows a call to Contract.Ensures. This makes intuitive sense because, otherwise, it would be possible to further modify state in a way that might break the postcondition.
Listing 7-17 reiterates the ShippingCostMustBePositive unit test and includes a rewritten CalculateShippingCost implementation that uses the Contract.Ensures method as a postcondition.
LISTING 7-17 The Ensures method creates a postcondition that should be true on exiting the method.
[Test] public void ShippingCostMustBePositive() { strategy.CalculateShippingCost(1, ValidSize, null) .Should().BeGreaterThan(decimal.MinusOne); } // . . . public class ShippingStrategy { public decimal CalculateShippingCost(float packageWeightInKilograms, Size<float> packageDimensionsInInches, RegionInfo destination) { Contract.Requires<ArgumentOutOfRangeException>(packageWeightInKilograms > 0f, "Package weight must be positive and non-zero"); Contract.Requires<ArgumentOutOfRangeException>(packageDimensionsInInches.X > 0f && packageDimensionsInInches.Y > 0f, "Package dimensions must be positive and non-zero"); Contract.Ensures(Contract.Result<decimal>() > 0m); return decimal.MinusOne; } }
The predicate in this example is a bit different from the ones in prior examples and demonstrates a common use of the postcondition: testing that a return value is valid. Checking that the shipping cost is positive (and, in fact, non-negative) requires knowledge of the return value. The return value is often, but not always, a local variable that is declared and defined within the method. You could trivially assert that the value you are returning is greater than zero, but this is not really foolproof. To access the value that is actually returned from the method, you can use the Contract.Result method to retrieve it. This generic method accepts the return type of the method and returns whichever result is eventually returned by the method. This is how you can ensure that no subsequent lines can replace a valid value with an invalid value without the postcondition failing and an exception being thrown.
Data invariants
It is common for each method in a class to contain its own preconditions and postconditions, but data invariants relate to the class as a whole. Code contracts allow you to create a private method on the class that contains declarative definitions of the class’s invariants.
Each invariant is defined by another method of the Contract static class, as Listing 7-18 shows.
LISTING 7-18 Data invariants can be protected by a method dedicated to the purpose.
public class ShippingStrategy { public ShippingStrategy(decimal flatRate) { this.flatRate = flatRate; } [ContractInvariantMethod] private void ClassInvariant() { Contract.Invariant(this.flatRate > 0m, "Flat rate must be positive and non-zero"); } protected decimal flatRate; }
The Contract.Invariant method follows the same pattern as the Requires and Ensures methods in that it accepts a Boolean predicate that must be true in order to satisfy the contract. In this example, there is also a second string parameter provided that describes the fault if this contract fails to be met and the invariant is unprotected. The client is allowed to make as many calls to the Invariant method as necessary, so it is best to break the invariants down to their most granular, rather than logically AND them all together with the && operator. This gives you the maximum benefit of knowing exactly which data invariant has been broken.
If this were a normal private method, you would be obliged to call the method at the start and end of every method, to ensure that the invariants were correctly protected. Luckily, you can have code contracts do this on your behalf by marking the method with the ContractInvariantMethod-Attribute. Remember that attributes do not require the Attribute suffix, so this has been shortened in the example to ContractInvariantMethod. This flags the method as one that code contracts must call when entering and leaving a method, to confirm that the class’s data invariants are not being violated. The prerequisites for marking a method as a ContractInvariantMethod are that it must return void and accept no arguments. However, it can be public or private, and you can choose any name to describe the method. Classes can have more than one ContractInvariantMethod, so logically grouping them is also possible. The body of the method must only make calls to the Contract.Invariant method.
Interface contracts
The final feature of code contracts to be covered here is that of interface contracts. So far, you have embedded all of your calls to Contract.Requires, Contract.Ensures, and Contract.Invariant in the class implementation itself. As has been mentioned, the static nature of the Contract class makes this code ubiquitous and difficult to remove or change in favor of an alternative library in the future. This is somewhat contrary to the adaptive codebase that is the ideal, but some infrastructural concessions are justifiable for pragmatic reasons.
A more immediate concern is the drop in readability that occurs when code contracts are liberally applied to classes. In fact, this is not really a fault of code contracts but a result of diligently applying contracts in general. Preconditions, postconditions, and data invariants are naturally implemented in code, but this code tends to increase the noise-to-signal ratio.
An interface contract, such as that shown in Listing 7-19 for the ongoing ShippingStrategy example, can alleviate this problem in addition to providing another helpful feature.
LISTING 7-19 A dedicated class can define preconditions, postconditions, and invariants for every implementation of an interface.
[ContractClass(typeof(ShippingStrategyContract))] interface IShippingStrategy { decimal CalculateShippingCost( float packageWeightInKilograms, Size<float> packageDimensionsInInches, RegionInfo destination); } //. . . [ContractClassFor(typeof(IShippingStrategy))] public abstract class ShippingStrategyContract : IShippingStrategy { public decimal CalculateShippingCost(float packageWeightInKilograms, Size<float> packageDimensionsInInches, RegionInfo destination) { Contract.Requires<ArgumentOutOfRangeException>(packageWeightInKilograms > 0f, "Package weight must be positive and non-zero"); Contract.Requires<ArgumentOutOfRangeException>(packageDimensionsInInches.X > 0f && packageDimensionsInInches.Y > 0f, "Package dimensions must be positive and non-zero"); Contract.Ensures(Contract.Result<decimal>() > 0m); return decimal.One; } [ContractInvariantMethod] private void ClassInvariant() { Contract.Invariant(flatRate > 0m, "Flat rate must be positive and non-zero"); } }
For interface contracts, you of course need an interface to work with. In this example, the CalculateShippingCost method has been extracted into its own IShippingStrategy interface. It is this interface, rather than a single implementation, that is going to have the contracts applied. This is an important departure from the previous examples because it means that all implementations of this interface will acquire the applied contracts. This is how you can enhance a simple interface that provides few instructions for implementation and use, to give it more powerful requirements and assurances.
When writing an interface contract, you also need a class that is going to implement the methods of the interface but only fill them with uses of the Contract.Requires and Contract.Ensures methods. The abstract ShippingStrategyContract provides this functionality and looks like the prior examples, but what the prior examples lacked was the real functionality of the method. Even in production code, this is the limit of the code contained in a contract class. There is also a ContractInvariantMethod to house any calls to Contract.Invariant, just as if this class were the real implementation.
To link the interface to the contract class implementation, you unfortunately need a two-way reference via an attribute. This is somewhat unfortunate because it adds noise to the interface, which it would be nice to avoid. Nevertheless, by marking the interface with the ContractClass attribute and the contract class with the ContractClassFor attribute, you can write your preconditions, postconditions, and data invariant protection code once and have it apply to all subsequent implementations of the interface. Both the ContractClass and ContractClassFor attributes accept a Type argument. The ContractClass is applied to the interface and has the contract class type passed in, whereas the ContractClassFor is applied to the contract class and has the interface type passed in.
This concludes the introduction to code contracts and the foray into the Liskov substitution principle’s rules relating to contracts. One final important point needs to be emphasized. Whether they are implemented manually or by using code contracts, if a precondition, postcondition, or invariant fails, clients should not catch the exception. Catching an exception is an action that indicates that the client can recover from this situation, which is seldom likely or perhaps even possible when a contract is broken. The ideal is that all contract violations will happen during functional testing and that the offending code will be fixed before shipping. This is why it is so important to unit test contracts. If a contract violation is not fixed before shipping and an end user is unfortunate enough to trigger an exception, it is most likely the best course of action to force the application to close. It is advisable to allow the application to fail because it is now in a potentially invalid state. For a web application, this will mean that the global error page is displayed. For a desktop application, the user can be shown a friendly message and be given a chance to report the problem. In any and all cases, a log should be made of the exception, with full stack trace and as much context as possible.
The next section covers the rest of the LSP’s rules—those that apply to covariance and contravariance.