3

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;}
    }
}
6
  • what are you using the quote formatting to signify?
    – Ewan
    Commented May 6 at 12:05
  • 2
    P.S. For an ordinary class, it's the constructor of a class that establishes an invariant for a type, and then all other methods need to make sure it is not broken. However, in their paper, Liskov and Wing explicitly exclude constructors ("creators") from the supertype specification, kind of how C# interfaces don't have them, and in line with how in DI, the instances are created elsewhere and then injected into the client - to allow for polymorphic behavior. Because of this exclusion, invariants need to be explicitly stated as a part of the type specification (in docs, or in unit tests). Commented May 6 at 13:36
  • @FilipMilovanović Is there a contradiction between first and second sentence ("however")? Because "establishing" the invariants means that the invariants are true at the end of the construction but not necessarily at the beginning. In other words, the invariant is the post-condition of the construction. Which implies indeed that constructors obey a slightly different logic than all other operations (which is especially important considering that preconditions of subtypes constructors are often strengthened, which would make many designs LSP incompatible if constructors where in the rule).
    – Christophe
    Commented May 7 at 22:51
  • @Christophe - I guess I wasn't too careful in my phrasing due to constrained space. What I was trying to say is, before even taking subtyping into consideration, if you want to convince yourself that an implementation correctly maintains an invariant during the lifetime of an object, you can look at the specific implementation details of the constructor as a starting point, and then reason your way through every state change (that are possible/allowed via the methods). 1/2 Commented May 8 at 0:14
  • What Liskov and Wing did is, they placed stringent conditions on every public method of the supertype (in terms of behavior, preconditions, postconditions, parameter lists, the value space of the parameters), but not on the constructors (as you might want subtypes to be constructed in very different ways), so the constructors themselves (their names, param lists, etc.) are not included in the type spec, but the abstract requirement for the invariant is. 2/3 Commented May 8 at 0:14

2 Answers 2

3

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.

2

An Invariant is a statement about "property" of a type, in the plain english sense.

A precondition refers to the state before a method or function

So, ShippingStrategy.FlatRate being greater than 0 is an invariant of ShippingStrategy

and Flatrate > 0 is a precondition to CalculateShippingCost

Obviously if I start changing stuff like this its going to break the code when I substitute the classes for each other.

When considering the LSP as part of general SOLID practice you don't really need to worry too much about these rules. The Idea, that you should be able to substitute one class for another, or more usually these days an Interface with any implementation, that is your main concern. Don't get caught up in these 'rules' which are supposed to be able to guarantee that substitution is possible.

In regard to the various ways of implementation, the LSP is language agnostic, so from a LSP perspective it doesn't matter how you achieve the invariants or enforce preconditions.

Putting a check in a constructor doesn't change it from a precondition to an invariant. That's determined by the concept you are implementing, not the implementation. It would be better to add a comment to the ShippingStrategy class where you list the design choices ie.

//invariants:
//FlatRate is always a positive decimal with 2 decimal places.
//It's the price in dollars.
//CalcShippingCost will error if the package size or weight is outside these bounds.... 
//The package must fit in a standard shipping container or we cant physically ship it.
public class ShippingStrategy
{
  ...
3
  • In the last implementation WorldWideShippingStrategy which is using a local field for _flatRate. So the range of the value can be anything from negative to positive. Does this breaks the the invariant contract?
    – riki
    Commented May 6 at 16:09
  • 1
    @RahulChakrabarty yes that breaks the LSP because all types of ShippingStrategy should guarantee a non negative FlatRate.
    – Ewan
    Commented May 6 at 19:58
  • 4
    Just to point out a common misconception about invariants. Let's say your code does something you weren't expecting. Based on your root cause analysis, we can spot the invariant violations: (A) "Yeah if you pass in a DerivedBar instead of a DerivedFoo you have to first do [...]" => violation. (B) "No you can't pass in a DerivedBar here" => violation. (C) "Yeah DerivedBars are going to give you a different result than DerivedFoos" => not a violation. Different behavior is the key reason for having different derived classes. As long as it's structurally compatible, it's fine.
    – Flater
    Commented May 6 at 23:26

Not the answer you're looking for? Browse other questions tagged or ask your own question.