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
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
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.
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