Where does the Liskov Substitution Principle lie in a subclass passing extra arguments to similar, tightly-related callbacks?

softwareengineering.stackexchange https://softwareengineering.stackexchange.com/questions/270666

Question

Take the following examples, which have been abbreviated:

BasicButton:

public class BasicButton
{
    private var m_fOnClick:Function;

    public function BasicButton(pOnClick:Function)
    {
        m_fOnClick = pOnClick;
    }

    .
    .
    .
        // when the button has been clicked:
        m_fOnClick();
    .
    .
    .
}

DirectionalButton:

public final class DirectionalButton extends BasicButton
{
    private var m_eDirection:int;

    private var m_fOnClick:Function;

    public function DirectionalButton(pDirection:int, pOnClick:Function)
    {
        m_eDirection = pDirection;
        m_fOnClick = pOnClick;
        super(onClick);
    }

    private function onClick():void
    {
        m_fOnClick(m_eDirection);
    }
}

Is messing with the callback's parameter list like this a violation of the Liskov Substitution Principle? I know the constructors don't violate it, according to the way the industry generally interprets that principle, but whether messing with the callback's parameter list constitutes a mutation of a common output or whatever seems to be a little less clear.

On the one hand, both classes use a function called pOnClick that is called each time they're clicked on, and the instantiating code is able to pass in whichever function it wants to be referred to by pOnClick. On the other hand, since constructors tend to be abstracted from LSP, these two pOnClick parameters in the two constructors could be interpreted as referring to very different things.

Even if the two pOnClick parameters are interpreted as two separate inputs, there's still some ambiguity as to how much the subclasses's pOnClick effectively overwrites or destroys the superclass's (in a strictly virtual, publicly exposed sense), and how much it instead simply passes - in a very virtual sense - the equivalent of null to the superclass's still existent pOnClick.

It's a little tough to find the exact words for what I'm describing, but in general, where does LSP lie in this? Is this a violation? If so, can it be remedied by simply rewriting DirectionalButton.onClick as follows?

private function onClick():void
{
    try
    {
        m_fOnClick(m_eDirection);
    }
    catch (e:Error)
    {
        m_fOnClick(); // This is not how I would set this up in real life.
    }
}
Was it helpful?

Solution

The test is whether you can take code written to interact with the base class, and without modification, get it to work with an instance of the derived class instead. That means in most circumstances, your example would violate LSP.

However, in some circumstances it might not. If you're using a language like JavaScript that essentially ignores extra parameters, and that parameter is not semantically crucial, you could make a case that it doesn't violate LSP.

In a dynamically-typed language, you could make a case that your proposed fallback remedy doesn't violate LSP. However, I don't believe you could get something like that to compile in most statically-typed languages.

One remedy that would work in any language is to just change the base class to add the optional parameter. That feels kind of icky, though.

The easiest way to not accidentally break inheritance design principles is to not use inheritance. You could have your directional button class contain a basic button instead of inheriting from it, and forward any events with the parameter added. This is a good option if you never have to put a directional button into code that was originally designed to interact with a basic one.

Another very flexible option to pass arbitrary data back to a callback is to pass in a closure to the constructor instead.

Licensed under: CC-BY-SA with attribution
scroll top