The Liskov substitution principle - Writing SOLID code - Adaptive Code via C#. Agile coding with design patterns and SOLID principles (2014)

Adaptive Code via C#. Agile coding with design patterns and SOLID principles (2014)

Part II: Writing SOLID code

Chapter 7. The Liskov substitution principle

After completing this chapter, you will be able to

Image Understand the importance of the Liskov substitution principle.

Image Avoid breaking the rules of the Liskov substitution principle.

Image Further solidify your single responsibility principle and open/closed principle habits.

Image Create derived classes that honor the contracts of their base classes.

Image Use code contracts to implement preconditions, postconditions, and data invariants.

Image Write correct exception-throwing code.

Image Understand covariance, contravariance, and invariance and where each applies.

Introduction to the Liskov substitution principle

The Liskov substitution principle (LSP) is a collection of guidelines for creating inheritance hierarchies in which a client can reliably use any class or subclass without compromising the expected behavior.

If the rules of the LSP are not followed, an extension to a class hierarchy—that is, a new subclass—might necessitate changes to any client of the base class or interface. If the LSP is followed, clients can remain unaware of changes to the class hierarchy. As long as there are no changes to the interface, there should be no reason to change any existing code. The LSP, therefore, helps to enforce both the open/closed principle and the single responsibility principle.

Formal definition

The definition of the LSP by prominent computer scientist Barbara Liskov is a bit dry, so it requires further explanation. Here is the official definition:

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program.

—Barbara Liskov

There are three code ingredients relating to the LSP:

Image Base type The type (T) that clients have reference to. Clients call various methods, any of which can be overridden—or partially specialized—by the subtype.

Image Subtype Any one of a possible family of classes (S) that inherit from the base type (T). Clients should not know which specific subtype they are calling, nor should they need to. The client should behave the same regardless of the subtype instance that it is given.

Image Context The way in which the client interacts with the subtype. If the client doesn’t interact with a subtype, the LSP can neither be honored nor contravened.

LSP rules

There are several “rules” that must be followed for LSP compliance. These rules can be split into two categories: contract rules (relating to the expectations of classes) and variance rules (relating to the types that can be substituted in code).

Contract rules

These rules relate to the contract of the supertype and the restrictions placed on the contracts that can be added to the subtype.

Image Preconditions cannot be strengthened in a subtype.

Image Postconditions cannot be weakened in a subtype.

Image Invariants—conditions that must remain true—of the supertype must be preserved in a subtype.

To understand the contract rules, you should first understand the concept of contracts and then explore what you can do to ensure that you follow these rules when creating subtypes. The “Contracts” section later in this chapter covers both in depth.

Variance rules

These rules relate to the variance of arguments and return types.

Image There must be contravariance of the method arguments in the subtype.

Image There must be covariance of the return types in the subtype.

Image No new exceptions can be thrown by the subtype unless they are part of the existing exception hierarchy.

The concept of type variance in the languages of the Common Language Runtime (CLR) of the Microsoft .NET Framework is limited to generic types and delegates. However, variance in these scenarios is well worth exploring and will equip you with the requisite knowledge to write code that is LSP compliant for variance. This will be explored in depth in the “Covariance and contravariance” section later in this chapter.

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.

Image

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.


Image Note

It is not advisable to use the decimal type to represent currency values, as is done in Figure 7-1. Instead, a Money1 value type should be used. Although effort has been taken to ensure that the examples in this book are, as much as possible, relevant to a real-world context and are not just contrivances, some concessions have been made in the interest of brevity.

1 http://moneytype.codeplex.com/


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.


Image Tip

For further information on good variable and method naming and other best practices, Steve McConnell’s Code Complete2 is essential reading.

2 http://www.stevemcconnell.com/cc.htm


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.


Image Tip

Although contracts as outlined in this chapter add run-time protection against many invalid calls to methods, the importance of good method and parameter naming is hard to exaggerate. If the formal parameters of the CalculateShippingCost method did not specify that they are in inches or kilograms, clients could, for example, call the method with values representing centimeters and pounds, respectively.


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-4shows.

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.


Encapsulation vs. contracts

