1. Inappropriate behavior when casting occurs
2. Overly broad generalizations about child class behavior
Again, Martin relies on shapes to demonstrate how inheritance can create problems, this time using a square and a rectangle as the demonstration points.
In his example, he describes a Rectangle as a parent class, and a Square as a child class of Rectangle. Below is C# code that emulates behavior Martin describes:
public class Rectangle
{
protected double _width;
protected double _height;
///
/// Just to keep track of the object
///
protected string _objectName;
public Rectangle()
{
_objectName = "Rectangle";
}
public virtual void SetWidth(double w)
{
_width = w;
}
public virtual void SetHeight(double h)
{
_height = h;
}
public double GetWidth()
{
return _width;
}
public double GetHeight()
{
return _height;
}
public void Output()
{
Console.WriteLine(_objectName + " attributes: Height=" + _height + " | Width=" + _width);
}
}
public class Square : Rectangle
{
public Square()
{
_objectName = "Square";
}
public override void SetWidth(double w)
{
_width = w;
_height = w;
}
public override void SetHeight(double h)
{
_height = h;
_width = h;
}
}
The above code looks clean, elegant, etc, but there's a problem. And this problem comes when, later on in the development process, someone builds code that takes the parent class as a parameter, expecting
class SomeImplementation
{
public void DoStuff()
{
Square square = new Square();
manipulateShape(square);
}
private void manipulateShape(Rectangle rectangle)
{
rectangle.SetHeight(2);
rectangle.SetWidth(4);
if (rectangle.GetWidth() * rectangle.GetHeight() == 8)
return;
throw new Exception("Error with rectangle dimensions");
}
}
In the above example, if a Rectangle object were passed to manipulateShape(), everything would be fine; however, in this case, when a Square object (which also IS-A rectangle) was passed to manipulateShape(), it throws an exception, because the Width * Height would equal 16, and not 8. The real problem here is that we over-rely on the parent's behavior, and as the body of code grows, managability is a major concern, because other users of code ought to rely on expected behavior, without having to always factor the interaction that occurs in the inheritance chain.
Ultimately, the developer who wrote the manipulateShape() method is expecting a Rectangle, but not one of its children (which is a reasonable thing to do); however, the real issue in this design is the overreliance on virtual/overridden methods: a square is a rectangle based on its public interface, but not on its behavior.
Consider a modification to the example, where a Rectangle is generated from some outside method:
Rectangle someUnknownShape = getShape();
manipulateShape(someUnknownShape);
If the getShape() method returns anything other than a top-level Rectangle, the exception gets thrown.
Because the behavior of Square is not consistent with Rectangle, the ultimate solution is to not have Square inherit from Rectangle. The safer alternative, in C#, as I see it, is to create an interface that both Square and Rectangle implement:
public interface IBoxShape
{
void SetWidth(double width);
void SetHeight(double height);
void GetWidth();
void GetHeight();
}
If both Square and Rectangle implemented the IBoxShape interface, then the above manipulateShape() method could keep its parameter as Rectangle, and the behavior of the application would perform as expected, and the developer(s) would not be tempted to send inappropriate objects to manipulateShape().
Going back to the Liskov Substitution Principle: Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
The manipulateShape() method in its original state ought to be able to expect that the area of the rectangle, after manipulating it, will be 8 - that behavior ought to be transparent, and changing behavior in child classes by use of virtual and overridden methods should be avoided.
No comments:
Post a Comment