[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: OO should break when broken
I think the "right" way to implement circles and ellipses depends
entirely upon the intent and purpose of your program. For example, do
you intend to be able to change the dimensions of the objects? Do you
intend to change the class between circle and ellipse? Do you wish to
save on storage space for circles? Are you modeling ellipsoids for a
geometry program, or graphical objects in a drawing program, or perhaps
data in a particular file format? Are these classes interface classes
or implementation classes?
My point is that just using words like "circle" and "ellipse" doesn't
make it entirely clear what it is you're attempting to do with them.
Those names just give you a general idea.
If you just wanted to model geometric ellipsoids, then I'd suggest a
single class with a predicate for testing for equal radii.
If you wanted immutable objects and you wanted a separate circle class
primarily to save storage space by only having one radius, and possibly
to speed up the predicate for testing for circles, then I'd suggest
something like the following, in Dylan (untested):
// abstract class with interface slots (no storage)
define abstract class <ellipsoid> (<object>)
virtual constant slot rx, required-init-keyword: rx:;
virtual constant slot ry, required-init-keyword: ry:;
end class;
// concrete subclasses with implementation slots
define class <circle> (<ellipsoid>)
constant slot %r, required-init-keyword: r:;
end class;
define class <ellipse> (<ellipsoid>)
constant slot %rx, required-init-keyword: rx:;
constant slot %ry, required-init-keyword: rx:;
end class;
// make the abstract class instantiable; create an instance of
// one of the concrete subclasses depending on the init values
define method make (class == <ellipsoid>, #key rx, ry)
if (rx = ry)
make( <circle>, r: rx )
else
make( <ellipse>, rx: rx, ry: ry )
end
end method;
// implement the "interface" slots for the concrete subclasses
define method rx (circle :: <circle>) circle.%r end;
define method ry (circle :: <circle>) circle.%r end;
define method rx (ellipse :: <ellipse>) ellipse.%rx end;
define method ry (ellipse :: <ellipse>) ellipse.%ry end;
// define the "ellipsoid protocol":
// - predicate for testing whether an ellipsoid is a circle
// - constructors for creating ellipsoids and circles
define method circle? (circle :: <ellipsoid>) #f end;
define method circle? (circle :: <circle>) #t end;
define function ellipsoid (rx, ry) make( <ellipsoid>, rx: rx, ry: ry )
end;
define function circle (r) make( <circle>, r: r ) end;
Note that I don't define an ellipse() constructor, because I decided
the intent of this example code is to make it easy to create either a
general ellipsoid or a circle specifically. From this perspective, only
<ellipsoid> and <circle> are potential "public" classes. <ellipse> is
entirely an implementation detail that should remain private. (And
<circle> proper may be private as well, even though the concept of
circles is represented by the protocol.)
In fact, you might decide that <ellipsoid> should be named <ellipse>
and the implementation class should instead be named something like
<proper-ellipse>. Again, it all has to do with the context of this code
and your intent for its use. It has little to do with whether the set
of all circles are a proper subset of the set of all ellipses in
geometry, unless it is your intent, specifically, to model that
relationship.
--
Chris Page - Software Wrangler - Palm, Inc.
SmartFriends(TM) U: Languages and Libraries, Sept. 26-28
Keynote: STL Creator, Alexander Stepanov
<http://SmartFriends.com/U/?ll1>