WHAT DOES SUBSTITUTABILITY REALLY MEAN? PART 2
by Chris Date

 

 

 

(Continued from Part 1)

 

The Liskov Substitution Principle

 

In Part 1of this series I critiqued Tockey's article on LSP, which concludes with a reference to another article, The Liskov Substitution Principle, by Robert C. Martin (described by Martin himself as "the second of my Engineering Notebook columns for The C++ Report") -- so the next thing I did was take a look at that article, too.  Almost the first thing I found was the following "paraphrase of the Liskov Substitution Principle" (upper case and boldface as in the original):

 

"Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it."

 

Numerous objections spring to mind immediately!  First:  "Functions?"  Am I to understand that function is another word for method?  If so, why introduce the term?  If not, how do the concepts differ?  (Actually, it's quite clear that if we're to take the term function in its mathematical sense, then not all methods are functions, because mutators, at least, do not fit the mathematical definition.  See THE THIRD MANIFESTO for further discussion.)

 

Note: I'm informed by one reviewer (Dan Muller) that the things I'm objecting to in this paragraph and the next three are all features of C++ that "will be immediately understood by any C+ practitioner"; in fact, they all "have very precise meanings in the context of C++," and "the terms used [in Martin's sentence] are precisely the terms used in the standard that defines the language... Martin is using them quite correctly."  Very well; it follows that my criticisms should be taken, not as criticisms of Martin's article as such, but rather of the C++ language itself.  If you happen to be a C++ aficionado and find such criticisms offensive, then I apologize, and suggest you skip to the paragraph beginning "Fifth."

 

Second, why "pointers or references"?  Am I to understand that reference is just another word for pointer?  If so, why are there two terms?  If not, how do the concepts differ?

 

Third, does Martin really mean, as he states, "pointers or references to base classes"?  Shouldn't it really be "pointers or references to objects in base classes"? (I have another problem here, too.  As Hugh Darwen and I explain in THE THIRD MANIFESTO, pointers must be pointers to variables, not values, and so the "objects" in question here must be variables, not values.  But then I don't understand what it could possibly mean for a variable to be "in" a class.)  (See my earlier complaint regarding Tockey's talk of "substituting a subtype" when what he really meant was substituting an object of the subtype in question.  As I said previously, I don't think this complaint is just a quibble.) 

 

Fourth, shouldn't "use objects of derived classes" really be "use pointers or references to objects of derived classes"? Or is there no difference between (a) an object, on the one hand, and (b) a pointer or a reference to an object, on the other?  If so, why even mention "pointers or references"?

 

Fifth, all this talk of "pointers or references" makes me nervous, anyway.  In fact, it takes me straight back to the original debate over whether a circle is an ellipse.  In THE THIRD MANIFESTO, Hugh Darwen and I demonstrate clearly--and I think conclusively -- that if your inheritance model involves "pointers or references," then it's logically impossible to deal properly with the idea that a circle is an ellipse!  In fact, we strongly suspect that it's this fact (the fact, that is, that pointers and a good model of inheritance are fundamentally incompatible) that's the root of the problem.  Given that

 

a.      Most work on inheritance has been done in an object-oriented context, and

b.      Most if not all "object models" take pointers (in the shape of "object IDs") as a sine qua non,

 

it follows that

 

c. Most workers in this field are forced into the position that a circle isn't an ellipse (or, at least, isn't necessarily an ellipse).

 

But they don't seem to recognize that it's the pointers (i.e., the object IDs) that are the source of the difficulty (In fact, I might observe with a touch of malice that, since object IDs seem to be a necessary feature of objects, it's objects per se that are the source of the difficulty.  In other words, it's my opinion that objects per se and a good model of inheritance are logically incompatible.)  Instead, they give "justifications" for their apparently illogical position that typically look something like the following:

 

"Most object-oriented languages do not want objects to change class."

 

In other words, if we update an ellipse such that its semiaxes become equal, "most object-oriented languages" simply "don't want" the ellipse now to be regarded as a circle (I'm speaking pretty loosely here, as you'll probably realize, but you get the idea).

 

"It would be computationally infeasible to support a rule-based, intensional definition of class membership, because you would have to check the rules after each operation that affects an object." 

 

In other words, if we update an ellipse such that its semiaxes become equal, "it would be computationally infeasible" to do the work needed for the ellipse now to be regarded as a circle (again speaking pretty loosely).

 

Note:  Both of the foregoing quotes are taken from A Matter of Intent: How to Define Subclasses, by James Rumbaugh (Journal of Object-Oriented Programming, September 1996).  I quoted from this source in my original ellipses-and-circles article, too, where I also explained why we reject such "justifications."

 

