Last night I was reading Agile Software Development, Principles, Patterns, and Practices (freaking amazing book) and came across the Liskov Substitution Principle. I had never heard of this principle, so it got me intrigued.
What it is…
The idea is very simple. For any client using a base class, any child class should be able to be substituted in that client. It seems very straightforward, but the example Martin gives blew my mind.
Is a Square subclass of a Rectangle a valid model? Maybe. It depends on how clients use it. If a client expects to be able to change the height and width independently, then this model actually breaks down. Martin sums it up better than I,
This leads us to a very important conclusion. A model, viewed in isolation, can not be meaningfully validated. The validity of a model can only be expressed in terms of its clients.
He follows this up by saying that the Is-A relationship actually applies to the behavior of an object and not how we may envision an object relationship. A square Is-A rectangle because it has 4 sides and each side has a 90 degree angle between them. A square Is-Not-A rectangle because its height and width can change independently.
How many times have I created an object hierarchy based on how I perceive the objects vs how the behave? Too many. This principle is really easy to violate, and causes an application to become fragile when violated.
How to prevent it…
The book goes on to talk about Design by Contract, which can help prevent this behavior by specifying pre-conditions and post-conditions on any given method. These conditions are published with the base class. The oddity of Spec# makes a lot more sense when thought of in regards to the previous example.
Another way to accomplish this without built-in language support is to create a test fixture hierarchy similarly to the object hierarchy. That way, any expectations on the base class can be verified on all sub classes.
[TestFixture]
public abstract void RectangleBaseTests
{
protected abstract Rectangle { get; }
[Test]
public void ChangeWidth()
{
int oldHeight = Rectangle.GetHeight();
Rectangle.SetWidth(5);
Assert.That(Rectangle.GetHeight() == oldHeight);
Assert.That(Rectangle.GetWidth() == 5);
}
/* More Rectangle tests .... */
}
[TestFixture]
public void SquareTestFixture : RectangleBaseTests
{
private Square _square;
protected override Rectangle
{
get { return _square; }
}
[SetUp]
public void Setup()
{
_square = new Square(5,5);
}
/* more Square only tests... */
}
This still isn’t optimal because it requires developers to actually create the tests that verify functionality, but it is better than nothing.
In the end, the solution doesn’t matter as much as understand the problem. I highly recommend any developer read up on the Liskov Substitution Principle.