3

I have some doubts about practical ways to violate the Liskov Substitution Principle regarding pre and post conditions.

I created examples in which I believe the first child respects LSP and the second violates it. But the Liskov article is very mathematical and now I am not so sure.

The first example is about pre conditions:

<?php

class TermCalculator
{
    public function data(int $dias): DateTimeInterface
    {
        if ($dias > 0)
        {
            return (new DateTime())->modify("+$dias days");
        }

        throw new \InvalidArgumentException("Term needs to be above zero ");
    }
}

class TermCalculatorCLT extends TermCalculator
{
    public function data(int $dias): DateTimeInterface
    {
        if ($dias >= 0)
        {
            return (new DateTime())->modify("+$dias days");
        }

        throw new \InvalidArgumentException("Term needs to be above -1");
    }
}

class TermCalculatorCPC extends TermCalculator
{
    public function data(int $dias): DateTimeInterface
    {
        if (in_array($dias, range(1,30)))
        {
            return (new DateTime())->modify("+$dias days");
        }

        throw new \InvalidArgumentException("Term needs to be between 1 and 30 ");
    }
}

Which child class above violates LSP by pre conditions?

These are two situations about post conditions:

<?php
class Account
{
    protected float $balance;

    public function __construct(float $balanceInicial)
    {
        $this->balance = $balanceInicial;   
    }

    public function withdraw(float $value) : float
    { 
        if (($this->balance - $value) >= 0)
        {
          $this->balance -= $value;
        }

        return $this->balance;
    }
}

class AccountVip extends Account
{
    private const TAXA = 10.00;

    public function withdraw(float $value) : float
    {
        if (($this->balance - $value) >= self::TAXA)
        {
            $this->balance -= $value;
        }

        return $this->balance;
    }
}

class AccountIlimited extends Account
{
    public function withdraw(float $value) : float
    {
        $this->balance -= $value;
        return $this->balance;
    }
}

Which child class above violates LSP by post conditions?

2

2 Answers 2

5

The Liskov Substitution Principle (LSP) requires that all the child could be used interchangeably with their parents.

This means concretely that the child must work with the tests prepared for the parents. So if you have any pre-conditions for your parents, the child shall not strengthen them. Conversely, the child cannot weaken the post-conditions of the parent.

In your case:

  • TermCalculator has a precondition of $dias>0. It the precondition is not met, it throws.
  • TermCalculatorCLT has a precondition of $dias>=0. It's weaker than the precodintion of the parent, because it accepts the special case of $dias being 0 while the parent not. Weakening the parent conditions means that it doesn't strengthen them. So this is compliant with LSP.
  • TermCalculatorCPC has a predindition of $dias>0 and $dias<31. It's stronger than the precondition of the parent (i.e. more selective). Take the example of 32: it would work for the parent, but it would throw for the child. This means that the child might not pass all the tests of the parent. Tehrefore it doesn't respect LSP.

I leave you as an excercise the second case. If it's really to difficult, you may post as comment to this answer your guess and explanations of your choice, and I'd edit my answer accordingly ;-)

5
  • 1
    This means concretely that the child must work with the tests prepared for the parents - simple and easy to check
    – Fabio
    Jan 29, 2020 at 20:21
  • I think having a child class that can work in all tests written for the parent is a necessary but not sufficient condition for LSP compliance. If the parent guarantees immutability or some other form of change constraint (e.g. a list that can only be appended to) the child must respect that. The test for the parent won't have a call to $list->removeItem() since that doesn't exist on the parent. If it does exist on the child it's an LSP violation.
    – bdsl
    Jan 29, 2020 at 23:08
  • @bdsl I restricted my answer to the specific case of the questiin. Liskov’s argument is indeed not about passing the tests of the it’s parent but about being substitute for their parents. This means that invarients must be respected, that postconditions are not weakened (eg immutable for the parent requires immutable for the child. Last but bot least the child shall respect the history rule.
    – Christophe
    Jan 29, 2020 at 23:28
  • Thanks @Christophe, so Can we say that AccountVip has a stronger post-condition, because returns only >= TAXA and so is valid for LSP and, in other hand, AccountIlimited can returns negative number, weaking the original post-condition (only positive numbers), violating LSP, right?
    – celsowm
    Jan 30, 2020 at 0:41
  • @celsowm yes, that’s the principle. Assuming that the post condition is that balance must be positive or null. Note that in the first case, the precondition is coded (exception raised). But in the second case there is not such a post condition check. So we guessed it for the exercise. Do you have a test case that states that balance should be positive? in real life there is not necessarily a postcondition here. LSP is about contracts, i.e. promises made about the code and not the code itself. If postcondition would be that new balance is equal or smaler than initial balance, all classes comply
    – Christophe
    Jan 30, 2020 at 7:14
1

While the answer above is technically correct in mathematical terms I thought it may be useful to speak of this in practical terms. There is probably a reason our classes don't require preconditions and postconditions to be expressly called out in our interfaces.

If you just wanted to 'get around' the technical violation of the LSP you could simply derive all your substitutable classes from an abstract class. i.e.

abstract class Calculator {
    abstract public function data(int $dias): DateTimeInterface;
}

Now you are no longer violating the LSP and your program behaves exactly the same.

OR

You can create an interface for all the calculator's to implement.

interface ICalculator {
    public function data(int $dias): DateTimeInterface;
}

so, since your program still behaves exactly the same but no longer violates the principle does the LSP even matter?

The principle states:

if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program

One could argue that while the principle applies to all programs P you are writing a specific program, call it Q and the design of your specific program may benefit immensely from the ability to substitute one subclass for another and your specific program Q may NOT observe an UNDESIRED change in behavior.

The LSP provided the notion that you could design your software to make objects substitute-able. This is why it is so valuable to software engineering; because of the ability to substitute objects that satisfy a similar purpose our software becomes more easily maintainable, our design more flexible and unit tests can be used more efficiently to ensure that specific application rules are simple to test.

This is the way it is generally discussed, without reference to the pre and post condition details that our languages do not generally enforce. Even Uncle Bob who has made the LSP quite famous does not mention them in his book on clean architecture. Here is another example from the author of "Getting your hands dirty on clean architecture" https://reflectoring.io/lsp-explained/

So while it may be critical for one program to adhere strictly to the limitation on pre and post conditions, for another program it may be sufficient to substitute child classes for each other without concern for this criteria. If yours is such a case you can use abstract classes or interfaces to implement your solution without stepping on the LSP.

Also look for patterns like the strategy pattern that you may be implementing and see how they are documented because they will have been designed with these principles in mind. i.e. https://refactoring.guru/design-patterns/strategy

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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