Liskov Substitution Principle (LSP)
This is part of my SOLID Software Principles in Practice post.
LSP is named after Barbara Liskov and deals with describing how subtypes must be able to be used in place of their supertypes without breaking program functionality. This is one of the most difficult LSP principles to understand, so I have broken this one into several sections, which line up to the various LSP rules.
Note that I use the term subtype rather than subclass. Subtype has a much stronger definition (Subtyping) and is used to indicate that the type has been designed to be used in place of its supertype in a way that will not break a program. i.e. a subclass relation does NOT imply a subtype relation!
As a side note, parts of LSP overlap a software design approach called Design by contract (Dbc)
Method Signature Rules
Contravariance of Arguments
The two conditions which must be satisfied for this rule are as follows:
- A subclass implementing a method of its superclass must have the same number of parameters.
- The type of each parameter in the subclass must be the same or a supertype of the type used in the respective parameter in the superclass method. i.e. they can’t be more specific.
The first condition is trivial in OO languages, as if you define a method with the same name as a superclass method, but with a different number of parameters, than that method is sad to be overloaded, rather than overridden.
The second condition is best explained by example. Consider the following class diagram.
Here we have a special type of Invader called DiveBomber which has overridden checkCollision() The parameter type of DiveBomber.checkCollision() is Projectile, which is a superclass of Missile, therefore this condition is satisfied. Note that, if the parameter type was Missile, the condition would still be satisfied. However, were it of type GuidedMisile, we would break the condition, as it is a more specific type than the superclass is using.
Note that when using a statically typed language, such as C# or Java, the compiler will not let you make this mistake.
Covariance of Result
The two conditions which must be satisfied for this rule are as follows:
- Either both the subclass and superclass methods return a result, or they don’t.
- When they do return a result, the subclass method must return the same type or a subtype of the result returned by the superclass method. i.e. it can’t me more general.
Consider the following code (based on the class diagram above):
public class Invader {
public Missile checkCollision(){...}
}
public final class DiveBomber extends Invader {
@Override
public GuidedMissile checkCollision(){...}
}
DiveBomber.checkCollision()’s return type is GuidedMisile, wich is a more specific type than Invader.checkCollision() is returning, so this condition is satisfied. Were we to change DiveBomber.checkCollision() to return a Projectile, we would break the condition. Again, for statically typed languages, compilers will stop you from making this mistake.
Exception Rule
This rule states that any exceptions thrown by a subclass method should be the same, or a subtype of the exception thrown by the respective superclass method.
Here’s an example of that rule being violated:
public class Main {
static class GraphicsException extends RuntimeException {
}
static final class OpenGlException extends RuntimeException {
}
static class Invaders {
public void draw() {
throw new GraphicsException();
}
}
static class FancyInvaders extends Invaders {
@Override
public void draw() {
throw new OpenGlException();
}
}
public static void main(String[] args) {
Invaders invaders = new FancyInvaders();
try {
invaders.draw();
} catch (GraphicsException e) {
// Will NOT catch OpenGlException as FancyInvaders violates LSP :(
}
}
}
In the above code, FancyInvaders.draw() violates the exception rule, as OpenGlException is NOT a subclass of GraphicsException. This means that the exception will NOT be caught in the main method and instead the program will bomb out with an error.
Were we to change the exception class hierarchy to:
static class GraphicsException extends RuntimeException {
}
static final class OpenGlException extends GraphicsException {
}
then, the exception would be handled correctly, as in this case, OpenGlException “is a” GraphicsException.
Note that in the java code above, I am using unchecked exceptions. This means that the compiler can’t help us out here. If we were to change the exception class hierarchy as follows:
static class GraphicsException extends Exception {
}
static final class OpenGlException extends Exception {
}
then we are now declaring checked exceptions, which means the compiler will prevent us from violating the LSP exception rule. So, we need to be extra careful when using a language that doesn’t support checked exceptions, such as C#.
Method Condition Rules
Method Pre-condition Rule
A pre-condition refers to something that can be asserted about the state of a system/program before a method/function executes.
The pre-condition rule states that the pre-conditions required by subclass methods must not be stronger than the pre-conditions required by the respective superclass method. Here’s some example code to clarify this rule.
static class Invader {
boolean checkCollision(Missile missile) {
if (missile == null) {
return false;
} else {
return missile.intersects(this);
}
}
}
static final class DiveBomber extends Invader {
@Override
boolean checkCollision(Missile missile) {
if (missile.active()) {
return missile.intersects(this);
}
return false;
}
}
In this example, Invader.checkCollision() can handle nulls being passed. So, the pre-condition here is that the missile being checked can be null. Consider the following code, which is banking on that assertion:
static final class Invaders {
private Missile[] missiles = {
null, null, null
};
private Invader[] invaders = {
new Invader(), new Invader()
};
public Invader nextHit() {
for (Invader invader : invaders) {
for (Missile missile : missiles) {
if (invader.checkCollision(missile)) {
return invader;
}
}
}
return null;
}
}
This will work fine. However consider what will happen when we introduce DivBomber into the mix:
private Invader[] invaders = {
new Invader(), new DiveBomber()
};
DiveBomber.checkCollision() strengthens the pre-condition rule, as it doesn’t allow for null Missile instances. So, when the program runs now, it will throw a NullPointerException.
Method Post-condition Rule
A post-condition refers to something that can be asserted about the state of a system/program after a method/function executes.
The post-condition rule states that the post-conditions guaranteed by subclass methods must not be weaker than the post-conditions guaranteed by the respective superclass method. Again, this rule is best clarified with some code.
static class Invader {
static final Missile dummyMissile = new Missile();
Missile checkCollision(Missile missile) {
if (missile == null) {
return dummyMissile;
} else {
return missile.intersects(this) ? missile : dummyMissile;
}
}
}
static final class DiveBomber extends Invader {
@Override
Missile checkCollision(Missile missile) {
if (missile.active()) {
return missile.intersects(this) ? missile : null;
}
return null;
}
}
In this example Invader.checkCollision() can never return null. However, it’s subclass’ method DiveBomber.checkCollision() has broken the post-condition rule by weakening that guarantee and allowing nulls to be returned.
So, the following code is all fine and dandy:
static final class Invaders {
private Missile[] missiles = {
new Missile().setActive(true)
};
private Invader[] invaders = {
new Invader(), new DiveBomber()
};
public Invader nextHit() {
for (Invader invader : invaders) {
for (Missile missile : missiles) {
Missile collided = invader.checkCollision(missile);
collided.explode();
}
}
return null;
}
}
However, the following change exposes the weaker post-condition and results in a NullPointerException:
private Missile[] missiles = {
new Missile().setActive(false)
};
Class Property rules
Note that these rules apply to the class-level, unlike earlier rules, which apply to the method-level.
Invariant Rule
An invariant refers to something that must never change and always hold true. In terms of code this could be some property belonging to a class that clients of the class are relying heavily on.
The Invariant Rule states that any invariants guaranteed by a superclass must also be guaranteed by its subclass.
Thinking about this, it makes complete sense. If someone has written client code that is relying on invariants of your class, should subclass instances start coming through, which don’t guarantee those invariants, the client code will break.
Consider the following code:
public class Invaders {
private final int maxInvaders;
protected Collection<Invader> invaders;
public Invaders(int maxInvaders) {
this.maxInvaders = maxInvaders;
}
public void add(Invader invader) {
if (invaders.size() <= this.maxInvaders) {
invaders.add(invader);
}
}
}
public final class SuperInvaders extends Invaders {
public SuperInvaders(int maxInvaders) {
super(maxInvaders);
}
@Override
public void add(Invader invader) {
invaders.add(invader);
}
}
Constructors of both classes require an argument which states the maximum number of Invaders that should be held. This will clearly be driven by the calling code (i.e. the client) and therefore it is reasonable to assume that the client will make various assumptions on that invariant holding true. In this particular example, the Invariant rule is broken by SuperInvaders.add(), where it simply ignores the maximum allowed and gladly pops another Invader directly into the invaders collection.
Constraint Rule
The Constraint Rule, AKA History Rule, states that constraints adhered to by a superclass must be adhered to by its subclasses.
At first glance, you may think that this rule is the same as the Invariant Rule. However, it is subtly different. In the Invariant Rule example earlier, the invariant is that there is a maximum number of Invaders held in the collection, which should never be exceeded. An example of a constraint could be that there are multiple instances of the Invaders class, each with their own maximum specified, where the constraint is that that maximum can never be changed by any of the Invaders subclasses.
Another example:
public class Invader {
protected int strength;
public Invader(int strength) {
this.strength = strength;
}
public int getStrength() {
return strength;
}
}
public final class DiveBomber extends Invader {
public DiveBomber(int strength) {
super(strength);
}
public void setStrength(int strength) {
this.strength = strength;
}
}
So, the Invader class takes strength as a constructor parameter and only allows clients access via its getter. So, there is a constraint here, that the strength of an Invader never changes. However DiveBomber breaks that constraint by providing a setter method. This is just plain bad code and could be easily addressed by making Invader.strength immutable like so:
public class Invader {
private final int strength;
public Invader(int strength) {
this.strength = strength;
}
public int getStrength() {
return strength;
}
}
public final class DiveBomber extends Invader {
public DiveBomber(int strength) {
super(strength);
}
}
Now that strength is both private and immutable, the compiler will not allow us to add a setter in DiveBomber and therefore prevents us from breaking the constraint rule.
We still need to be careful though, as the ‘hackery’ known as reflection could still be used to break the rule!