Samy's blog

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:

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, "\n")
}

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 ?!
}

Implementation ruined

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:

A bit of tinkering a day keeps the boredom away. 🚀 ko-fi