I'm trying to figure out the Liskov's Substitution Principle and the example of the rectangle and square doesn't really click with me. So the example for the rectangle and square goes that if you have a base rectangle class and a square class that inherits from it, the square class' setWidth/setHeight method are implemented differently such that when it calls setWidth it will also change it's height to be the same as width.
But I don't understand why that is a problem. Surely you would want the square to have the same width/height right? Anyways I want to know if the same applies to this vehicle/car/plane structure I have below.
TLDR: I have a car and a plane that inherit the abstract move() method from Vehicle class. Car increases it's x and y location and plane increases x,y, and z location. Does this break liskov's substitution principle if I have a function that takes in a Vehicle class and calls move on it? If so, why is that bad design?
I have an abstract Vehicle class
abstract class Vehicle {
private wheels;
private make;
private seats;
abstract honk(){};
abstract move(){};
}
and then I have 2 subclasses of Plane and Car
Car
class Car extends Vehicle {
private location: Location2d;
public honk() {
console.log(this.getMake() " IS HONKING");
}
public move(){
this.location.x ;
this.location.y
}
}
export default Car;
Plane
class Plane extends Vehicle implements FlyingVehicle {
private maxAltitude;
private location: Location3d;
public honk() {
console.log(this.getMake() " is HONKING")
}
public move(){
this.location.x ;
this.location.y ;
this.location.z ;
}
}
Now If I have a function that goes through an array of vehicles and then calls move on all of them. Is this breaking Liskov's substitution principle?
CodePudding user response:
I never thought Square / Triangle / Car / Bicycle were ever good examples for OOP. Never really seen these in real code, which makes arguing about this stuff a bit harder because you need to do this in a way that maintains the metaphor.
Planes and Cars rarely get controlled by some code that uses both ;)
The substitution principle is mainly about types though. So any subclass' method should be callable by something that can call the main class, and the type returned should be compatible with what was returned from the parent.
It doesn't really care about the side-effects. You could implement a new subclass that doesn't move the vehicle at all, and instead does something completely unrelated. As long as the types make sense, it's valid.
Now I'd argue that a move
method that's implemented by either should probably do something that's in the spirit of the original method, but that's more about general reasonable design vs specifically liskov.
CodePudding user response:
Now If I have a function that goes through an array of vehicles and then calls move on all of them. Is this breaking Liskov's substitution principle?
No, this would not be breaking the Liskov substitution principle. (That doesn't mean it's necessarily good code, of course.)
The key insight I think you're missing is that the LSP is about the interface of a method - its parameters and its return values. Subclasses must accept any sets of parameters that the parent class accepts, and may accept values that the parent rejects. Subclasses must return values that the parent class could return, but may not return all possible values that the parent class might have returned.
The internals of the method can change, and it may have different side effects.
Here's an example that hopefully explains why this matters.
There's a software system for HR. It has an employee class, and one method on this employee class is
calculatePaycheque(month: int)
, which returns the amount that employee should be paid that month, of typeint
. It throws an error if themonth
is <1 or >12. The paycheque is calculated by dividing the salary by 12. This method is called in a few different places, both to predict total payroll outgoings for a given month and to actually award the pay at the end of each month.Some employees are seasonal workers, so they don't work or get paid in the winter. However, they get a bonus based on performance for the months they do work. To facilitate this, a new class 'seasonalEmployee' is created. It overwrites the
calculatePaycheque()
method, so that in March through September they get 1/12 of the yearly salary, and also a bonus is added.If we don't adhere to the LSP, then we could instead return an object
{ fixedAmount: int, bonus: int }
. Since the workers only work 7 months, we can take amonth: int
but throw an error if it's <3 or >9.Let's say we do this, and then we add a
seasonalEmployee
to the big list ofemployee
objects that are processed for predicting the payroll for September. The code that adds up the paycheques for each employee will not be able to add our{fixedAmount: int, bonus: int}
to a list ofint
values without us writing some additional code that checks the type of the response, and does some maths on it. When we do the payroll for October, theseasonalEmployee
will throw an error because the employee is not paid that month. So now we need a guard that prevents us callingcalculatePaycheque()
on seasonal employees outside of their season.We can spare ourselves all this headache if we follow the Liskov Substitution Principle. In this case, we still take an argument
month: int
that can be in the range 1-12, and we still return a single amount of typeint
. This way, we don't need to update the rest of the codebase when we start addingseasonalEmployee
objects to the system.
The internals of how the method works have changed. However, the method itself is still interchangeable with the parent method. This means that we can work on only the new code, without needing to refactor or expand the rest of the codebase to play nice with the changes.
CodePudding user response:
The Square
/Rectangle
example with setWidth
/setHeight
is supposed to illustrate a problem with the concept of inheritance. It's assumed that
- The base class
Rectangle
offerssetWidth
andsetHeight
methods which are supposed to only change those respective attributes, and - The subclass
Square
has an invariant that the width always equals the height.
Given those two requirements, you must either violate the Liskov substitution principle (by violating the contract of the setWidth
and setHeight
methods) or otherwise you must violate the subclass's invariant (by allowing the width to be different from the height).
However, if the Rectangle
class's contract doesn't state that setWidth
must only change the width (and likewise that setHeight
must only change the height), then the Square
class can override these methods without violating the base class's contract, and everything is fine. This is more like your Car
and Plane
example; the Vehicle
base class probably does not have a contract that states the move
method must not change the z
position, so Plane.move
doesn't violate such a contract. In that case there is no violation of the Liskov substitution principle.