A legendary conundrum of object oriented design.

Mathematicians tell us that a circle is a special kind of ellipse, one whose eccentricity is 0, and whose foci coincide.

When we're designing object-oriented systems, the phrase "A is a kind of B" is supposed to ring all sorts of warning bells telling us that A should inherit from B.

Yet, if we go ahead and make classes Circle and Ellipse, where Circle inherits from Ellipse, we could be letting ourselves in for all sorts of problems.

Consider the following C++ declarations:

class Ellipse

 double f1x;
 double f1y;
 double f2x;
 double f2y;
 double k;


 Ellipse (double ifx1, double ify1, double ifx2, double ify2, double ik);
 Ellipse (Ellipse const &ie);
 virtual void draw_me (drawing_surface &surf);

 virtual void get_foci (double &f1x, double &f1y, dobule &f2x, double &f2y) const;
 virtual double eccentricity() const;
 virtual void translate (double ncx, double ncy);
 virtual void rotate (double radians);
 virtual void affine (double a, double b, double c, double d, double e, double f);
 virtual ~Ellipse() {}

class Circle: public Ellipse

 Circle (double icx, double icy, double radius);
 Circle (Circle const &);
 virtual void draw_me (drawing_surface &surf); // does something more efficient?

 virtual double eccentricity() { return 0.0; }
 virtual void rotate (double radians); { /* no-op and no need to redraw */ }
 virtual void affine (double a, double b, double c, double d, double e, double f); // Uh-oh...

Circle has a special constraint on it, that its foci shoud coincide (that is, f1x == f2 x && f1y == f2y). Yet this constraint is not invariant under affine transformations. Any implementation of Ellipse::affine is likely to break any Circle it's performed on. Yet affine is a reasonable operation on the general run of ellipse, and it always results in another ellipse!

What can you do?

  1. Punt, and let Ellipse::affine break Circles. This isn't nice to do to people using your code. But of course, it may do the job.
  2. Forget about Ellipse::affine. But this may be the most important thing your Ellipse can do.
  3. Forget about making Circle inherit from Ellipse, that is, code two separate classes. Of course, Circle won't be able to do the things Ellipse can any more.
  4. Forget about Circle altogether, and let Ellipse do all the work. Your customer will then take a micrometer to any printed diagrams using Ellipse and go ballistic when rounding errors make them go slightly out of shape.
  5. Use the envelope / letter idiom: an abstract class AEllipse without affine, concrete classes Ellipse (with affine) and Circle that inherit from AEllipse, as well as a smart pointer type that inherits from AEllipse, and can be set to either Ellipse or Circle. This is extremely complicated and will make you finish a year late and two million dollars over budget. You may be lucky enough to have a boss enlightened enough to realize that this was the salesman's fault for lowballing the bid. Who knows?
Of course, this problem is not restricted to these particular geometric shapes. In fact, it is a very common occurrence. Any time you have:
  • A more general class of possible objects
  • A more specialized class of objects, a subset of the more general class based upon a more restrictive invariant
  • A reasonable operation on the more general class which breaks the more specialized class
this problem is likely to crop up. I'm sure you can think of dozens of cases.

So what is a designer to do? Notice that all of the examples above involved different expectations by the customer. The answer is, then,

  • Design before you start coding. Make sure your design meets your customer's requirements. Don't let anyone distract you from this goal by telling you to do it "the right way". If you meet your design goals, you are doing it "the right way", by definition!
  • Make sure a code library meets your design requirements before you buy it, even if it looks like it solves your problem.
  • CYA. Make sure that someone higher up, or even the customer, signs off on your design decisions.