12 August 2021

SOLID: The Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types.

Introduction

Robert C. Martin coined the mnomonic acronym SOLID to represent 5 principles of software design. This is the third in my own five-part series exploring my understanding of and experience applying these principles:

  1. [SRP] The Single-Responsibility Principle:
    • "Software entities should have only one reason to change."
  2. [OCP] The Open–Closed Principle:
    • "Software entities should be 'open for extension but closed for modification'."
  3. [LSP] The Liskov Substitution Principle: (you are here)
    • "Subtypes must be substitutable for their base types."
  4. [ISP] The Interface Segregation Principle:
    • "Clients should not be forced to depend on methods they do not use."
  5. [DIP] The Dependency Inversion Principle:
    • "High-level policy should depend upon abstractions, not concretions."

LSP

This principle is mostly concerned with the potential for voliating invariants when using inheritance to define a heirachy. The example below is in Go, which doesn't really have traditional inheritance, but does support a concept called embedding, which is simliar:

type Rectangle struct {
	TopLeft Point
	Length  int
	Width   int
}

type Square struct {
	Rectangle
}

In this example, the Square struct embeds the Rectangle. This is probably because the developer who wrote this code was used to thinking of a square being a special case of rectangle, which can often indicate a good candidate for sub-classing and inheritance. 'Under the hood', there's an actual field on the Square of type Rectangle, but you can access the Rectangle fields (and methods, if any are defined) as if they were defined on the Square.

func main() {
	s := Square{
		Rectangle: Rectangle{
			TopLeft: Point{0, 0},
			Length:  4,
			Width:   4,
		},
	}
	fmt.Println(s.Length, s.Width) // Output: 4 4
}

But the embedding of Rectangle to represent a Square is...not good:

func main() {
	s := Square{
		Rectangle: Rectangle{
			TopLeft: Point{0, 0},
			Length:  4,
			Width:   4,
		},
	}
	s.Length = 5
	fmt.Println(s.Length, s.Width) // Output: 5 4
}

Well, that's awkward, a word which here means a violation of LSP.


Next Section: The Interface Segragation Principle