The contracts implemented in this example make sense, but they are caused by a poor choice of types for each value. The precondition contract for ensuring that the package weight argument is non-zero and positive is intrinsically linked with the type of the variable: weight should never be zero or negative. This makes weight a candidate for encapsulation into its own type. If, as is likely, another class or method requires a weight, you would need to carry this precondition across to the new code. This is inefficient, hard to maintain, and error-prone. It makes more sense to create a new type and define the precondition with it so that all uses of the Weight type must have a non-zero and positive value. It is, in fact, an invariant of the type rather than a precondition of the CalculateShippingCost method.

Similarly, the flat rate is modeled poorly by the decimal type. Instead, this should be promoted to its own value type, and the invariant requiring it to also be non-zero and positive should be applied to this type.


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:

Image Preconditions cannot be strengthened in a subtype.

Image Postconditions cannot be weakened in a subtype.

Image 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 newWorldWideShippingStrategy 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.


Image Note

If you make the decision to use code contracts, the static Contract class will permeate throughout almost all of your code base. This is less of a problem than it is with most static references because code contracts are ubiquitous infrastructure that, it is assumed, will not be removed or replaced. Thus, it is a significant undertaking to undo the decision to use code contracts, and it is best to use them from the outset of a project, or not at all.


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 packageDimensionsInInches 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.

Image

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.

Image

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 theContract.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 theContractInvariantMethod-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 ContractClassattribute 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 ContractClassForattributes 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.

Covariance and contravariance

The remaining rules of the Liskov substitution principle all relate to covariance and contravariance. Generally, variance is a term applied to the expected behavior of subtypes in a class hierarchy containing complex types.

Definitions

As previously demonstrated, it is important to cover the basics of this topic before diving in to the specifics of the LSP’s requirements for variance.

Covariance

Figure 7-4 shows a very small class hierarchy of just two types: the generically named Supertype and Subtype, which are conveniently named after their respective roles in the inheritance structure. Supertype defines some fields and methods that are inherited by Subtype.Subtype enhances the Supertype by defining its own fields and methods.

Image

FIGURE 7-4 Supertype and Subtype have a parent/child relationship in this class hierarchy.

Polymorphism is the ability of a subtype to be treated as if it were an instance of the supertype. Thanks to this feature of object-oriented programming, which C# supports, any method that accepts an instance of Supertype will also be able to accept an instance of Subtype without any casting required by either the client or service code, and also without any type sniffing by the service. To the service, it has been handed an instance of Supertype, and this is the only fact it is concerned with. It doesn’t care what specific subtype has been handed to it.

Variance enters the discussion when you introduce another type that might use Supertype and/or Subtype through a generic parameter.

Figure 7-5 is a visual explanation of the concept of covariance. First, you define a new interface called ICovariant. This interface is a generic of type T and contains a single method that returns this type, T. Because the out keyword is used before the generic type argument T, this interface is well named because it exhibits covariant behavior.

Image

FIGURE 7-5 Due to covariance of the generic parameter, the base-class/subclass relationship is preserved.

The second half of the class diagram details a new inheritance hierarchy that has been created thanks to the covariance of the ICovariant interface. By plugging in the values for the Supertype and Subtype classes that were defined previously, ICovariant<Supertype>becomes a supertype for the ICovariant<Subtype> interface.

Polymorphism applies here, just as it did previously, and this is where it gets interesting. Thanks to covariance, whenever a method requires an instance of ICovariant<Supertype>, you are perfectly at liberty to provide it with an instance of ICovariant<Subtype>, instead. This will work seamlessly thanks to the simultaneous interoperating of both covariance and polymorphism.

So far, this is of limited general use. To firm up this explanation, I’ll move away from class diagrams and instructive type names to a more real-world scenario. Listing 7-20 shows a class hierarchy between a general Entity base class and a specific User subclass. All Entity types inherit a GUID unique identifier and a string name, and each User has an EmailAddress and a DateOfBirth.

LISTING 7-20 In this small domain, a User is a specialization of the Entity type.


public class Entity
{
public Guid ID { get; private set; }

public string Name { get; private set; }
}
// . . .
public class User : Entity
{
public string EmailAddress { get; private set; }

public DateTime DateOfBirth { get; private set; }
}


This is directly analogous to the Supertype/Subtype example, but with a more directed purpose. This small domain is going to have the Repository pattern applied to it. The Repository pattern provides you with an interface for retrieving objects as if they were in memory but that could realistically be loaded from a very different storage medium. Listing 7-21 shows an EntityRepository class and its UserRepository subclass.

