Software Engineering — Lab 4

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.

Deliverables

Please follow the below folder/file structure for the handin of this block.

    B4.zip
    1. Ex1
      1. Vehicles (VS solution)

4.1 State-space, behavior and Code Contracts

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.

Setting up Visual Studio to use Code Contracts

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.

Pre- and post-conditions

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
RoadVehicleAutomobile

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.

Adding invariants

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);
}

What errors do we get? Why? Did they correspond to your assumptions?

Reinterpreting the requirements

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 methods

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);