5

There are two other questions I've posted that dealt with specific cases of this:

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

Matching the superclass's constructor's parameter list, is treating a null default value as a non-null value within a constructor a violation of LSP?

but these have kind of left me a little lost on the general case. A basic summary of what I've gotten out of the answers to these questions is as follows:

The Liskov Substitution Principle does not apply to constructors; it only applies post-construction. You can change parameter lists all you want, and you can even make matching parameters exhibit very different behaviors.

An exception to this rule is that if a callback is passed in to the constructor of both the base class and the superclass, and if it cannot be set at a later time, then the subclass's version, in normal situations, must be backwards-compatible with superclass's version. The reason for this is that, in normal situations, outside code cannot otherwise enter into a state in which it would behave in the same way.

Even though this statement doesn't necessarily contradict itself, it comes close, yet only on a halfway fine-grained point. So at this point, it's probably a good idea for me to ask about the subprinciple of LSP that deals specifically with constructors, particularly with their parameter lists.

What is the general application of LSP here?


Example

Consider the following example, which only illustrates certain specific points (this question is still about the general case though, not this example):

BasicButton:

public class BasicButton extends Sprite
{
    private var m_fOnClick:Function;
    private var m_fOnPress:Function;
    private var m_fOnRelease:Function;

    private var m_iColorPressed:uint;
    private var m_iColorReleased:uint;

    public function BasicButton(pColorPressed:uint, pColorReleased:uint, pOnClick:Function
            = null, pOnPress:Function = null, pOnRelease:Function = null)
    {
        m_iColorPressed = pColorPressed;
        m_iColorReleased = pColorReleased;
        m_fOnClick = pOnClick;
        m_fOnPress = pOnPress;
        m_fOnRelease = pOnRelease;
        drawBackground(pColorReleased);
    }

    private function drawBackground(pColor:uint):void
    {
        // completely fill the entire rectangular area of the button with one solid color
    }

    .
    .
    .
        // when the button has been pressed:
        if (m_fOnPress)
        {
            m_fOnPress();
        }
    .
    .
    .
}

DirectionalButton:

// This class illustrates several different points.
public final class DirectionalButton extends BasicButton
{
    private var m_eDirection:int;

    private var m_fOnPress:Function;

    // Multiple parameters are omitted.  Two are just uint configurations to decide a color
    // with, and one is a callback that, if a non-null value had been included, would have
    // interacted directly with outside code.  Furthermore the pOnPress callback is treated
    // differently throughout this class.  Finally, pOnPress and pOnRelease are no longer
    // optional.
    public function DirectionalButton(pDirection:int, pOnPress:Function,
            pOnRelase:Function)
    {
        m_eDirection = pDirection;
        m_fOnClick = pOnClick;

        // pOnPress is nullified; the superclass won't even try to call it.
        // The colors are hard-coded.
        super(0x858585, 0xCCCCCC, null, onPress, pOnRelease); 

        addArrowSprite(pDirection);
    }

    private function addArrowSprite(pDirection:int):void
    {
        // Add a new Sprite instance as a child, which will take the form of an arrow
        // pointing in the specified direction, using a different color than the
        // background.  Whereas the you could always assume that the superclass maintained
        // one color throughout at all times, this assumption will now be broken in the
        // subclass.  That means that, depending on how you interpret things, the
        // background color specified in the superclass's constructor uses either differing
        // or equivalent behavior.
    }

    private function onPress():void
    {
        m_fOnPress(m_eDirection); // pOnPress from before has had its parameter
                                  // list interfered with.  Also it is now assumed to be
                                  // non-null.
    }
}

Remember that this example is still only dealing with the constructors' parameters, not with regular public functions or anything. Furthermore you have several different so-called "properties" that are suppliable to the constructors, but which have no way to be configured afterward.

Whether dealing with these sorts of cases, or with different types of cases, what is the LSP rule about constructors, especially with their parameter lists?

