Meta

September Writing Challenge, Post 16: The L in SOLID

Note: This is the third of five posts I’m writing on the SOLID principles of object-oriented programming. Part 1: S, Part 2: O

The Liskov Substitution Principle is probably the deepest and most “academic” of the SOLID principles of object oriented design. It states that replacing an object with an instance of a subtype of that object should not alter the correctness of the program.

If you read the Wikipedia page for the Liskov Substitution Principle, you’ll see that there is a whole lot packed into that word “correctness”. It touches on programming by contract, const correctness, and a lot of terms that will have limited meaning to people who don’t have a degree in computer science or mathematics. It can also be difficult to see how some of the more academic-sounding constraints apply to the real-world systems that we write. I’m going to try to back into it with a couple of “ferinstances” that motivate a more practically applicable (if slightly less rigorous) formulation of the LSP.

The “classic” LSP example is the square/rectangle problem. It’s natural for us to think of a square as a “specialization” of a rectangle; if you say, “a square is just like a rectangle except that all its sides are of equal length”, most people won’t object.

When you try to bring this abstraction to an object design, however, things break down. Let’s lay out this object hierarchy in Swift – where I had to jump through a couple of hoops to get the square’s constraint to work as needed:

Square and Rectangle

Swift

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

classRectangle{

varheight:UInt=0

varwidth:UInt=0

}

classSquare: Rectangle{

private var_side:UInt=0

overridevarheight: UInt{

get{

return_side

}

set(newHeight){

_side=newHeight

}

}

overridevarwidth: UInt{

get{

return_side

}

set(newWidth){

_side=newWidth

}

}

}

funcdoStuffToRect(rect:Rectangle){

rect.height=5

rect.width=10

println("rectangle measures \(rect.height) by \(rect.width)")

}

doStuffToRect(Rectangle())

// output: "rectangle measures 5 by 10" 👍

doStuffToRect(Square())

// output: "rectangle measures 10 by 10" 😱

Calling code that expected a rectangle to have its height and width vary independently, or code that had expectations about any derived quantity (like area or the position of a vertex) is at risk for being broken now.

What’s the general principle we can draw from this? It might help to restate the square/rectangle relationship: “A square satisfies all of the constraints of a rectangle, and adds the constraint that its sides must be of equal length.” For the operation of setting width, the Rectangle allowed us to expect that its height would be invariant. The Square breaks that expectation – because of its extra constraint, its property setters mutate state that the parent class’s setters don’t touch. This is part of what it means in that Wikipedia article when it says that “Invariants of the supertype must be preserved in a subtype.”

There are other kinds of constraints that break expectations of calling code. You might be writing an object in a payroll system that has a method to compute compensation, and it might have a method signature like Currency computeCompensation(Employee emp, Timesheet latestTimesheet). That’s a very specific contract made with the calling code, and a subclass may not add a constraint by, for example, demanding that emp must be of the subclass OvertimeEligibleEmployee. Calling code has the reasonable expectation that it may pass in any Employee object or any instance of a subclass of Employee, and further constraining the type of emp breaks that expectation – so badly, in fact, that every OO language that I’ve worked in (which isn’t all of them, by any means, but it’s a fair sample of the common ones) disallows changes to overridden method signatures. You could get around it in the child class’s overridden method by downcasting to OvertimeEligibleEmployee. If you’ve ever been warned against downcasting, this is exactly why – you’re basically saying, “the caller says this is an instance of Employee, but I know better”, and sometimes you’ll be right, but at some point you’re going to be wrong about that and introduce a crash or a hard-to-trace logic error.

This, to me, is the core of the Liskov Substitution Principle: it’s all about constraints and expectations. If your child class introduces a constraint that would break any plausible expectation of the code calling an instance of the parent class, you’re breaking the LSP, and you may or may not be breaking your program.

The LSP is the most restrictive of the five SOLID principles and the easiest to break, either unintentionally, or because you decided that a downcast or an extra property mutation in a child class is okay just this one time. And the LSP gets broken all the time in production code and even well-regarded framework code, sometimes productively. For you Cocoa heads: You’ve seen mutable subtypes of immutable types – NSMutableArray is a subtype of NSArray, NSMutableString is a subtype of NSString… How does that stack up against the “history constraint” cited in the Wikipedia article? Bonus question (that might lead you to drink): How would you change this hierarchy of types to “fix” that?

I encourage you to do some reading on it, and to develop a feel for the innocuous-seeming changes in the lower reaches of your class hierarchies that might break expectations of code written against your parent classes – and likewise for the times when you can profitably but deliberately break the LSP to get things done.