Smalltalk and Python

Alex Martelli aleaxit at yahoo.com
Thu Dec 14 13:25:51 CET 2000


"Ian Wild" <ian at cfmu.eurocontrol.be> wrote in message
news:3A389B1A.4F33FFD4 at cfmu.eurocontrol.be...
> Alex Martelli wrote:
> >
> > I can't find a source any more, but one of the most interesting
> > arguments I've heard for "only inherit from abstract classes"
> [...otherwise...]
> > it would break the idea that your concrete
> > classes partition the universe of discourse, if it were.
>
> Where does this partitioning requirement come from?

>From the fact that an object 'is an instance' of one,
and only one, class -- 'THE class' (singular) 'of the
object'.  If an object 'belongs' to n>1 classes, in
this framing, this is modeled by having the (inevitably
singular) class 'of the object' _inherit_ from the
said n>1 classes (i.e., 'belongs' is a weaker relationship
than 'being an instance of').

I am not aware of any OOA approaches in which is-instance-of
(as here defined) isn't an exhaustive and many-to-one
relationship between objects and (concrete) classes;
in other words, OOA approaches which let some objects
(in the universe-of-discourse that is being modeled)
be an instance of 'no class', or of several classes
at the same time.


> > This is based on the idea of subclassing as asserting IS-A.
>
> ?

IS-A implies several important things, among which is
the fact that the constraints on an object for subclass
membership are at least as strong as those for base
class membership (in Eiffel terms, class-invariants
are at least as strong for subclasses as for base
classes).


> It seems to me you'd need an "(e) None of the above"
> class very frequently if your world doesn't happen to have
> any genuine structural divisions.
>
> Dragging in that old favourite, "Squares and Rectangles",
> your partitioning clause means I'd have to invent a new class,
> "NonSquareRectangle", adding no interesting features, just so
> I can have aSquare behave like aRectangle.  This seems like
> unnecessary work, just to keep some C++ guru happy.

Forget C++ -- we're talking OO _analysis_.  Assume you're
trying to perform OOA on the universe of discourse "immutable
planar 2D geometric polygons", for the sake of definiteness.

If Rectangle and Square are both concrete (instantiable)
classes, with *overlapping* requirements for membership,
how do you classify a given rectangular object which
happens to have equal sides?  Toss a coin?

If you always classify such objects as Square, then you
have a constraint on actual membership to Rectangle _which
is not expressed in your OO model_ -- the fact that the
sides are not equal.  It's inconsistent to claim that
the constraint about equality of sides is 'no interesting
feature' -- if side-equality was of no interest, then
you would not be singling out squares!

You can't place the constraint (on sides being unequal)
on Rectangle itself, which is a baseclass of Square --
because Square would inherit the constraint, which is
inconsistent with _its_ intended constraint (equal sides).

So, you have a feature (constraint) which IS of interest
(you ARE interested in side-equality -- else, forget
squares!) but cannot be in the baseclass -- therefore,
in an OOA view, you must inherit and add the feature
(constraint) in the subclass.  So, we have an abstract
baseclass Rectangle (which knows and cares nothing
about its sides being equal or not) and concrete
subclasses Square (constraint: equal sides) and
NonsquareRectangle (constraint: unequal sides).

(If an object which happens to be a Rectangle AND have
equal sides ends up classified as EITHER a Square OR
a more generic Rectangle depending on accidental issues,
such as what path-of-construction was followed, you'll
get all of the usual darned issues to which CSG is so
prone to -- without even having to go 3D for them!-).


If you're modeling *mutable* objects, which is more
usually the case except in functional-programming
contexts, then the case is even sharper.  Square has
a constraint *on future mutation* -- its sides not
just 'are equal' at a given time, they're constrained
to *remain* equal as the object mutates (else, the
*class itself* of the object would have to switch --
and we've already clarified that we're treating classic
OOA, where objects belong to given classes "for the
duration", so changing class at runtime is not ok:-).

NonsquareRectangle has no such constraint, and will
allow each parallel pair of its sides to vary freely,
independently of the _other_ parallel pair of sides.

This is best modeled by giving the two concrete
classes in question different mutator-methods:
Square will have, say, SetSide, taking a single
positive number argument; NonsquareRectangle will
have SetSides, taking _two_ positive numbers as
arguments.  In this case, a NonsquareRectangle
whose sides happen to be set equal at a given time
will not protest -- it keeps its distinguishing
characteristic from Square, which is, there is
NO guarantee/constraint against *further* changes
preserving the current 'accidental' equality.

(I consider mutability/immutability need to be
addressed pretty early on in any OOA task -- they
are HIGHLY crucial to determine what we're trying
to accomplish, and how we're going to set about
it...!-).

The (clearly abstract!) common superclass Rectangle
may be factored to present methods common (with
different implementation) to both subclasses,
such as a GetSides accessor returning a Pair of
non-negative numbers, and also, no doubt, many
other nice things (accessors/mutators for center,
slant, perimeter, area, etc, etc).  Or, there may
be further abstract classes up the hierarchy, with
Rectangle inheriting from FixedNumberOfSides which
in turn subclasses Polygon (assuming we want to
model some kinds of polygons whose number of sides
is allowed to change during runtime, while others
have a fixed number of sides -- Rectangle will then
clearly fall among the latter) -- the various
methods will be exposed at appropriate levels in
the hierarchy.

Here's where MI peeks in, by the way -- we might
clearly like to have a RegularPolygon abstract
subclass of Polygon, right?  Then, it would be
most handy for Square to inherit, multiply, from
both RegularPolygon AND Rectangle.  Clearly, as
is quite reasonable in an analysis phase, this is
more about inheritance of interface, than of
implementation -- Square, EquilateralTriangle,
and so on, may not in fact use many (or any)
data held in either (or both) of their supers
(RegularPolygon for both, plus either Rectangle
or Triangle); but we do want to be able to pass
a reference to either Square or EquilateralTriangle
to any method requiring a reference to any
RegularPolygon -- and we also want to pass a
reference to Square to any method requiring a
reference to a Rectangle; the interfaces (which
include some possibly-abstracted versions of the
class-invariants) must thus be 'implemented' (as
commonly framed -- in OOA, in Eiffel, in C++ --
'inherited').

When you move to implementation issues, the
possible overheads of inheriting-from-concrete
can be heavy.  A Polygon (if concrete) needs
a generic sequence (array, list, whatever) of
Sides (or, equivalently, Vertices); a concrete
Hexagon needs to fix that sequence's multiplicity
to exactly six; a RegularHexagon would need only
1/6 of that -- either you fudge by not expressing
the 6-sides constraint on Hexagon (which feels rather
funny, doesn't it?-), or you waste 80% of your
store on duplicate data... if most of your polygons
are many-sided regular ones, that can kill you!-)

But that's implementation, and a well known
problem with inheriting-from-concrete in that
realm.  What I'm saying is that the problems with
inheritance-from-instantiable (fully concrete)
classes start much earlier than at implementation
time -- start with conceptual, ontological issues
regarding OOA and related classification tasks.


Alex






More information about the Python-list mailing list