The circle–ellipse problem claims that OOP's inheritance is flawed due to the simple case of a circle not being able to inherit from an ellipse:
class Ellipse {
void stretchHorizontally(int amount) { ... }
void stretchVertically(int amount) { ... }
...
}
class Circle extends Ellipse {
// Oops, a circle doesn't support horizontal and vertical stretch!
}
Nor an ellipse being able to inherit from a circle:
class Circle {
int radius;
...
}
class Ellipse extends Circle {
// Oops, an ellipse doesn't support a constant 'radius' field!
}
But this is obviously hogwash.
The data-efficient solution: Separate the two shapes completely
We can hopefully all agree that an ellipse is not a circle, but what if we decided that a circle wasn't an ellipse either? Instead we could separate the classes from each other, and at most have them implement a common Round
interface:
interface Round {
int getWidth();
int getHeight();
}
class Circle implements Round {
int radius;
int getWidth() {
return radius;
}
int getHeight() {
return radius;
}
}
class Ellipse implements Round {
int width;
int height;
int getWidth() {
return width;
}
int getHeight() {
return height;
}
}
This works, and it works pretty well to be honest, but there is an ideological problem that I can already hear half the readers screaming: mathematically a circle is also an ellipse, OOP is broken!
The ideal world solution: A circle class doesn't exist
Find a ball with a constant radius, like a soccer ball or a tennis ball. Now step on it. Congratulations, you just broke OOP by changing an object's type from a ball to an ellipsoid with nothing but the bottom of your foot!
Like I said earlier, hogwash.
A ball is not a class, a ball is an object. An object of type ellipsoid that happens to have the same length in all three dimensions.
Now apply this logic to the 2D world and we get:
class Ellipse {
int width;
int height;
static createCircle(int radius) {
return new Ellipse(radius, radius);
}
void stretchWidth(amount) { ... }
void stretchHeight(amount) { ... }
}
var tennisCircle = Ellipse.createCircle(3);
// Oh no, why would an user of my class stretch their tennis circle!
tennisCircle.stretchWidth(1);
// Wait, why would I care? Maybe their game supports bouncy circles.
// If not, they probably shouldn't have called that particular method.
And this solves the "problem"; the issue was never in inheritance itself, but in the assumption that we should use inheritance here in the first place. While a circle is an ellipse, it's not a new subclass, just an instance. What's next, a new subclass of Human
for every single possible height,weight
combination?
Now to be fair there is an unfortunate real world issue with this ideal world solution; data efficiency. Our computers have a finite amount of memory in them, and wasting data is typically non-ideal. If you're working with limited data or countless circles, you might want to minimize the number of fields in your class. This is nothing but you taking a conscious step away from an ideal world implementation to bend to our hardware limitations. And this is why I introduced the alternative, data-efficient approach as well.
At the end of the day all paradigms are just that, paradigms. They're not some universal laws you must abide by at all cost. A great developer understands when to take a non-ideal route.
Top comments (0)