2
  • 5
    I'd say LSP talks about properties of objects, the constructor however is called while bringing the object into existence and isn't part of "property provable about objects" ... but others might have better supported answers :-)
    – johannes
    Commented Jan 21, 2015 at 14:52
  • 3
    Reference to the actual paper: reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.pdf
    – snakehiss
    Commented Aug 2, 2015 at 22:26

3 Answers 3

9

It depends on what language you're working with.

Let's look at the informal definition of the Liskov Substitution Principle (herafter LSP):

You should be able to use an instance of a subtype anywhere you could use the base type.

That only tangentially impacts constructors. LSP doesn't particularly care how instances are created, only that once created they play nice. Constructors obviously can impact if the instances play nice when used, but that's not really different from other object state impacting their behavior.

In a few more esoteric languages though, the types (and their constructors) can be treated like objects. Having the constructors behave differently in that sort of context would be a violation of LSP, but of the type objects, not the actual class instances.

5
  • 1
    For example, in Ruby, classes are themselves objects. And there are no constructors, only methods. So, in theory, instances of subclasses of Class should be substitutable for instances of Class … but Ruby doesn't allow Class to be subclassed anyway, so the problem doesn't even arise. (Plus, classes aren't types in Ruby, so instances of subclasses aren't necessarily instances of subtypes.) Commented Jan 21, 2015 at 17:55
  • Thanks! Where does the exception above fit in? Commented Jan 21, 2015 at 22:03
  • 2
    @Panzercrisis - to be honest, it's not entirely clear. But if I understand properly, I don't agree that it's an exception. If you have a callback defined in both places then any method that satisfies the callback is satisfying the contract stipulated by LSP. It's like saying you can't pass 4 into the subtype and 2 into the basetype because then it will do different things. While that might not be a good idea, there's no clear substitute-ability violation going on there.
    – Telastyn
    Commented Jan 21, 2015 at 22:10
  • The exception I was thinking of was basically the same thing as the way pOnClick is handled in the two code snippets. It's only passed in during construction, never post-construction, but it'll either be called with one argument, be called with no arguments, or just not be called in later code, depending on what's passed to which constructor. That doesn't change the way outside code interfaces with member functions or variables post-construction, but it does relate somewhat to the way outside code has to define the callback itself. Commented Jan 22, 2015 at 21:57
  • If that's not an exception, there's probably not too much that is; but if it is an exception, it makes the line in the dust hard to see for me personally. Commented Jan 22, 2015 at 22:01
4

In my opinion, constructors don't relate to LSP. I can argue this in two ways, both on a technical level and on the spirit of the principle. I'll do both.

Before I delve into it, I do want to acknowledge the caveat from the already posted answer:

In a few more esoteric languages though, the types (and their constructors) can be treated like objects.

The answerer is correct that some languages do allow for this, but I would argue that this is not what LSP was focusing on when it was originally coined. The definition of LSP makes it clear, in my opinion, that it was focusing on statically typed class definitions, not dynamically assignable lambdas.

And in the latter case, you would only really interchange the constructors if they had the same signature (i.e. input parameters), which is not an inherent constraint for derived class constructors. If the signature is the same, that is either coincidental or by willful design, not because the language enforced or somehow mandated it.

There's room for discussion on that point, but I am going to simply sidestep it for the sake of answering your question, which I suspect was not focusing on this edge case anyway.


Firstly,

on a technical level: you can't inherit constructors. They are uniquely scoped to the class in which they are located, regardless of whether that class derives from something else.

Yes, you have to chain the derived constructor to one of the base class' constructors; but the signature of your derived constructor is not directly based on that of the base constructor.

Essentially, LSP does not concern itself with unique signatures found only in the derived class. Take this example:

public class Base
{
    private readonly int a;

    public Base(int a)
    {
        this.a = a;
    }

    public virtual int MyBaseMethod()
    {
        // Some logic
    }
}

