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, "\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 ?!
}
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: