SOLID and Liskov Substitution Principle
Overview
In this series of articles, I will cover all the SOLID patterns and their use in examples.
The SOLID principles were introduced by Robert C. Martin in his 2000 paper “Design Principles and Design Patterns.”
The SOLID principles consist of the following five concepts:
Single Responsibility Principle Open/Closed Principle Liskov Substitution Principle Interface Segregation Principle Dependency Inversion Principle
Liskov Substitution Principle
Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
Why
Firstly as you can see in previous principles srp and ocp inheritance can help us in general to solve many problems. It helps us to keep our code base small and I can focused on interfaces or classes. I can make classes and interfaces independent and less coupled.
However, I have to be careful when I am making connections between components. I can easily slip into an incorrect relationship between types and objects. Therefore, this principle is always about the relationship between all components.
Code Smells
For better understanding I can take a look on code smells which often indicates some violation of this principle. And can help to understand this principle.
It happens often when developer implements interface or extend class and do not fill all override methods with new code. Instead of code it will throw an exception. I would not be useful for client code, where is excepted value.
public abstract class Account {
protected abstract void deposit(BigDecimal amount);
protected abstract void withdraw(BigDecimal amount);
}
public class FixedTermDepositAccount extends Account {
@Override
protected void deposit(BigDecimal amount) {
// Deposit into this account
}
@Override
protected void withdraw(BigDecimal amount) {
throw new UnsupportedOperationException("Withdrawals are not supported by FixedTermDepositAccount!!");
}
}
In some scenarios developer would not implement nothing at all. A Subtype Provides No Implementation for a Behavior It Ca not Fulfill.
public interface FileSystem {
File[] listFiles(String path);
void deleteFile(String path) throws IOException;
}
public class ReadOnlyFileSystem implements FileSystem {
public File[] listFiles(String path) {
// code to list files
return new File[0];
}
// deleteFile operation is not supported on a read-only file system
public void deleteFile(String path) throws IOException {
// Do nothing.
}
}
Is some special occasions, clients knows about different types of object in hierarchy and check in their code for specific type of object (java word instanceof
).
This violation of liskov principle too.
for (Task t : tasks) {
if (t instanceof BugFix) {
Bugfix bf = (BugFix) t;
bf.initBugDescription();
}
t.setInProgress();
}
And worst case is when override method return the same value. This behavior is hard to find and can easy slide into huge bug. In generally hardcoding values should have very good reason be in that place and in changeable state should not be presented at all.
public abstract class Car {
// use fuel for running car
private int fuel;
}
public class ToyCar extends Car {
@Override
protected int getRemainingFuel() {
return 0;
}
}
Implementation Strategies or how to avoid code smells
Now we can move on implementation strategies.
In case of existing code there are three options:
Delete relation between classes
Most common and most recommend to apply is remove existing inheritance relation between components.
Interface segregation principle
There will be separated article about that. In short terms: create connection with new interface or class.
Tell, don’t ask
In short summary, this principle tells you that instead of asking object for data you should tell it what should do and then wait for the result of the operation.
In other case when you need to implement new functionality or new code, you can follow these rules:
class BugFix extends Task {
@Override
public void setInProgress() {
this.initBugDescription();
super.setInProgress();
}
}
# From previous example, regarding `BugFix` and `Task`
for (Task t : tasks) {
t.setInProgress();
}
Signature Rule
Method arguments types
This rule states that the overridden subtype method argument types can be identical or wider than the supertype method argument types.
public abstract class Foo {
public abstract Number generateNumber();
}
public class Bar extends Foo {
@Override
public Integer generateNumber() {
return new Integer(10);
}
}
Return types
(Covariance) The return type of the overridden subtype method can be narrower than the return type of the supertype method.
Exceptions
The subtype method can throw fewer or narrower exceptions than the supertype method.
Properties rule
Class invariants
A class invariant is an assertion concerning object properties that must be true for all valid states of the object.
History constraint
The history constraint states that the subclass methods (new or inherited) should not allow state changes that the base class did not allow.
public abstract class Car {
// Allowed to be set once at the time of creation.
// Value can only increment thereafter.
// Value cannot be reset.
protected int mileage;
public Car(int mileage) {
this.mileage = mileage;
}
}
public class ToyCar extends Car {
public void reset() {
mileage = 0;
}
}
This can be fixed by made mileage
immutable.