Results (
Indonesian) 1:
[Copy]Copied!
1The Liskov Substitution PrincipleThis is the second of my Engineering Notebookcolumns for The C++ Report. The articlesthat will appear in this column will focus on the use of C++ and OOD, and will addressissues of software engineering. I will strive for articlesthat are pragmatic anddirectly useful to the software engineer in thetrenches. In these articles Iwill make use of Booch’sand Rumbaugh’s new unifiednotation (Version 0.8)for documenting object oriented designs. The sidebarprovides a brief lexicon ofthis notation.IntroductionMy last column (Jan, 96) talked about the Open-Closed principle. This principle is thefoundation for building code that is maintainable and reusable. It states that well designedcode can be extended without modification; that in a well designed program new featuresare added by adding new code, rather than by changing old, already working, code. The primary mechanisms behind the Open-Closed principle are abstraction and polymorphism. In statically typed languages like C++, one of the key mechanisms that supports abstraction and polymorphism is inheritance. It is by using inheritance that we cancreate derived classes that conform to the abstract polymorphic interfaces defined by purevirtual functions in abstract base classes.What are the design rules that govern this particular use of inheritance? What are thecharacteristics of the best inheritance hierarchies? What are the traps that will cause us tocreate hierarchies that do not conform to the Open-Closed principle? These are the questions that this article will address.
Base Class
Derived 1 Derived 2
Had by
Reference
Had By Value
Used
Sidebar: Unified Notation 0.8
2 The Liskov Substitution Principle
The Liskov Substitution Principle
FUNCTIONSTHATUSEPOINTERSORREFERENCESTOBASE
CLASSESMUSTBEABLETOUSEOBJECTSOFDERIVEDCLASSES
WITHOUTKNOWINGIT.
The above is a paraphrase of the Liskov Substitution Principle (LSP). Barbara Liskov first
wrote it as follows nearly 8 years ago
1
:
What is wanted here is something like the following substitution property: If
for each object o
1of type S there is an object o2
of type T such that for all
programs Pdefined in terms of T, the behavior of P is unchanged when o1is
substituted for o2
then S is a subtype of T.
The importance of this principle becomes obvious when you consider the consequences of violating it. If there is a function which does not conform to the LSP, then that
function uses a pointer or reference to a base class, but must knowabout all the derivatives
of that base class. Such a function violates the Open-Closed principle because it must be
modified whenever a new derivative of the base class is created.
A Simple Example of a Violation of LSP
One of the most glaring violations of this principle is the use of C++ Run-Time Type
Information (RTTI) to select a function based upon the type of an object. i.e.:
void DrawShape(const Shape& s)
{
if (typeid(s) == typeid(Square))
DrawSquare(static_cast(s));
else if (typeid(s) == typeid(Circle))
DrawCircle(static_cast(s));
}
[Note: static_castis one of the new cast operators. In this example it works
exactly like a regular cast. i.e. DrawSquare((Square&)s);. However the new syntax has more stringent rules that make is safer to use, and is easier to locate with tools such
as grep. It is therefore preferred.]
Clearly the DrawShapefunction is badly formed. It must know about every possible
derivative of the Shapeclass, and it must be changed whenever new derivatives of
Shapeare created. Indeed, many view the structure of this function as anathema to
Object Oriented Design.
1. Barbara Liskov, “Data Abstraction and Hierarchy,” SIGPLAN Notices, 23,5 (May, 1988).
3 : The Liskov Substitution Principle
Square and Rectangle, a More Subtle Violation.
However, there are other, far more subtle, ways of violating the LSP. Consider an
application which uses the Rectangleclass as described below:
class Rectangle
{
public:
void SetWidth(double w) {itsWidth=w;}
void SetHeight(double h) {itsHeight=w;}
double GetHeight() const {return itsHeight;}
double GetWidth() const {return itsWidth;}
private:
double itsWidth;
double itsHeight;
};
Imagine that this application works well, and is installed in
many sites. As is the case with all successful software, as its
users’ needs change, new functions are needed. Imagine that
one day the users demand the ability to manipulate squares in
addition to rectangles.
It is often said that, in C++, inheritance is the ISA relationship. In other words, if a new kind of object can be said to fulfill
the ISA relationship with an old kind of object, then the class of
the new object should be derived from the class of the old
object.
Clearly, a square is a rectangle for all normal intents and
purposes. Since the ISA relationship holds, it is logical to
model the Squareclass as being derived from Rectangle.
(See Figure 1.)
This use of the ISA relationship is considered by many to be one of the fundamental
techniques of Object Oriented Analysis. A square is a rectangle, and so the Squareclass
should be derived from the Rectangleclass. However this kind of thinking can lead to
some subtle, yet significant, problems. Generally these problem are not foreseen until we
actually try to code the application.
Our first clue might be the fact that a Squaredoes not need both itsHeightand
itsWidthmember variables. Yet it will inherit them anyway. Clearly this is wasteful.
Moreover, if we are going to create hundreds of thousands of Squareobjects (e.g. a
CAD/CAE program in which every pin of every component of a complex circuit is drawn
as a square), this waste could be extremely significant.
However, let’s assume that we are not very concerned with memory efficiency. Are
there other problems? Indeed! Squarewill inherit the SetWidthand SetHeight
functions. These functions are utterly inappropriate for a Square, since the width and
height of a square are identical.”. This should be a significant clue that there is a problem
Rectangle
Square
Figure 1.
4 The Liskov Substitution Principle
with the design. However, there is a way to sidestep the problem. We could override SetWidthand SetHeightas follows:
void Square::SetWidth(double w)
{
Rectangle::SetWidth(w);
Rectangle::SetHeight(w);
}
void Square::SetHeight(double h)
{
Rectangle::SetHeight(h);
Rectangle::SetWidth(h);
}
Now, when someone sets the width of a Squareobject, its height will change correspondingly. And when someone sets its height, the width will change with it. Thus, the
invariants of the Squareremain intact. The Squareobject will remain a mathematically
proper square.
Square s;
s.SetWidth(1); // Fortunately sets the height to 1 too.
s,SetHeight(2); // sets width and heigt to 2, good thing.
But consider the following function:
void f(Rectangle& r)
{
r.SetWidth(32); // calls Rectangle::SetWidth
}
If we pass a reference to a Squareobject into this function, the Squareobject will
be corrupted because the height won’t be changed. This is a clear violation of LSP. The f
function does not work for derivatives of its arguments. The reason for the failure is that
SetWidthand SetHeightwere not declared virtualin Rectangle.
We can fix this easily. However, when the creation of a derived class causes us to
make changes to the base class, it often implies that the design is faulty. Indeed, it violates
the Open-Closed principle. We might counter this with argument that forgetting to make
SetWidthand SetHeightvirtualwas the real design flaw, and we are just fixing it
now. However, this is hard to justify since setting the height and width of a rectangle are
exceedingly primitive operations. By what reasoning would we make them virtualif
we did not anticipate the existence of Square.
Still, let’s assume that we accept the argument, and fix the classes. We wind up with
the following code:
class Rectangle
{
public:
virtual void SetWidth(double w) {itsWidth=w;}
virtual void SetHeight(double h) {itsHeight=h;}
double GetHeight() const {return itsHeight;}
double GetWidth() const {return itsWidth;}
5 : The Liskov Substitution Principle
private:
double itsHeight;
double itsWidth;
};
class Square : public Rectangle
{
public:
virtual void SetWidth(double w);
virtual void SetHeight(double h);
};
void Square::SetWidth(double w)
{
Rectangle::SetWidth(w);
Rectangle::SetHeight(w);
}
void Square::SetHeight(double h)
{
Rectangle::SetHeight(h);
Rectangle::SetWidth(h);
}
The Real Problem
At this point in time we have two classes, Squareand Rectangle, that appear to work.
No matter what you do to a Squareobject, it will remain consistent with a mathematical
square. And regardless of what you do to a Rectangleobject, it will remain a mathematical rectangle. Moreover, you can pass a Squareinto a function that accepts a pointer
or reference to a Rectangle, and the Squarewill still act like a square and will remain
consistent.
Thus, we might conclude that the model is now self consistent, and correct. However,
this conclusion would be amiss. A model that is self consistent is not necessarily consistent with all its users! Consider function gbelow.
void g(Rectangle& r)
{
r.SetWidth(5);
r.SetHeight(4);
assert(r.GetWidth() * r.GetHeight()) == 20);
}
This function invokes the SetWidthand SetHeightmembers of what it believes
to be a Rectangle. The function works just fine for a Rectangle, but declares an
assertion error if passed a Square. So here is the real problem: Was the programmer who
wrote that function justified in assuming that changing the width of a Rectangleleaves
its height unchanged?
Clearly, the programmer of gmade this very reasonable assumption. Passing a
Squareto functions whose programmers made this assumption will result in problems.
Therefore, there exist functions that take pointers or references to Rectangleobjects,
6 The Liskov Substitution Principle
but cannot operate properly upon Squareobjects. These functions expose a violation of
the LSP. The addition of
Being translated, please wait..