public class Derived
{
    private readonly int b;

    public Derived(int a, int b) : base (b)
    {
        this.b = b;
    }

    public override int MyBaseMethod()
    {
        // Some logic
    }

    public virtual int MyDerivedMethod()
    {
        // Some logic
    }
}

The key thing to consider here is that MyBaseMethod is subject to LSP considerations, but MyDerivedMethod is not.

MyDerivedMethod cannot possibly violate LSP in this example. Base has no definition of MyDerivedMethod and therefore Derived couldn't be guilty of "changing the contract" of the MyDerivedMethod method.

Given that MyDerivedMethod is unrelated to LSP because it is only defined in Derived, not Base; the same argument applies to the constructor of Derived, which Base has no definition for either.


Secondly,

on a spiritual level, LSP exists in the spirit of making sure that polymorphism doesn't introduce unexpected behavior. Consider this code:

public void DoSomething(Base b)
{
    var price = b.MyBaseMethod();

    // ...
}

If you build a Derived : Base class which violates LSP, what this means is that this code will behave unexpectedly differently when you pass a Derived object into it, compared to when you pass a Base object into it.

For the sake of thought exercise, consider this second example:

public void DoSomethingElse(Derived d)
{
    var price = d.MyBaseMethod();

    // ...
}

While we can definitely argue that LSP is defined by bad class design, not how the class is used by consumers; I do think it is correct to say that DoSomethingElse is not direct proof of an LSP violation, because there is never any expectation of d acting as if it were a Base object.
d couldn't possible be just a Base instance, because the method parameter mandates that the object is at least a Derived object (or further derivation thereof).

With this in mind, for the purpose of the point I'm trying to make here, I could rephrase LSP to something like:

A derived class should respect and maintain the intended behavior of the base class that it has chosen to inherit from, so that when the derived object is disguised as if it were an instance of the base class, the consuming logic need not be aware that it is in fact a derived object and not "just" a base object.

What this means is that LSP only really targets potential problem scenarios whereby a base-type variable unknowingly contains a derived-type object. Or, in code:

Base a    = new Base();        // Further usage of a is not an LSP concern

Derived b = new Derived();     // Further usage of b is not an LSP concern

Derived c = new Base();        // Not syntactically valid

Base d    = new Derived();     // Further usage of d is an LSP concern

When dealing with constructors, you must be aware of what concrete class you're instantiating. There is no possible way that you can construct a Derived object while only being aware of the Base type, and therefore LSP concerns do not apply.

What this means is that this line:

Base myObj = new Derived();

by itself is not an LSP concern, because you are acutely aware that you're dealing with a Derived object. However, any subsequent code that makes use of myObj is no longer aware what the real type of the myObj instance is, and therefore this subsequent code is subject to LSP concerns.

1

The LSP was coined by Barbara Liskov in the context of class hierarchies and OOP/OOD. Typically, in examples, you have a base class and a derived class and some code using a base instance. This code is then called with a derived instance to show that it works that way as well. Another ofter occurring example uses a base class (or a pure interface, depending on the language) and two derived classes (implementing that interface). Similarly, the example code is called with instances of the two derived classes to show that it doesn't need to be adjusted to work with either.

There is one thing that's required by the LSP and practically true in many languages, but not necessarily in general: The two instances the code is called with don't have to be related via some language construct at all, neither via a base class nor via a pure interface. Some languages will make this work just as well without, as long as objects support the interface implicitly defined by those methods called by the code.

If you take a further abstraction step back from the LSP, it then means that you can call C(A) and C(B) without having to change the code in C. In the examples mentioned above, A and B are instances of different classes. This doesn't have to be the case though, nor is this enough. Let's illustrate that claim with some examples...

Example 1

