Liskov Substitution Principle
That is, if
S
is a subtype of T
, then objects of type T
in a program P
may be replaced with objects of type S
without altering any of the desirable properties(correctness) of that program P
.Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program. - general form
It means that we must ensure that new derived classes extend the base classes without changing ancestor's behavior.
Child classes should never break the parent class’ type definitions - simplified interpretation
Otherwords, Subclass should override the parent class’ methods in a way that does not break functionality from a client’s point of view.

Barbara Liskov Substitution Principle benefits:
- the way to characterize good inheritance hierarchies;
- decreases a possibility of creation of hierarchies that do not conform Open-Closed Principle (OCP).
If doSomething(SomeClass duck)
method works with instances of SomeClass
, it does so with instances of any subclass of SomeClass
.
Classic LSP example (Rectangle & Square)
- Is
Square
aRectangle
? - Yes.
So, since square
is a rectangle
(mathematically) we can implement square
as a descendant of rectangle
:
Implementation with violation of LSP:
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
Formally we could pass Square
wherever Rectangle
is expected,
but by doing so we break everyone's assumptions about the behavior of Rectangle
:
@Test
public void testRectangleArea() throws Exception {
Rectangle rectangle = new Square();
rectangle.setWidth(20);
rectangle.setHeight(4);
assertEquals(80, rectangle.area());
}
In the following use-case violation is even more implicit:
public void clientMethod(Rectangle rectangle) {
rectangle.setWidth(20);
rectangle.setHeight(4);
assert(rectangle.area() == 80); // gonna fail with square
}
As U see, Square
is behaviorally not a correct substitution for Rectangle
, which means this hierarchy violates Liskov Substitution Principle (LSP).
Changing the height/width of a square
behaves differently from changing the height/width of a rectangle
.
Actually, there is no reason to have two different methods: setWidth
& setHeight
for the square at all.
Validity is not Intrinsic
Inspecting the Square/Rectangle hierarchy in isolation did not show any problems. In fact it even seemed like a self-consistent design. We had to inspect clients to identify the problems.
- Inspecting a model hierarchies in isolation can not provide meaningful validation.
- Therefore, the validity of a model must be judged against the possible users of the model.
We need to anticipate the assumptions that clients will make about our classes.
LSP Compliant Solution - Siblings
We make Rectangle
& Square
siblings via introducing the interface Shape
for common methods:
public interface Shape {
long area();
}
- Clients of
Shape
cannot make any assumptions about setters behavior. - To change properties of the shapes, clients have to work with the concrete implementations.
public class Square implements Shape {
private int size;
public Square(int size) {
this.size = size;
}
@Override
public long area() {
return size * size;
}
public void setSize(int size) {
this.size = size;
}
}
Square
doesn't contain different methods for setting width & height.
public class Rectangle implements Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public long area() {
return width * height;
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
}
Rectangle
has consistent behavior regardless of its properties mutation.
@Test
public void testRectangleArea() throws Exception {
Shape rectangle = new Rectangle(10, 5);
assertEquals(50, rectangle.area());
rectangle.setHeight(3);
assertEquals(30, rectangle.area());
}
@Test
public void testSquareArea() throws Exception {
Shape square = new Square(5);
assertEquals(25, square.area());
}
LSP Compliant Solution - Proper Abstraction
This time, we will just enhance the initial implementation through the introducing the proper abstraction in order to eliminate any wrong behavioral assumptions.
As width = height
is a rule of a Square
not Rectangle
, we can't just extend one from another without the LSP violation.
Wait, what about Quadrangle
? Does it restricts the area calculation logic either to one or another?
public class Rectangle extends Quadrangle {
public Rectangle(int width, int height) {
super(width, height);
}
}
So, our Rectangle
is exact Quadrangle
, but with meaningful behavioral rules.
public class Square extends Quadrangle {
public Square(int size) {
super(size, size);
}
@Override
public void setWidth(int width) {
setSize(width);
}
@Override
public void setHeight(int height) {
setSize(height);
}
private void setSize(int size) {
super.setWidth(size);
super.setHeight(size);
}
Our Square
is also descendant of Quadrangle
with it's own width = height
rule.
Rectangle
&Square
are siblings with common ancestorQuadrangle
.- When client receives
Quadrangle
, he can't form strictarea
behavior assumptions, cause to do so he needs more info about this abstraction.
@Test
public void testRectangleArea() throws Exception {
Quadrangle quadrangle = new Rectangle(10, 5);
assertEquals(50, quadrangle.area());
quadrangle.setWidth(20);
quadrangle.setHeight(4);
assertEquals(80, quadrangle.area());
}
@Test
public void testSquareArea() throws Exception {
Quadrangle quadrangle = new Square(5);
assertEquals(25, quadrangle.area());
// if someone'll mutate our quadrangle
// it won't violate our assumptions about it's area
// cause client understands that it might be either square or rectangle.
quadrangle.setWidth(20);
quadrangle.setHeight(4);
assertEquals(16, quadrangle.area());
}
Anyway i like the 1st solution more than this one, cause it left no doubts & narrowed the square behavior to the only needed methods.
LSP Compliant Solution - Immutability
Here we make the initial implementation immutable (eliminate all setters), leaving NO way for properties mutation or forming the wrong assumptions regarding the objects behavior.
public class Rectangle {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public long area() { return width * height; }
public int getWidth() { return width; }
public int getHeight() { return height; }
}
All properly constructed Square
s will be valid Rectangle
s.
When we creating an immutable Square
we enforce our invariants without an influence on the base class behavior.
Now, Wherever the client method will use rectangle, it would expect only its area value without making any assumptions regardless its behavior related to its properties mutation.
public class Square extends Rectangle {
private int size;
public Square(int size) {
super(size, size);
}
public int getSize() { return size; }
}
Now the width & height literally are not state anymore, those are rather the properties of the shape identity. Once we change width & height, we would be talking about another shape, than before.
@Test
public void testRectangleArea() throws Exception {
Rectangle rectangle = new Rectangle(10, 5);
assertEquals(50, rectangle.area());
}
@Test
public void testSquareArea() throws Exception {
Rectangle square = new Square(5);
assertEquals(25, square.area());
}
The fact, that immutability solved our problem, doesn't mean we could not thing about the proper classes hierarchy in our application:
ImmutableCar
can't beImmutableBicycle
, cause despite that it's also kind of vehicle, its usage and expected behavior is different.
LSP and OCP relationships
In most cases, violation of LSP as a consequence violates OCP as well.
Cause if we have a clientFunction f
parameterized over type T
.
If object S
which is subtype of T
violates LSP, when it's passed to f
among the objects of type T
might cause f
to misbehave.
In this scenario f
is fragile in the presence of S
and not closed against derivations of T
anymore.
LSP violation code smells
- Subclass overrides a method of the superclass by an empty method.
- Subclass, which expects, that certain methods inherited from the superclass should not be called by clients.
- Subclass that throws additional (unchecked) exceptions.
- ...
Extra Examples of LSP Violations
-
Tesla
can't be a descendant ofFerrari
or itsFuelCar
abstraction, cause to start the engine it should be charged first. Clients of theFuelCar
do not expect this. -
java.util.Properties
(From Java Doc) :Because
Properties
inherits fromHashtable
, the put andputAll
methods can be applied to aProperties
object. Their use is strongly discouraged as they allow the caller to insert entries whose keys or values are notStrings
. ThesetProperty
method should be used instead. If the store or save method is called on a "compromised"Properties
object that contains a non-String key or value, the call will fail.
Final Thoughts
Programmers do not define entities that are something, but entities that behave somehow.
The LSP is about the building hierarchies with predictable Behavior.
It's not enough for instances of sub-classes to provide all declared method in their parent class. These methods should also behave appropriately. If you give to your client an object, which substitutes its ancestor with violation of LSP, you might brake your client assumptions regarding the behavior of the object used by him. A client method should not be able to distinguish whether it works with some class or its descendant by behavior.
We also ensured, that immutability is simple and extremely powerful concept, which respects many of software-design principles.
P.S. I've touched SOLID two years ago, so this is more detailed article about LSP ^_^
No comments:
Post a Comment