I was listening to Hanselminutes a few weeks back and Scott Hanselman had Uncle Bob Martin on to talk about the SOLID principles of object-oriented design. SOLID stands for
- Single responsibility principle
- Open closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
Obviously, each one of those could warrant its own blog post and Robert Martin himself has written and spoken about them extensively. Uncle Bob did bring up a classic problem on Hanselminutes, though, that I wanted to take some time to talk about.
We who design object-oriented systems have a problem. We’ve been taught that the whole world is made of objects and that we are supposed to model our software after the real world. However, as I’ll cover in this post, sometimes that is a mistake.
The Liskov Substitution Principle says that “Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it” (Uncle Bob’s paraphrase). I have found this one somewhat confusing in the past, but I think that this Rectangle-Square problem explains the problem very well.
In math, the definition of a rectangle is “a parallelogram with four right angles”. One Webster’s definition of a square is “a rectangle with all four sides equal”. Right in the definition from the real world, a square is a rectangle. “Is a” is often a key phrase in object-oriented design used to denote an inheritance relationship.
So, let’s pretend we have the following code:
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public virtual int ComputeArea()
{
return Width * Height;
}
}
public class Square : Rectangle
{
private int _height;
public override int Width
{
get { return _height; }
set
{
_height = value;
}
}
public override int Height
{
get { return _height; }
set
{
_height = value;
}
}
public override int ComputeArea()
{
return base.ComputeArea();
}
}
Okay, that works. You can run code against it and at first blush it behaves like it should. However, the Liskov Substitution Principle says that you should be able to have
Rectangle r = new Square();
and have no problems.
While you can do that and can then operate on r as if it were a rectangle, there is a problem. A person who only knows about rectangles might do this to our r.
Rectangle r = someMethodThatWillReturnASquareSometimes();
r.Width = 10;
r.Height = 15;
They would then get entirely unpredictable results when they computed the area or even went back in to retrieve the properties (finding one had changed without their knowledge). A person who would want to operate in a safe way would have to eventually do the following:
Rectangle r = someMethodThatWillReturnASquareSometimes();
if (r is Square)
{
// Special Square Processing
}
else
{
// Normal Rectangle Stuff
}
That now pretty much defeats the purpose of using base classes and interfaces. If you have to know about derived classes and their implementation, you’ve lost the battle. I think that this is a great reminder to model objects logically how they affect the program, not how they reflect “real life”.