LISTING 7-21 Without involving generics, all inheritance in C# is invariant.


public class EntityRepository
{
public virtual Entity GetByID(Guid id)
{
return new Entity();
}
}
// . . .
public class UserRepository : EntityRepository
{
public override User GetByID(Guid id)
{
return new User();
}
}


This example is not the same as that previously described because of one key difference: in the absence of generic types, C# is not covariant for method return types. In fact, a compilation error is generated due to an attempt to change the return type of the GetByID method in the subclass to match the User class.

error CS0508: 'SubtypeCovariance.UserRepository.GetByID(System.Guid)': return type must be
'SubtypeCovariance.Entity' to match overridden member
'SubtypeCovariance.EntityRepository.GetByID(System.Guid)'

Perhaps experience tells you that this will not work, but the reason is a lack of covariance in this scenario. If C# supported covariance for general classes, you would be able to enforce the change of return type in the UserRepository. Because it does not, you have only two options. You can amend the UserRepository.GetByID method’s return type to be Entity and use polymorphism to allow you to return a User in its place. This is dissatisfying because clients of the UserRepository would have to downcast the return type from an Entity type to aUser type, or they would have to sniff for the User type and execute specific code if the expected type was returned.

Instead, you should redefine EntityRepository as a generic class that requires the Entity type it intends to operate on via a generic type argument. This generic parameter can be marked out, thus covariant, and the UserRepository subclass can specialize its parent base class for the User type. Listing 7-22 exemplifies this.

LISTING 7-22 Make base classes generic to take advantage of covariance and allow subclasses to override the return type.


public interface IEntityRepository<TEntity>
where TEntity : Entity
{
TEntity GetByID(Guid id);
}
// . . .
public class UserRepository : IEntityRepository<User>
{
public User GetByID(Guid id)
{
return new User();
}
}


Rather than maintaining EntityRepository as a concrete class that can be instantiated, this code has converted it into an interface that removes the default implementation of GetByID. This is not entirely necessary, but the benefits of clients depending on interfaces rather than implementations have been demonstrated consistently, so it is a sensible reinforcement of that policy.

Note also that there is a where clause applied to the generic type parameter of the Entity-Repository class. This clause prevents subclasses from supplying a type that is not part of the Entity class hierarchy, which would have made this new version more permissive than the original implementation.

This version prevents the need for UserRepository clients to mess around with downcasting because they are guaranteed to receive a User object, rather than an Entity object, and yet the inheritance of EntityRepository and UserRepository is preserved.

Contravariance

Contravariance is a similar concept to covariance. Whereas covariance relates to the treatment of types that are used as return values, contravariance relates to the treatment of types that are used as method parameters.

Using the same Supertype and Subtype class hierarchy as previously discussed, Figure 7-6 explores the relationship between types that are marked as contravariant via generic type parameters.

Image

FIGURE 7-6 Due to contravariance of the generic parameter, the base-class/subclass relationship is inverted.

The IContravariant interface defines a method that accepts a single parameter of the type dictated by the generic parameter. Here, the generic parameter is marked with the in keyword, meaning that it is contravariant.

The subsequent class hierarchy can be inferred, indicating that the inheritance hierarchy has been inverted: IContravariant<Subtype> becomes the superclass, and IContravariant<Supertype> becomes the subclass. This seems strange and counterintuitive, but it will soon become apparent why contravariance exhibits this behavior—and why it is useful.

In Listing 7-23, the .NET Framework IEqualityComparer interface is provided for reference and an application-specific implementation is created. The EntityEqualityComparer accepts the previous Entity class as a parameter to the Equals method. The details of the comparison are not relevant, but a simple identity comparison is used.

LISTING 7-23 The IEqualityComparer interface allows the definition of function objects like EntityEqualityComparer.


public interface IEqualityComparer<in TEntity>
where TEntity : Entity
{
bool Equals(TEntity left, TEntity right);
}
// . . .
public class EntityEqualityComparer : IEqualityComparer<Entity>
{
public bool Equals(Entity left, Entity right)
{
return left.ID == right.ID;
}
}


The unit test in Listing 7-24 explores the affect that contravariance has on the EntityEqualityComparer.

LISTING 7-24 Contravariance inverts class hierarchies, allowing a more general comparer to be used wherever a more specific comparer is requested.


