c++oopinterface

How do interfaces solve the circle-ellipse problem?


It is sometimes said that interfaces solve several problems of object-oriented programming, in particular the circle-ellipse problem. It would be this interface

class IEllipse {
public:
  virtual double GetXRadius() const = 0;
  virtual double GetYRadius() const = 0;
  virtual void SetXRadius(double r) = 0;
  virtual void SetYRadius(double r) = 0;
} 

But then we'd still have this kind of problematic function, that works for some implementations of IEllipse, but not for the circle implementation (that plugs both SetXRadius and SetYRadius to the same storage variable radius):

std::string SerializeDestructively(IEllipse& ellipse) {
  std::stringstream s;
  s << ellipse.GetXRadius() << " ";
  ellipse.SetXRadius(0.0);
  s << ellipse.GetYRadius();
  ellipse.SetYRadius(0.0);
  return s.str();
}

It seems the problem is rather that IEllipse represents a resizable ellispe, in each dimension. The circle does not implement that interface, because when flattened, it is no longer a circle. And writing the definition of an ellipse in an interface does not solve that.


Solution

  • Interfaces do not solve the circle-ellipse problem and cannot solve the problem, at least if you're trying to also maintain the mathematical properties of each shape. The reason this problem is used is because it's a good example of a mathematical concept (a circle is a special kind of ellipse) not working with proper inheritance (i.e., the Liskov substitution principle).

    Here's a silly example that shows the problem:

    void makeTallAndSkinny(IEllipse &e) {
        auto const oldX = e.GetXRadius();
        auto const oldY = e.GetYRadius();
        e.SetYRadius(oldY * 2);
        e.SetXRadius(oldX * 0.5);
    }
    

    The only way an interface would work is if you break the contract of IEllipse to the point where SetXRadius and SetYRadius are effectively meaningless. In fairness you could also break the concept of Circle to the point where it's also meaningless (at least compared to the mathematical notion of a circle), but you're still breaking something.

    This is also true if your Mammal interface assumes GivesLiveBirth() == true and you have Platypus modeled as a Mammal. Just because a relationship matches your mental model doesn't mean it fits with interfaces and object-oriented programming.

    What you could do in C++ is have Circle leverage private inheritance. That would let Circle and friends use it as an IEllipse where appropriate, in cases where they knew the interface may not perfectly hold but it's safe (e.g., not my example above). Generic code wouldn't be able to pass a Circle though, which maintains the type model your program depends on it.