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.
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 friend
s 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.