[Test]
public void UserCanBeComparedWithEntityComparer()
{
SubtypeCovariance.IEqualityComparer<User> entityComparer = new
EntityEqualityComparer();
var user1 = new User();
var user2 = new User();
entityComparer.Equals(user1, user2)
.Should().BeFalse();
}


Without contravariance—the innocent-looking in keyword applied to generic type parameters—the following error would be shown at compile time.

error CS0266: Cannot implicitly convert type 'SubtypeCovariance.EntityEqualityComparer' to
'SubtypeCovariance.IEqualityComparer<SubtypeCovariance.User>'. An explicit conversion exists
(are you missing a cast?)

There would be no type conversion from EntityEqualityComparer to IEqualityComparer-<User>, which is intuitive because Entity is the supertype and User is the subtype. However, because the IEqualityComparer supports contravariance, the existing inheritance hierarchy is inverted and you are able to assign what was originally a less specific type to a more specific type via the IEqualityComparer interface.

Invariance

Beyond covariant or contravariant behavior, types are said to be invariant. This is not to be confused with the term data invariant used earlier in this chapter as it relates to code contracts. Instead, invariant in this context is used to mean “not variant.” If a type is not variant at all, no arrangement of types will yield a class hierarchy. Listing 7-25 uses the IDictionary generic type to demonstrate this fact.

LISTING 7-25 Some generic types are neither covariant or contravariant. This makes them invariant.


[TestFixture]
public class DictionaryTests
{
[Test]
public void DictionaryIsInvariant()
{
// Attempt covariance...
IDictionary<Supertype, Supertype> supertypeDictionary = new Dictionary<Subtype,
Subtype>();

// Attempt contravariance...
IDictionary<Subtype, Subtype> subtypeDictionary = new Dictionary<Supertype,
Supertype>();
}
}


The first line of the DictionaryIsInvariant test method attempts to assign a dictionary whose key and value parameters are of type Subtype to a dictionary whose key and value parameters are of type Supertype. This will not work because the IDictionary type is not covariant, which would preserve the class hierarchy of Subtype and Supertype.

The second line is also invalid, because it attempts the inverse: to assign a dictionary of Supertype to a dictionary of Subtype. This fails because the IDictionary type is not contravariant, which would invert the class hierarchy of Subtype and Supertype.

The fact that the IDictionary type is neither covariant nor contravariant leads to the conclusion that it must be invariant. Indeed, Listing 7-26 shows how the IDictionary type is declared, and you can tell that there is no reference to the out or in keywords that would specify covariance and contravariance, respectively.

LISTING 7-26 None of the generic parameters of the IDictionary interface are marked with in or out.


public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>,
IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable


As previously proven for the general case—that is, without generic types—C# is invariant for both method parameter types and return types. Only when generics are involved is variance customizable on a per-type basis.

Liskov type system rules

Now that you have a grounding in variance, this section can circle back and relate all of this to the Liskov substitution principle. The LSP defines the following rules, two of which relate directly to variance:

Image There must be contravariance of the method arguments in the subtype.

Image There must be covariance of the return types in the subtype.

Image No new exceptions are allowed.

Without contravariance of method arguments and covariance of return types, you cannot write code that is LSP-compliant.

The third rule stands alone as not relating to variance and bears its own discussion.

No new exceptions are allowed

This rule is more intuitive than the other LSP rules that relate to the type system of a language. First, you should consider: what is the purpose of exceptions?

Exceptions aim to separate the reporting of an error from the handling of an error. It is common for the reporter and the handler to be very different classes with different purposes and context. The exception object represents the error that occurred through its type and the data that it carries with it. Any code can construct and throw an exception, just as any code can catch and respond to an exception. However, it is recommended that an exception only be caught if something meaningful can be done at that point in the code. This could be as simple as rolling back a database transaction or as complex as showing users a fancy user interface for them to view the error details and to report the error to the developers.

It is also often inadvisable to catch an exception and silently do nothing, or catch the general Exception base type. Both of these two scenarios together are even more discouraged. With the latter scenario, you end up attempting to catch and respond to everything, including exceptions that you realistically have no meaningful way of recovering from, like OutOfMemoryException, StackOverflowException, or ThreadAbortException. You could improve this situation by ensuring that you always inherit your exceptions from ApplicationException, because many unrecoverable exceptions inherit from SystemException. However, this is not a common practice and relies on third-party libraries to also follow this practice.

