Reputation: 51
I'm trying to understand the Liskov substitution principle, and I have the following code:
class Vehicle {
}
class VehicleWithDoors extends Vehicle {
public void openDoor () {
System.out.println("Doors opened.");
}
}
class Car extends VehicleWithDoors {
}
class Scooter extends Vehicle {
}
class Liskov {
public static void function(VehicleWithDoors vehicle) {
vehicle.openDoor();
}
public static void main(String[] args) {
Car car = new Car();
function(car);
Scooter scooter = new Scooter();
//function(scooter); --> compile error
}
}
I'm not sure if this violates it or not. The principle says that if you have an object of class S, then you can substitute it with another object of class T, where S is a subclass of T. However, what if I wrote
Vehicle vehicle = new Vehicle();
function(vehicle);
This of course gives compile error, because the Vehicle class doesn't have an openDoor() method. But this means I can't substitute VehicleWithDoors objects with their parent class, Vehicle, which seems to violate the principle. So does this code violate it or not? I need a good explanation because I can't seem to understand it.
Upvotes: 4
Views: 3551
Reputation: 43728
You got that backwards. The principle states that "if S
is a subtype of T
, then objects of type T
in a program may be replaced with objects of type S
without altering any of the desirable properties of that program".
Basically, VehicleWithDoors
should work where Vehicle
works. That obviously doesn't mean Vehicule
should work where VehiculeWithDoors
work. Yet in other words, you should be able to substitute a generalization by a specialization without affecting the program's correctness.
A sample violation would be an ImmutableList
extending a List
that defines an add
operation, where the immutable implementation throws an exception.
class List {
constructor() {
this._items = [];
}
add(item) {
this._items.push(item);
}
itemAt(index) {
return this._items[index];
}
}
class ImmutableList extends List {
constructor() {
super();
}
add(item) {
throw new Error("Can't add items to an immutable list.");
}
}
The Interface Segregation Principle (ISP) can be used to avoid the violation here, where you'd declare ReadableList
and WritableList
interfaces.
Another way to communicate that adding an item may not be supported could be to add a canAddItem(item): boolean
method. The design may not be as elegant, but it makes it clear not all implementation supports the operation.
I actually prefer this definition of the LSP: "LSP says that every subclass must obey the same contracts as the superclass". The "contract" may be defined not only in code (better when it does IMO), but also through documentation, etc.
Upvotes: 6
Reputation: 1854
When you extend a class or an interface, the new class is still of the type that it extended. The easiest way to reason about this (IMO) is thinking of a subclass as a specialised type of the superclass. So it's still an instance of the superclass, with some additional behaviors.
For example, your VehicleWithDoor
is still a Vehicle
, but it also have doors. A Scooter
is a vehicle too, but it doesn't have doors. If you have a method to open a vehicle's door, the vehicle must have doors (hence the compile time error when you pass a scooter to it). The same is for a method that takes an object of a certain class, you can pass an object that is instance of its subclass and the method will still work.
In terms of implementation, you can safely cast any object to one of its supertype (e.g. Car and Scooter
to Vehicle
, Car
to VehicleWithDoors
), but not the other way around (you can safely do so if you do some checks and cast it explicitly).
Upvotes: 1