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 ClassDerived 1 Derived 2Had byReferenceHad By ValueUsedSidebar: Unified Notation 0.82 The Liskov Substitution PrincipleThe Liskov Substitution PrincipleFUNCTIONSTHATUSEPOINTERSORREFERENCESTOBASECLASSESMUSTBEABLETOUSEOBJECTSOFDERIVEDCLASSESWITHOUTKNOWINGIT.The above is a paraphrase of the Liskov Substitution Principle (LSP). Barbara Liskov firstwrote it as follows nearly 8 years ago1:What is wanted here is something like the following substitution property: Iffor each object o1of type S there is an object o2of type T such that for allprograms Pdefined in terms of T, the behavior of P is unchanged when o1issubstituted for o2then 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 thatfunction uses a pointer or reference to a base class, but must knowabout all the derivativesof that base class. Such a function violates the Open-Closed principle because it must bemodified whenever a new derivative of the base class is created.A Simple Example of a Violation of LSPOne of the most glaring violations of this principle is the use of C++ Run-Time TypeInformation (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 worksexactly 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 suchas grep. It is therefore preferred.]Clearly the DrawShapefunction is badly formed. It must know about every possiblederivative of the Shapeclass, and it must be changed whenever new derivatives ofShapeare created. Indeed, many view the structure of this function as anathema toObject Oriented Design.1. Barbara Liskov, “Data Abstraction and Hierarchy,” SIGPLAN Notices, 23,5 (May, 1988).3 : The Liskov Substitution PrincipleSquare and Rectangle, a More Subtle Violation.However, there are other, far more subtle, ways of violating the LSP. Consider anapplication 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 inmany sites. As is the case with all successful software, as itsusers’ needs change, new functions are needed. Imagine thatone day the users demand the ability to manipulate squares inaddition 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 fulfillthe ISA relationship with an old kind of object, then the class ofthe new object should be derived from the class of the oldobject.Clearly, a square is a rectangle for all normal intents andpurposes. Since the ISA relationship holds, it is logical tomodel 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 fundamentaltechniques of Object Oriented Analysis. A square is a rectangle, and so the Squareclassshould be derived from the Rectangleclass. However this kind of thinking can lead tosome subtle, yet significant, problems. Generally these problem are not foreseen until weactually try to code the application. Our first clue might be the fact that a Squaredoes not need both itsHeightanditsWidthmember 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. aCAD/CAE program in which every pin of every component of a complex circuit is drawnas a square), this waste could be extremely significant. However, let’s assume that we are not very concerned with memory efficiency. Arethere other problems? Indeed! Squarewill inherit the SetWidthand SetHeightfunctions. These functions are utterly inappropriate for a Square, since the width andheight of a square are identical.”. This should be a significant clue that there is a problemRectangleSquareFigure 1.4 The Liskov Substitution Principlewith 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, theinvariants of the Squareremain intact. The Squareobject will remain a mathematicallyproper 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 willbe corrupted because the height won’t be changed. This is a clear violation of LSP. The ffunction does not work for derivatives of its arguments. The reason for the failure is thatSetWidthand SetHeightwere not declared virtualin Rectangle.We can fix this easily. However, when the creation of a derived class causes us tomake changes to the base class, it often implies that the design is faulty. Indeed, it violatesthe Open-Closed principle. We might counter this with argument that forgetting to makeSetWidthand SetHeightvirtualwas the real design flaw, and we are just fixing itnow. However, this is hard to justify since setting the height and width of a rectangle areexceedingly primitive operations. By what reasoning would we make them virtualifwe did not anticipate the existence of Square.Still, let’s assume that we accept the argument, and fix the classes. We wind up withthe 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 Principleprivate: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 ProblemAt 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 mathematicalsquare. 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 pointeror reference to a Rectangle, and the Squarewill still act like a square and will remainconsistent. 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 believesto be a Rectangle. The function works just fine for a Rectangle, but declares anassertion error if passed a Square. So here is the real problem: Was the programmer whowrote that function justified in assuming that changing the width of a Rectangleleavesits height unchanged? Clearly, the programmer of gmade this very reasonable assumption. Passing aSquareto 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 Principlebut cannot operate properly upon Squareobjects. These functions expose a violation ofthe LSP. The addition of
Being translated, please wait..
