(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