[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 )
     make( <ellipse>, rx: rx, ry: ry )
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 ) 
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 

Chris Page - Software Wrangler - Palm, Inc.

   SmartFriends(TM) U: Languages and Libraries, Sept. 26-28
   Keynote: STL Creator, Alexander Stepanov