2

According to the Liskov principle, a construction like the one below is invalid, as it strengthens a pre-condition.

I know the example is pointless/nonsense, but when I last asked a question like this, and used a more elaborate code sample, it seemed to distract people too much from the actual question.

//Data models
abstract class Argument
{
    protected $value = null;
    public function getValue()
    {
        return $this->value;
    }
    abstract public function setValue($val);
}
class Numeric extends Argument
{
    public function setValue($val)
    {
        $this->value = $val + 0;//coerce to number
        return $this;
    }
}
//used here:
abstract class Output
{
    public function printValue(Argument $arg)
    {
        echo $this->format($arg);
        return $this;
    }
    abstract public function format(Argument $arg);
}
class OutputNumeric extends Output
{
    public function format(Numeric $arg)//<-- VIOLATION!
    {
        $format = is_float($arg->getValue()) ? '%.3f' : '%d';
        return sprintf($format, $arg->getValue());
    }
}

Why would this kind of "violation" be considered harmful? So much so that some languages, like the one I used in this example (PHP), don't even allow this?

I'm not allowed to strengthen the type-hint of an abstract method but, by overriding the printValue method, I am allowed to write:

class OutputNumeric extends Output
{
    final public function printValue(Numeric $arg)
    {
        echo $this->format($arg);
    }
    public function format(Argument $arg)
    {
        $format = is_float($arg->getValue()) ? '%.3f' : '%d';
        return sprintf($format, $arg->getValue());
    }
}

But this would imply repeating myself for each and every child of Output, and makes my objects harder to reuse.

I understand why the Liskov principle exists, don't get me wrong, but I find it somewhat difficult to fathom why the signature of an abstract method in an abstract class has to be adhered to so much stricter than a non-abstract method.

Why I'm not allowed to hind at a child class, in a child class?

The way I see it, the child class OutputNumeric is a specific use-case of Output, and thus might need a specific instance of Argument, namely Numeric. Is it really so wrong of me to write code like this, when I am allowed to write this:

abstract class Output
{
    public function printValue(Argument $arg)
    {
        echo $this->format($arg);
    }
    public function(Argument $arg)
    {
        throw new RuntimeException(__METHOD__. ' Has to be overridden by child class');
    }
}
class OutputNumeric extends Output
{
    final public function format(Numeric $arg)
    {
        $format = is_float($arg->getValue()) ? '%.3f' : '%d';
        return sprintf($format, $arg->getValue());
    }
}

This sort of acchieves the same thing, but it's just hacky and, to my eye, horrid code...

5

You aren't just breaking LSP here, you're breaking the basic concept of inheritance. If you have a format(Argument $arg) in your base type, then you must have something which can match it in the derived type.

Consider that if your example were allowed, what would happen if you attempted to pass a string to OutputNumeric::format? If you had specific type information and you knew you were passing it to an OutputNumeric, it would be a type error. If you didn't and you were passing it to an Output, it wouldn't be a type error, but where could it dispatch to?

This isn't so much a violation of LSP as it is a violation of the basic contract set out by that superclass, and you can't go around violating those or suddenly your compiler can get into situations where it can't tell you that you're wrong, but it also has nowhere to dispatch the function to!

What could be a violation of LSP in this case would be if you had a NumericOutput which accepted any natural number, but also a subclass which would fail on any number higher than n. The class's contract (or "interface") hasn't changed, but the preconditions have.

  • But Numeric is a child of Argument, and therefore $foo = new Numeric; var_dump($foo instanceof Argument); yields true. If you were to pass a string to OutputNumeric::format you'll get an error, and rightfully so: the class is clearly meant not to deal with strings, but it's job is to deal with numeric values. If you pass a string, the user isn't using the objects in the right way. Of course, code like this would also have a class String extends Argument and an OutputString extends Output class – Elias Van Ootegem Nov 12 '13 at 10:12
  • Numeric's relationship to argument isn't the point. The point is that OutputNumeric's interface does not cover the input space of Output's interface. Whether it covered a subset or an entirely orthogonal set is irrelevant, because you cannot not cover that basic interface. Consider what that error would be from passing a string to OutputNumeric. It's not a missing method, because Output's interface /does/ cover that. It's not an invalid argument, again, because Output's interface covers it. It isn't thrown by your code because you have no code to handle it. – Phoshi Nov 12 '13 at 10:15
  • Ok, I'm with you so far: I can pass any child of Argument to any child of Output, because they all share printValue, and it allows for all Argument children to be passed. But because format is declared abstract, each child of Output is obliged to implement the method separately. Why not implement the same method in each child with a signature, better befitting each child's point. – Elias Van Ootegem Nov 12 '13 at 10:22
  • Every child must implement printValue separately, but each is still obliged to accept the same interface. Now, if your OutputNumeric does typecasting and breaks on non-numeric inputs that's a violation of LSP, yes. However, you simply may not break the contract or your application is not compilable, there become states in which there is no valid path to proceed. – Phoshi Nov 12 '13 at 10:26
  • 1
    Above any contracts or principles, the violation is, simply, a type error. See en.wikipedia.org/wiki/… – Thiago Silva Nov 12 '13 at 15:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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