Question

Having Circle extend Ellipse breaks the Liskov Substition Principle, because it modifies a postcondition: namely, you can set X and Y independently to draw an ellipse, but X must always equal Y for circles.

But isn't the problem here caused by having Circle be the subtype of an Ellipse? Couldn't we reverse the relationship?

So, Circle is the supertype - it has a single method setRadius.

Then, Ellipse extends Circle by adding setX and setY. Calling setRadius on Ellipse would set both X and Y - meaning the postcondition on setRadius is maintained, but you can now set X and Y independently through an extended interface.

Was it helpful?

Solution

But isn't the problem here caused by having Circle be the subtype of an Ellipse? Couldn't we reverse the relationship?

The problem with this (and the square/rectangle problem) is falsely assuming a relationship in one domain (geometry) holds in another (behaviour)

A circle and ellipse are related if you are viewing them through the prism of geometrical theory. But that is not the only domain you can look at.

Object orientated design deals with behaviour.

The defining characteristic of an object is the behaviour the object is responsible for. And in the domain of behaviour, a circle and ellipse have such different behaviour that it is probably better not to think of them as related at all. In this domain, an ellipse and a circle have no significant relationship.

The lesson here is to choose the domain that makes the most sense for OOD, not to try and shoehorn in a relationship simply because it exists in a different domain.

The most common real-world example of this mistake is to assume objects are related (or even the same class) because they have similar data even if their behaviour is very different. This is a common problem when you start constructing objects "data first" by defining where the data goes. You can end up with a class that are related via data that have completely different behaviour. For example, both the payslip and the employee objects might have a "gross salary" attribute, but an employee is not a type of payslip and a payslip is not a type of employee.

OTHER TIPS

Circles are a special case of ellipses, namely that both axes of the ellipsis are the same. It is fundamentally false in the problem domain (geometry) to state that ellipses might be a kind of circle. Using this flawed model would violate many guarantees of a circle, for example “all points on the circle have the same distance to the center”. That too would be a Liskov Substitution Principle violation. How would an ellipse have a single radius? (Not setRadius() but more importantly getRadius())

While the modelling of circles as a subtype of ellipses is not fundamentally wrong, it is the introduction of mutability that breaks this model. Without the setX() and setY() methods, there is no LSP violation. If there is a need to have an object with different dimensions, creating a new instance is a better solution:

class Ellipse {
  final double x;
  final double y;
  ...
  Ellipse withX(double newX) {
    return new Ellipse(x: newX, y: y);
  }
}

Cormac has a really great answer, but I just want to elaborate a bit on the reason for the confusion in the first place.

Inheritance in OO is often taught using real-world metaphors, like "apples and oranges are both sub-classes of fruit". Unfortunately this leads to the mistaken belief that types in OO should be modeled according to some taxonomical hierarchies existing independent of the program.

But in software design, types should be modeled according to the requirements of the application. Classifications in other domains are usually irrelevant. In an actual application with "Apple" and "Orange" objects - say an inventory management system for a supermarket - they will probably not be distinct classes at all, and categories like "Fruit" will be attributes rather than supertypes.

The circle-ellipse problem is a red herring. In geometry a circle is a specialization of an ellipse, but the classes in your example are not geometrical figures. Crucially, geometrical figures are not mutable. They can be transformed, though, but then a circle can be transformed into an ellipsis. So a model where circles can change radius but not change to an ellipsis does not correspond to geometry. Such a model might make sense in a particular application (say a drawing tool) but geometrical classification is irrelevant for how you design the class hierarchy.

So should Circle be a subclass of Ellipse or vice versa? It totally depends on the requirements of the particular application which uses these objects. A drawing application could have different choices in how to treat circles and ellipses:

  1. Treat circles and ellipses as distinct types of shapes with different UI (e.g. two resize-handles on an ellipsis, one handle on a circle). This means you can have an ellipse which is geometrically a circle but not a Circle from the perspective of the application.

  2. Treat all ellipses including circles the same, but have an option to "lock" x and y to the same value.

  3. Ellipses are just circles where a scaling transformation have been applies.

Each possible design will lead to different object model -

In the 1st case, Circle and Ellipses will be sibling classes

In the 2nd one, there will not be a distinct Circle class at all

In the 3rd one, there will not be a distinct Ellipse class. So the so-called circle-ellipse problem does not enter the picture in any of these.

So to answer the question as posed: Should circle extend ellipse? The answer is: It depends on what you want to do with it. But probably not.

It's a mistake from the start to insist of having an "Ellipse" and a "Circle" class where one is a subclass of the other. You have two realistic choices: One is to have separate classes. They may have a common superclass, for things like color, whether the object is filled, line width for drawing etc.

The other is to have one class named "Ellipse" only. If you have that class, it's easy enough to use it to represent circles (there may be traps depending on implementation details; an Ellipse will have some angle and the calculation of that angle must not run into trouble for a circle-shaped ellipse). You could even have specialised methods for circular ellipses, but these "circular ellipses" would still be full "Ellipse" objects.

Following the LSP points, one 'proper' solution to this problem is as @HorusKol and @Ixrec came upon - deriving both types from Shape. But it depends on the model you are working from, so you should always go back to that.

What I was taught is:

If the sub-type cannot perform the same behaviour as the super-type, the relationship doesn't hold in the IS-A premise - it should be altered.

  • A sub-type is a SUPERSET of the super-type.
  • A super-type is a SUBSET of the sub-type.

In english:

  • A derived-type is a SUPERSET of the base-type.
  • A base-type is a SUBSET of the derived-type.

(Example:

  • A car with a bad-boy exhaust is still a car (according to some).
  • A car without an engine, wheels, steering rack, drivetrain, and only the shell left, is not a 'car', it's just a shell.)

That's how classification works (ie. in the animal world), and in principal, in OO.

Using this as the definition of inheritance and polymorphism (which always get written together), if this principle is broken, you should be trying to re-think the types you are trying to model.

As mentioned by @HorusKul and @Ixrec, in maths you have clearly defined types. But in maths, a circle is an ellipse because it is a SUBSET of ellipse. But in OOP this is not how inheritance works. A class should only inherit if it is a SUPERSET (an extension) of an existing class - meaning, it still IS the base class in all contexts.

Based on that, I think the solution should be slightly reworded.

Have a Shape base type, then RoundedShape (effectively a circle but I've used a different name here DELIBERATELY...)

...then Ellipse.

That way:

  • RoundedShape is a Shape.
  • Ellipse is a RoundedShape.

(This now makes sense to people in language. We have a clearly defined concept of a 'circle' in our minds already, and what we're trying to do here by generalising (aggregation) breaks that concept.)

From an OO perspective ellipse does extend circle, it specializes on it by adding some properties. The existing properties of circle still hold in ellipse, it just gets more complex and more specific. I do not see any issues with behavior in this case like Cormac does, shapes have no behavior. The only trouble is that in a liguistic or mathematical sense it does not feel right to say "an ellipse IS A circle". Because the whole point of the exercise that isn't mentioned but is nonetheless implicit, was to classify geometric shapes. That may be a good reason to regard circle and ellipse as peers, not link them by inheritence and accept that they just happen to have some of the same properties and NOT let your twisted OO mind have its way with that observation.

Licensed under: CC-BY-SA with attribution
scroll top