Today I Learned: SOLID - Liskov Substitution Principle (LSP)
Today I Learned: SOLID - Liskov Substitution Principle (LSP)
So Continuing on my list of learning about the SOLID principles:
- S - Single-responsiblity Principle
- O - Open-Closed Principle
- L - Liskov Substitution Principle (you are here)
- I - Interface Segregation Principle
- D- Dependency Inversion Principle
Today I'm going to write about one that starts with an L, The third principle states: Derived classes must be substitutable for their base classes. or in other words:
"Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it"
Example time:
Suppose we have an interface Shape and things like a Rectangle that implement that interface, and we also have a function that takes Shape things
type Shape interface { GetWidth() int SetWidth(width int) GetHeight() int SetHeight(height int) }
type Rectangle struct { width, height int }
// vvv !! POINTER func (r *Rectangle) GetWidth() int { return r.width }
func (r *Rectangle) SetWidth(width int) { r.width = width }
func (r *Rectangle) GetHeight() int { return r.height }
func (r *Rectangle) SetHeight(height int) { r.height = height }
func DoStuff(shape Shape) {
width := shape.GetWidth()
shape.SetHeight(10)
expectedArea := 10 * width
actualArea := shape.GetWidth() * shape.GetHeight()
fmt.Print("Expected an area of ", expectedArea,
", but got ", actualArea, "
")
}
lets test our DoStuff function:
func main() {
rc := &Rectangle{2, 3}
DoStuff(rc) //Expected an area of 20, but got 20
}
Looks like it works as intended based on our implementations...
but things can go awry and here is where things break down, lets suppose some developer out there wants to implement another type of Shape a Square perhaps, it kinda "inherit" our Rectangle:
type Square struct { Rectangle }
func NewSquare(size int) *Square { sq := Square{} sq.width = size sq.height = size return &sq }
func (s *Square) SetWidth(width int) { s.width = width s.height = width }
func (s *Square) SetHeight(height int) { s.width = height s.height = height }
The dev proceeds to test its new Square struct, by passing it to our DoStuff function, lets take a look at what happens:
func main() { sq := NewSquare(5) DoStuff(sq) //Expected an area of 50, but got 100 ?! }

I'm pretty sure that's not what the Dev expected, this is a violation of the Liskov Substitution Principle:
We should not have generalizations like interfaces or inheritance and have implementers of those generalizations break some of the assumptions which are set up at the higher level.
So at the higher level we kind of assumed that if you have a Shape object and someone set its height we are just setting its height not both the height and the width, and here the implementer Square broke this assumption by setting both the width and the height which is a noble goal, I mean we can see how somebody would try to enforce the square invariant by setting both the width and the height, It's a noble goal but it doesn't work.
So the Liskov Substitution Principle is one of those situations where there is no right answer and there is no right solution to this problem. I mean we can take different approaches like for example we can say that squares don't exist that since every square is a rectangle we don't work with squares at all.
Or for example, we could do is we could explicitly make illegal states unrepresentable at all so to speak, So basically we can say that a square doesn't have a width and a height, a square has a size and that's pretty much it.
let's see how this might work, lets create a struct called Square2 which would have a size which is an int which would be both a width and height, and a method on that struct for creating Rectangles based on the Square2 width and height
type Square2 struct { size int }
func (s *Square2) Rectangle() Rectangle { return Rectangle{s.size, s.size} }
and use it:
func main() {
sq2 := Square2{5} rc2 := sq2.Rectangle() DoStuff(&rc2) //Expected an area of 50, but got 50 👍 } A temporary solution, a bit hacky, but it will work, for now 😅, Full Code can be found 👉Here👈
I hope anyone who is reading my short learning summaries finds them useful somehow...and as always: