Using code contracts we're now going to see how we can "guard" behavior and state. Also how to ensure that subclasses respect the restrictions set by their superclasses.
There will be no seminar for this block. Please participate actively in the guest lectures and workshops.
At the lectures you've discussed an example with vehicles of different weights. We're going to implement that example and attempt to enforce behaviour and state-space through the use of contracts. Let's start off with some basic classes:
class RoadVehicle
{
public double Weight { get; protected set; }
public RoadVehicle() { }
public RoadVehicle(double weight)
{
if (weight < 0.5 || 10.0 < weight)
throw new ArgumentException();
Weight = weight;
}
}
class Automobile : RoadVehicle
{
public Automobile(double weight)
{
if (weight < 0.2 || 13 < weight)
throw new ArgumentException();
Weight = weight;
}
}
Try recalling the discussion you had in the lecture. Notice how the subclass above ignores the restrictions posed by the superclass's constructor. A simple mistake you may say, but surely an easy one to make. We're violating Liskov's substitution principle.
To enable Visual Studios static and runtime contract checking, right-click your project in the Solution Explorer and choose Properties.
Select the Code Contracts tab and make sure to check Perform Runtime Contract Checking, Perform Static Contract Checking and also to set the Warning Level to High.
Ensure Show squigglies is checked, so that error locations are showed in the code editor.
Uncheck Check in Background but do check Fail build on warnings. It takes significant time for the static checker to run, and since we want to make deliberate changes step by step we want to be sure when the static checker runs, and when it finishes.
Also uncheck Infer Requires and Infer Ensures.
Save the configuration file. Whenever you now build the project the static checker will run. You will notice that building takes significantly longer. Upon errors, your build will fail, and errors will be reported in the error pane.
We're now going to replace the ArgumentException
's with preconditions, but also add post-conditions that ensure that our methods behave as expected.
Pre- and post conditions can be added to any method in C# when using the Code Contracts library. They must all be added in the beginning of the body of a method. I.e. before the actual work of the method is performed. Syntax examples below.
Contract.Requies(expression); // Pre-condition
Contract.Ensures(expression); // Post-condition
RoadVehicleweight
to the range 0.5 - 10
.Weight
is set to the value of the argument weight
.weight
to the range 0.2 - 13
.Weight
is set to the value of the argument weight
.Don't forget to remove the ArgumentException
throws as they are now superflous.
Build the project and observe the error list. We should be ok. Why? Discuss with a partner.
Actually we didn't really violate Liskov's substitution principle in the original case, nor in the one above. The argument-less constructor in the superclass indicates that there actually aren't any restrictions on the weight of a RoadVehicle and thus that the Automobile indeed is a proper specialization of the RoadVehicle.
Discuss the implications of this with a partner. What does it mean? How should it have been designed? Why is it not enough to limit the range of the constructor argument weight
?
What do you think will happen if we add invariants that force the classes to maintain their respective weight ranges? Discuss with a partner. Make sure you have a hypotheses before actually adding the invariants in the code.
Invariants in C# are added through private void returning instance methods decorated with [ContractInvariantMethod]
. These method must never be called from anywhere by the user code. Syntax as below:
[ContractInvariantMethod]
private void ObjectInvariant()
{
Contract.Invariant(expression);
}
RoadVehicle
, ensuring that Weight
remains in the range 0.5 - 10
.Automobile
, ensuring that Weight
remains in the range 0.2 - 13
.What errors do we get? Why? Did they correspond to your assumptions?
Now that we're actually limiting weight in the superclass we're successfully violating Liskov's substition principle, and thus need to redesign. Obviously we must have misinterpreted the requirements.
Rewrite the weight range rules of the superclass to something more appropriate. Rewrite it to a range that is both reasonable from a real-world-perspective, and causes the Automobile to be a proper subclass.
Add at least two methods to the superclass, and at least two methods to the subclass. One of the methods of the superclass should be overriden in the subclass.
Remember to model things that make real-world sense, and also respect Liskov's substitution principle. Remember the principles of Contravariance and Covariance that you've talked about in class.
Here are some random examples of method signatures, but you are completely free to make up any methods you see fit.
AddPassenger(string name);
RemovePassenger(Passenger p);
RemoveWheel(int position);
SetWheels(int num);
Accellerate();
TurnWheel(double deg);
Introduce Code Contracts to the Phone Book that you refined in Block 2, but originally built in Block 1. Make sure that you do consider both pre-conditions, post-conditions and invariants. Further, make sure that you always know why you are introducing a particular constraint. Lastly, do remember to respect Liskov's substitution priciple , and the concepts of covariance and contravariance.