Invariant rule in Liskov Substitution Principle

cliskov-substitutionsolid

From Liskov Substitution Principle, I am still not very clear about the invariant rule. I read through many posts but I still have doubts.

My example is picked from this blog, the example is slightly modified in this question to "simplify" the example for my onw understanding purposes, which eventually created the doubts. – from the blog in the section: "Invariants must be maintained" in the link.

From LSP: Invariant cannot be weakened in the subtype.

For an example:

public class ShippingStrategy
{
    private decimal _flatRate;
    public virtual decimal FlatRate
    {
        get { return _flatRate; }
        set
        {
            if (value <= decimal.Zero)
            {
                throw new ArgumentOutOfRangeException("value", "Flat rate must be positive and non - zero");
            }
            _flatRate = value;
        }
    }
}
public class AsiaPacificShippingStrategy : ShippingStrategy
{
    public override decimal FlatRate
    {
        get { return base.FlatRate; }
        set
        {
            if (value <= 10m) // m == decimal denotation for c#
            {
                throw new ArgumentOutOfRangeException("value", "Flat rate greater than 10");
            }
            base.FlatRate = value;
        }
    }
}
public class EuropeShippingStrategy : ShippingStrategy
{
    public override decimal FlatRate
    {
        get { return base.FlatRate; }
        set { base.FlatRate = value; }
    }
}
public class WorldWideShippingStrategy : ShippingStrategy
{
    private decimal _flatRate; // local field
    public override decimal FlatRate
    {
        get { return _flatRate; }
        set { _flatRate = value; }
    }
}

What have here is:

The parent, ShippingStrategy has a property call FlatRate and its condition is the value has to be greater than zero.

Now in the AsiaPacificShippingStrategy, it overrides the property and tightening the condition – I think that complies with LSP.

Now in EuropeShippingStrategy it is weakening the condition – which I feel like breaking LSP and same for WorldWideShippingStrategy, I guess.

I am not clear about a few things-

First, these all feels like the precondition rule to me. – but again FlatRate with its own condition makes itself as a candidate for encapsulation into its own type. So that distinguishes it from the precondition rule. Am I correct here?

The example in the post took a different approach to explain the invariant rule. The parent uses a constructor to set the value for flatRate and has the FlatRate as a protected member. Then method hiding is used in the child to override it.

Second, Well, I think we are achieving the same demonstration of invariant rule(?) but what I am unclear is that in the below example why parent class is only lettings to set the flatRate using a constructor and not directly by the property and so is FlatRate marked protected. Is there something that I am missing to understand?

public class ShippingStrategy
{
    private decimal flatRate;
    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;
        }
    }
}

public class WorldWideShippingStrategy : ShippingStrategy
{
    public WorldWideShippingStrategy(decimal flatRate) : base(flatRate){}
    public new decimal FlatRate
    {
        get{return base.FlatRate; }
        set{base.FlatRate = value;}
    }
}

Best Answer

Why this invariant rule?

A class invariant is a condition that is guaranteed to be true for the lifetime of an object, i.e. once the object construction is finished, up to the start to its destruction, whatever class operation is performed. The invariants are part of the contract offered by the class, and the using code should be able to rely on it.

The very idea of LSP is to enable the substitution of an object of the supertype with an object of the subtype. If the subtype would be allowed to weaken the invariant, some assumptions of the "calling" code would no longer be true and the system as a whole might no longer work as expected. If the subtype strengthen the invariants, then the assumptions of the "calling" code would still be guaranteed.

Why does your first snippet infringe LSP?

LSP is about the contract offered, not about its implementation: You deduce the contract from the code, considering that throwing is the enforcement of the invariant. In this case, the two last subtypes indeed infringe LSP. But be aware that contracts may also be documented and some of such contracts may foresee throwing as a promised outcome.

Is your revised code better?

In your revised code, you move the throwing to the constructor. In this case you have more freedom, because LSP's invariant rule does not apply to the construction process itself. Liskov and Wing explained it in their foundational paper:

Objects come into existence and get their initial values through creators. Unlike other kinds of methods, creators do not belong to particular objects, but rather are independent operations.

However, you only move the problem and at the same time alter the exposed interface. Moreover, protected members are not recommended regarding LSP, as they create the risks of infringing the history rule. Moreover, in your second snippet the common interface exposed by the base and its specialisation is rather limited. So this design is flawed for a lot of reasons.

How to get it right?

Let's reframe the problem: your base class ShippingPolicy enforces invariants that are obviously not relevant for all possible policies. Instead of changing the exposed interface dramatically and rewrite all the using code, let's just refactor the hierarchy correctly.

The solution is to refactor the the shipping policy into one base class, with the right invariants that would apply to every possible shipping policy subtype, and another class subtyping this common base, that would have the more restricted invariants. All the other policies would then be subtypes from the common base, making them LSP compliant.

Note that an inspection of the "calling" code is required to verify that it does not rely on the prematurely strengthened invariants of the old shipping class, or if on should use the instantiation of the new shipping class.

Related Topic