1
votes

I'm trying to learn OOP and OOD principles in correct way. I would like to get some clarification on Liskov substitution principle and their PRE and POST conditions. I have read some topics here, some articles from http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod and other places.

I have written a simple base class and several example sub classes with my assumptions on pre and post conditions for very of them and i would like to know if they are correct. Comment lines is what i think: is it violates or not PRE and POST conditions.

public abstract class BaseClass
{
    public virtual int GetResult(int x, int y)
    {
        if (x > 10 && y < 20)
        {
            return y - x;
        }
        throw new Exception();
    }
}

public class LSPExample1 : BaseClass
{
    public override int GetResult(int x, int y)
    {
        // PRE: weakened pre condition is ok
        if (x > 10 && y <= 15)
        {
            // POST: Is it ok? because the available result range is narrowed by y <= 15 
            return y - x;
        }
        throw new Exception();
    }
}

public class LSPExample2 : BaseClass
{
    public override int GetResult(int x, int y)
    {
        // PRE: Same as base - OK
        if (x > 10 && y < 20)
        {
            // POST: I assume it's bad because of parameters place changed in substraction ?
            return x-y;
        }
        throw new Exception();
    }
}

public class LSPExample3 : BaseClass
{
    public override int GetResult(int x, int y)
    {
        // PRE Condition is bad (Strenghtened) because of (X >5) and (Y>20) ?
        if (x > 5 && y > 20)
        {
            // POST condition is ok because base class do substraction which is weaker than multiplication ?
            return x * y;
        }
        throw new Exception();
    }
}

I would really appreciate your time

1

1 Answers

5
votes

This is one of those wonderful situations where you can actually go to the source. Barbara Liskov's original paper is available and quite accessible and easy to read. http://csnell.net/computerscience/Liskov_subtypes.pdf

All four GetResult methods you show accept two integers as input and either return an integer, or throw an exception. In this respect they all have the same behavior so it could be said that LSP is fully satisfied. Without explicit pre/postconditions or invariants, nothing else can really be said about the code.

What is missing in your example is the contract. What does GetResult require from its caller and what does it guarantee it will produce?

If, for example, the contract guarantees that the return value will equal y - x then Examples 2 and 3 fail the contract and therefore break LSP. If, however, the only guarantee is that the return value will be an Int or Exception, then they all pass.

If the contract guarantees that and exception will be thrown if x <=10 || y >= 20 then Example 1 & 3 break LSP. If the only guarantee is that the method will return an Int or throw an Exception, then they all satisfy it.

The code can't tell you what the guarantees are, it can only tell you want the code does.

Since I'm getting lots of up-votes, I'll add an example (pseudo-code):

class Line {
    int start
    int end
    int length() { return end - start } // ensure: length = end - start

    void updateLength(int value) {
        end = start + value
        // ensure: this.length == value
    }
}

The "ensure" clause in each of the two functions are guarantees regarding the state of a Line object after the function is called. As long as I satisfy the guarantees in sub-classes, my sub-class will conform to LSP. So for example:

class Example1: Line {
    void updateLength(int value) {
        start = end - value
    }
}

The above satisfies LSP.

If the Line's updateLength function also has an ensure clause of "this.start unchanged" then my subclass would not satisfy LSP.