Anyway, back to the Martin article.  A little further on, Martin discusses, not the ellipses-and-circles example as such, but a rectangles-and-squares example (which is isomorphic to the ellipses-and-circles example in all essential respects, of course):

 

"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 Square class as being derived from the Rectangle class ... However this kind of thinking can lead to some subtle, yet significant, problems ... Our first clue might be the fact that a Square does not need both itsHeight and itsWidth member variables.  Yet it will inherit them anyway.  Clearly this is wasteful ... Are there other problems?  Indeed!  Square will inherit the SetWidth and SetHeight functions.  These functions are utterly inappropriate for a Square, since the width and height of a square are identical."

 

I agree with the overall sense of this quote, up to but not including what comes after that "However."  But that business of inheriting "member variables" just points up the fact that a good "object model" shouldn't involve "member variables," anyway!  Objects--"encapsulated" objects, at any rate--should have behavior but not structure.  Then "methods" (behavior) would be inherited but "member variables" (structure) wouldn't, because there wouldn't be any "member variables" (structure) to inherit.  After all, it's obvious that squares can be represented more economically in storage than rectangles can, and this fact in itself is, precisely, a good argument for not exposing the "structure" of squares and rectangles in the first place.  As it is, Martin is using a bad feature of a particular object model as the basis for an argument that we shouldn't do what is clearly the logically correct thing to do.  Myself, I think this argument is completely backward; as I say, it's at least in part because (e.g.) squares are rectangles that we shouldn't expose member variables.

 

As for the question of Square inheriting the SetWidth and SetHeight functions, here we run smack into the confusion over values and variables yet again.  I don't want to get into a lot of detail on this point here, but will just observe once again that the whole picture becomes so much clearer if we frame our arguments and discussions in terms of values and variables instead of objects.  And I think our own inheritance model deals with this particular issue -- the issue, that is, of "inheriting the SetWidth and SetHeight functions" -- in a logically defensible and correct manner, too.

 

Well, I don't think I want to beat this particular dead horse very much longer.  Suffice it to say that Martin perseveres with the rectangles-and-squares example, adding epicycles to epicycles, and getting deeper and deeper into confusion, without ever seeming to recognize what the real problem is.  In fact, he says this (in a section entitled "What Went Wrong?"):

 

"So what happened?  Why did the apparently reasonable model of the Square and Rectangle go bad?  After all, isn't a Square a Rectangle?  Doesn't the ISA relationship hold?

 

No!  A square might be [sic!] a rectangle, but a Square object is definitely not a Rectangle object. Why?  Because the behavior of a Square object is not consistent with the behavior of a Rectangle object.  Behaviorally, a Square is not a Rectangle!  And it is behavior that software is really all about ... The LSP makes clear that in [object-oriented design] the ISA relationship pertains to behavior."

 

Well, it seems to me that "what went wrong" was that we got mired in object-orientation.  To say it one more time:  The inheritance model that Hugh Darwen and I propose (a) does not involve objects and (b) does understand that a square is a rectangle (and a circle is an ellipse).  And we would really like to suggest, with all due respect, that the industry leaders in this field, instead of spending so much time and effort in trying to persuade the rest of us that squares aren't rectangles and circles aren't ellipses, would take a look at our model and see how we do it!

 

Anyway, I more or less stopped reading Martin's paper at this point -- except for the following small point, which I noticed on the next page:

 

"[When] redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one."

 

Actually, Martin says this is a quote from Bertrand Meyer's book OBJECT ORIENTED SOFTWARE CONSTRUCTION (Prentice Hall, 1988); the text in brackets is thus Tockey's editing of Meyer's original, not editing by me.  Anyway, the overall quote looks like something Tockey said in his article too (perhaps Tockey got it from here); in effect, therefore, I've already dealt with it, at least to some extent, but as previously promised I'll come back and take a closer look at it later in this series.

 

A Behavioral Notion of Subtyping

 

It was with a certain sense of relief and anticipation that I turned to the original Liskov/Wing paper.  Surely here, I thought, I would find much greater precision and clarity of thinking and expression.  Nor was I disappointed.  Indeed, the paper was so clear that its errors were clear, too!  In fact, it was precisely because I found I wanted to make so many comments on that paper that I decided to split the results of my investigation into six parts.  In particular, it seemed best to defer my detailed commentary on the Liskov/Wing paper to Parts 3,4,5 and 6, and that's what I'm going to do.

 

(Continued in Part 3)

 

 

Posted 06/15/02