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(&quot;Term needs to be above zero &quot;);
}

}

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

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

}

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(&quot;Term needs to be between 1 and 30 &quot;);
}

}

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-&gt;balance = $balanceInicial;   
}

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

    return $this-&gt;balance;
}

}

class AccountVip extends Account { private const TAXA = 10.00;

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

    return $this-&gt;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?

Martin Maat
  • 18,652
celsowm
  • 253
  • 2
  • 11

2 Answers2

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

Christophe
  • 81,699
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