I _Really_ Don't Know

A low-frequency blog by Rob Styles

TDD, Liskov Substitution Principle and Open/Closed Principle

In my current role I've been working on a number of framework style components that allow developers to focus on the specifics of the task in hand and, hopefully, ignore the generic and common plumbing and orchestration. One of the frameworks is a reporting framework, the other an exception handling framework. One of the things we've been trying to avoid is inheritance where other methods would be better, but type compatibility and inheritance of some functionality appears to be the best model for some of what we're doing at least. Which raised a big debate about Fragile Base Class problems*. Of course, one of the guys piped up with the Open/Closed Principle, but was trumped by a reference to Liskov's Substitution Principle and so the talking shop went on...

Rather than endlessly debate the issues I started looking for a pragmatic solution and remembered that a while ago, a colleague of mine the great Ben Gutteridge, pointed out to me that NUnit fixtures can inherit from another fixture and that the base fixture tests get run for each derived fixture. He'd used this to enforce certain behaviour in a hierarchical control (asp.net server controls) framework he'd written.

It occurred to me that TDD could really help us here. Take the classic Liskov problem of a square and a rectangle...

A Rectangle defined as:


public class Rectangle
{
public virtual int Width
{
get
{
return _width;
}
set
{
_width = value;
}
}
public virtual int Height
{
get
{
return _height;
}
set
{
_height = value;
}
}
private int _width;
private int _height;
}

And clearly a Square is a Rectangle, so we write the obvious implementation of Square:


public class Square : Rectangle
{
public int Size
{
get
{
return _size;
}
set
{
_size = value;
}
}

public override int Height
{
get
{
return _size;
}
set
{
_size = value;
}
}

public override int Width
{
get
{
return _size;
}
set
{
_size = value;
}
}
private int _size;
}

All appears to work well, until we consider the use of Rectangle by our World:


public class World
{
public void Scale(Rectangle rectangle)
{
rectangle.Width = rectangle.Width * _scalingFactor;
rectangle.Height = rectangle.Height * _scalingFactor;
}
private int _scalingFactor;
}

Substituing a Square into this will result in the square being scaled twice, so a Square may be type substitutable with Rectangle, but as Liskov says, it is not semantically the same as it does not support independant scaling of the axes.

Now, if we look at the test fixture for our Rectangle we get some idea of the contract for a Rectangle:


[TestFixture]
public class RectangleFixture
{
[Test]
public void SetHeightAndWidth()
{
Rectangle rectangle = new Rectangle();

int expectedWidth = 3;
int expectedHeight = 7;

rectangle.Width = expectedWidth;
rectangle.Height = expectedHeight;

Assertion.AssertEquals(expectedWidth, rectangle.Width);
Assertion.AssertEquals(expectedHeight, rectangle.Height);
}
}

Seems simple enough, the test fixture for our Square also looks simple enough:


[TestFixture]
public class SquareFixture
{
[Test]
public void SetSize()
{
Square square = new Square();

int expectedSize = 9;

square.Size = expectedSize;
Assertion.AssertEquals(expectedSize, square.Size);
}
}

And, of course, the tests pass. Now the problem arises because the tests that get run against Square don't enforce the contract of Rectangle even though Square claims, thorugh inheritance, to be type compatible. By adding test inheritance we get a very different result:


public class SquareFixture : RectangleFixture

Now the SquareFixture as seen by NUnit also includes all the tests of RectangleFixture, but to allow the tests to run against the derived classes we have to extract and delegate the object construction:


[TestFixture]
public class RectangleFixture
{
[Test]
public void SetHeightAndWidth()
{
Rectangle rectangle = GetShape();
...
}

protected virtual Rectangle GetShape()
{
return new Rectangle();
}
}

[TestFixture]
public class SquareFixture : RectangleFixture
{
[Test]
public void SetSize()
{
Square square = GetShape();
...
}

protected override Rectangle GetShape()
{
return new Square();
}
}

Now the Rectangle tests get run against the Square and we can see quite clearly that it doesn't live up to the Rectangles reputation.

This technique for inheriting tests allows us to see clearly where we have violations of Liskov's Principle and also helps enforce the Open/Closed Principle.