[comp.lang.c++] Design question

roger@procase.UUCP (Roger H. Scott) (05/25/90)

In article <CLINE.90May23160755@cheetah.ece.clarkson.edu> cline@sun.soe.clarkson.edu (Marshall Cline) writes:
>...
>	class Locn { ... };			// an (x,y) pair
>	class Edge { ... };			// contains two Locn's
>	class Region { ... };			// contains a list of Edge's
>	class Segment : public Region { ... };	// only has one edge
>	class Point   : public Segment { ... };	// zero length segment
>	class Ray     : public Segment { ... };	// one endpoint at infinity
>	class Line    : public Ray { ... };	// other endpoint at infinity
>
>It is unfortunate that to find the actual (x,y) of a Point, you have to
>realize that a Point is-a Segment which is-a Region which has-a Edge which
>has-a Locn, when the point itself could simply be the Locn.  The space burden
>of this seems harsh.

You're heading in the right direction, but you're not quite there.  Try this:

    #define redefine
    typedef int Coord;

    class Location {
    public:
	static const Location Infinity;

	Location(Coord, Coord);
	Coord x(), y();
	double distanceTo(Location);

    protected:
	Coord myX, myY;
    };

    class EdgeBlock {
    public:
	virtual proc(Location, Location) {}
    };

    class Region;
    class Segment;
    class Point;
    class Line;
    class Ray;

    class Region { // abstract class
    public:
	virtual void doForEdges(EdgeBlock&) = 0;
	virtual double area() = 0;
    };

    class Segment : public Region { // abstract class
    public:
	virtual Location oneEnd() = 0;
	virtual Location otherEnd() = 0;
	virtual double length() {return oneEnd().distanceTo(otherEnd());}
	redefine void doForEdges(EdgeBlock& block) {
	    block.proc(oneEnd(), otherEnd());
	}
	redefine double area() {return 0;}
    };

    class Point : public Segment { // abstract class
    public:
	virtual Location where();
	redefine Location oneEnd() {return where();}
	redefine Location otherEnd() {return where();}
	redefine double length() {return 0;} // optimization
    };

    class Ray : public Segment { // abstract class
    public:
	virtual Location theEnd();
	redefine Location oneEnd() {return theEnd();}
	redefine Location otherEnd() {return Location::Infinity;}
	redefine double length() {return INFINITY;} // optimization
	virtual double slope() = 0;
    };

    class Line : public Ray { // abstract class
    public:
	redefine Location oneEnd() {return Location::Infinity;}
	virtual Location intersept() = 0;
    };


    class RealRegion : public Region {
    public:
	...
	redefine void doForEdges(EdgeBlock& block) {
	    // block.proc() for each edge
	}
	redefine double area();

    protected:
	// Locations for vertices of boundary?
    };

    class RealSegment : public Segment {
    public:
	RealSegment(Location l1, Location l2) : myL1(l1), myL2(l2) {}
	redefine Location oneEnd() {return myL1;}
	redefine Location otherEnd() {return myL2;}
    
    protected:
	Location myL1, myL2;
    };

    class RealPoint : public Point {
    public:
	RealPoint(Location whr) : myWhere(whr) {}
	redefine Location where() {return myWhere;}
    
    protected:
	Location myWhere;
    };

    class RealRay : public Ray {
    public:
	RealRay(Location end_pt, double sloap) : myEnd(end_pt), mySlope(sloap) {}
	redefine Location theEnd() {return myEnd;}
	redefine double slope() {return mySlope;}

    protected:
	Location myEnd;
	double mySlope;
    };
    
    class RealLine : public Line {
    public:
	RealLine(double sloap, Location inter) : mySlope(sloap), myIntersept(inter) {}
	redefine double slope() {return mySlope;}
	redefine Location intersept() {return myIntersept;}

    protected:
	double mySlope;
	Location myIntersept;
    };

Now a RealPoint contains just a pair of coordinates.  Inheriting protocol is
much more important than inheriting implementation.  As you demonstated, if
one doesn't construct hierarchies of abstract classes one is often forced to
inherit cumbersome implementation as the "price" of inheriting the desired
type and interface.  Note that getting the (x,y) for a Point is simple - you
just return the appropriate member data.  The operation that's interesting is
doForEdges():

    Point::doForEdges == Segment::doForEdges = {
	block.proc(
	    Point::oneEnd = {
		return RealPoint::where = {
		    return myWhere;
		}
	    },
	    Point::otherEnd = {
		return RealPoint::where = {
		    return myWhere;
		}
	    }
	)
    }

johnson@p.cs.uiuc.edu (05/26/90)

Reid Spencer wrote:
> Initially, we might think of a hierarchy containing an extremely abstract
> type "geo_obj" as the base of the hierarchy:
>		 geo_obj
>			point
>			line
>				ray
>					segment
>			region

Marshall Cline didn't like this, suggesting that ray and line were actually
subtypes of segment.  Rays are just segments with one end point infinitely
far away, while both of the endpoints of a line are infinitely far away.
Walt Peterson argued the opposite, that a line has a slope and a zero 
intercept, while a ray and a segment each add a constraint of an endpoint.
Both points of view are valid in certain circumstances, which leads to
my claim:

	line and segment are not subtypes of each other.

Taking either point of view is too restrictive.  Either class hierarchy
will not be general purpose.  The solution is an abstract class.

A good rule of thumb is that most superclasses in a library should be
abstract.  Subclassing a nonabstract class is usually a little messy.
It is fine for application programmers, who just want to get their job
done and are not so concerned about other people having to reuse and
learn their code, but it is usually a bad idea for class library designers
to use it.

I would have a class "AbstractLine" that defines abstract operations
to return the slope, a point on the line (if any) that intersects with
another line, etc.  Each subclass will implement these operations
differently.  This will allow each subclass to have the most efficient
implementation, rather than using an over general one.


Ralph Johnson -- University of Illinois at Urbana-Champaign