Listing 7-27 shows two exceptions that have a sibling relationship in the class hierarchy. It is important to note that this precludes the ability to create a catch block specifically targeting one of the exception types and to intercept both types of exception.

LISTING 7-27 Both of these exceptions are of type Exception, but neither inherits from the other.


public class EntityNotFoundException : Exception
{
public EntityNotFoundException()
: base()
{

}

public EntityNotFoundException(string message)
: base(message)
{

}
}
//. . .
public class UserNotFoundException : Exception
{
public UserNotFoundException()
: base()
{

}

public UserNotFoundException(string message)
: base(message)
{

}
}


Instead, in order to catch both an EntityNotFoundException and a UserNotFoundException with a single catch block, you would have to resort to catching the general Exception, which is not recommended.

This problem is exacerbated in the potential code taken from the EntityRepository and UserRepository classes, as shown in Listing 7-28.

LISTING 7-28 Two different implementations of an interface might throw different types of exception.


public Entity GetByID(Guid id)
{
Contract.Requires<EntityNotFoundException>(id != Guid.Empty);

return new Entity();
}
//. . .
public User GetByID(Guid id)
{
Contract.Requires<UserNotFoundException>(id != Guid.Empty);

return new User();
}


Both of these classes use code contracts to assert a precondition: that the provided id parameter must not be equal to Guid.Empty. Each uses its own exception type if the contract is violated. Think for a second about the impact that this would have on a client using the repository. The client would need to catch both kinds of exception and could not use a single catch block to target both exceptions without resorting to catching the Exception type. Listing 7-29 shows a unit test that is a client to these two repositories.

LISTING 7-29 This unit test will fail because a UserNotFoundException is not assignable to an EntityNotFoundException.


[TestFixture(typeof(EntityRepository), typeof(Entity))]
[TestFixture(typeof(UserRepository), typeof(User))]
public class ExceptionRuleTests<TRepository, TEntity>
where TRepository : IEntityRepository<TEntity>, new()
{
[Test]
public void GetByIDThrowsEntityNotFoundException()
{
var repo = new TRepository();
Action getByID = () => repo.GetByID(Guid.Empty);

getByID.ShouldThrow<EntityNotFoundException>();
}
}


This unit test fails because the UserRepository does not, as required, throw an EntityNotFound-Exception. If the UserNotFoundException was a subclass of the type EntityNotFoundException, this test would pass and a single catch block could guarantee catching both kinds of exception.

This becomes a problem of client maintenance. If the client is using an interface as a dependency and calling methods on that interface, it should not know anything about the classes behind that interface. This is a return to the argument concerning the Entourage anti-pattern versus the Stairway pattern. If new exceptions that are not part of an expected exception class hierarchy are introduced, clients must start referencing these exceptions directly. And—even worse—clients will have to be updated whenever a new exception type is introduced.

Instead, it is important that every interface have a unifying base class exception that conveys the necessary information about an error from the exception reporter to the exception handler.

Conclusion

On the surface, the Liskov substitution principle is one of the more complex facets of the SOLID principles. It requires a foundational knowledge of both contracts and variance to build rules that guide you toward more adaptive code.

By default, interfaces do not convey rules for preconditions or postconditions to clients. Creating guard clauses that halt the application at run time serves to further narrow the allowed range of valid values for parameters. The LSP provides guidelines such that each subclass in a class hierarchy cannot strengthen preconditions or weaken postconditions.

Similarly, the LSP suggests rules for variance in subtypes. There should be contravariance of method arguments in subtypes and covariance of return values in subtypes. Additionally, any new exception that is introduced, perhaps with the creation of a new interface implementation, should inherit from an existing base exception. To do otherwise would be to potentially cause an existing client to miss the catch—effectively to fumble the exception and allow it to cause an application crash.

If the LSP is violated with respect to these rules, it becomes harder for clients to treat all types in a class hierarchy the same. Ideally, clients would be able to hold a reference to a base type or interface and not alter its own behavior depending on the concrete subclass that it is actually using at run time. Such mixed concerns create dependencies between sections of the code that are better kept separate. Any violation of the LSP should be considered technical debt and, as demonstrated in prior chapters, this debt should be paid off sooner rather than later.