You call C with two class instances, one derived from the other (classic LSP example). The base class defines a method doSomething(). The derived class overrides this method but throws a NotImplemented() exception. This will pass any static type check (e.g. a compiler will say it's okay), but radically change the behaviour so that actual substitution can not take place.

Example 1b

Similar to example 1a, only that you don't have two classes but simple floating point numbers. Code may well work in general but fail if you pass infinities or NaN.

Example 1c

Similar to example 1b, only that you pass a value that overflows. Or an empty string where it is not expected. Or a zero by which is then divided. Or a negative value who's square root is taken.

Note: Having a common base class or interface or even being the exact same type does not guarantee that two objects are exchangable. However, in many cases, they are, which explains why this is often confused.

Example 2

You have instances of two unrelated classes, e.g. a string A and a message queue B. There could be some code C that outputs the length of either object, which works because both have a length property. This can't be written as statically typed code, because that requires C to declare up front whether it wants a string or a message queue and it will reject the other. Some languages force you to write statically typed code, so you can't write this kind of code with them.

Note: This example shows that having similar or related types is not necessary at all for substitutability.

Example 3

You have two classes A and B and code C which takes a class as a parameter and returns an instance of that class. In other words, you have the simplest version of the factory pattern. Note again that you can't write that as statically typed code, because you can't declare the return type up front. In these languages, you will then find a common base class or interface as return type. Also, since classes there are often compile-time only constructs and not available as parameters, you will instead see a name identifying the class as parameter instead of the class itself and also a registry mapping each class name to a function for just that class.

For the implementation of the factory code in C, it is necessary that both new A(p..) and new B(p..) works and with the exact same parameters p... Only then you can really exchange A and B.

Note: Even if the two classes A and B here are exchangable, it doesn't follow that the same applies to instances of those two classes. If one is a string class and the other a message queue class, they may both accept an empty parameter list in their constructor, but that's about all they have in common.

Generalization Of The LSP

The original LSP applies to class hierarchies in OOP/OOD. It talks about instances of classes, not about how those instances are created. In that sense, the LSP doesn't apply to constructors.

As illustrated above, the original LSP is neither sufficient nor necessary for well-behaved programs. The more general ability to substitute A with B is relevant though, because it enables you to write C without having to include custom code for A, B or any other possible substitute. What is actually required is that the substitute conforms to the interface required and defined by C. In the worst case, that interface is only defined implicitly be the code in C. In order to better define that interface, static typing is supported in many languages, also classes and subclassing, and also plain old comments to guide the developer. Further, compilers/linters enforce rules and can also be customized, e.g. with an annotation that a parameter must not be null.

In summary, whether you want to include your constructor interface or not is up to where you want to exchange one class for the other. If you're merely substituting instances (which have been constructed already), then the constructor is irrelevant. If your code creates instances, the constructor is relevant.

On Statically Typed Languages

Many languages are actually hybrids which support static typing but don't require it:

  • Python is very dynamic, but allows you to add type annotations to functions.
  • PHP allows you to declare the type of a parameter but doesn't force you to.
  • C++ is rather static, but allows you to write generic code using its template mechanism.
  • Java has generics, similar to C++ templates.
  • Go has generics, similar to C++ templates.

Static typing is a tool that makes it easier to achieve substitutability, which is why many languages (even dynamic ones like Python) support it.

On Interfaces

Many languages have an "interface" feature:

  • Python has an ABC (Abstract Base Class) module.
  • PHP has an interface keyword to define a class interface.
  • C++ has abstract = 0 methods, which is an interface on the level of a method instead of the class level.
  • Java has an interface keyword to define a class interface.
  • Go has an interface keyword to define a class interface.

Such an interface is similar to a baseclass, but explicitly excludes an implementation that is inherited. It forces you to implement a set of methods in a class implementing the interface, which again makes it easier to achieve substitutability.

2
  • Did you type all this? It looks like A.I. generated.
    – Dominique
    Commented Jan 10 at 10:43
  • 2
    I did type it. What makes it look like it was generated? Commented Jan 10 at 10:46

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.