3

I was reading about composition over inheritance and came across a question about solving the Circle-Ellipse Problem in Object-Oriented Programming. This kind of problem is often used as an example of the limitations of OOP. I am from a C++ and Java, therefore heavily OOP, background, but I have struggled with trying to fit real-world scenarios into OOP so am well aware of the paradigm's flaws. Recently I have been learning Rust and Golang, neither of which have inheritance, but instead have mechanisms for composition, as does Python, which I have been using a lot in the last few years. Clearly this is the way modern programming languages are going, and going away from OOP!

In this StackOverflow answer, someone states that:

There is no programmatic problem you can solve with inheritance which you cannot solve with composition

though there is no source given for this assertion. I wonder if it is true? And to get back to my question: can composition "solve" the Circle-Ellipse Problem?

5
  • Perhaps I should rephrase this question - the Circle-Ellipse Problem is a problem specific to OOP and OO languages, I realise - the question perhaps should be: "What is the appropriate way to represent (e.g.) a circle type and an ellipse type if favouring composition over inheritance?"
    – drkvogel
    Jan 21, 2022 at 1:10
  • 4
    The circle-ellipse problem is more of a problem of how the two types are conceptualized; it's not that you can't derive Circle from Ellipse in general, but rather that you can't do it if the type of Ellipse promises to its users that Ellipse.Radius1 and Ellipse.Radius2 are independent properties. It's like, say, Stack promising that Push() will place the element on top, and then you create a DerivedStack that pushes the element to the bottom. Client code that receives that object while using the parent type polymorphically will crash, because it has expectations on the type. Jan 21, 2022 at 3:38
  • 10
    In other words, it's not really a problem to be solved, but an illustrative example of how the meaning of the "is-a" relationship depends on what you're doing (a circle is a kind of ellipse by mathematical criteria, a Circle type is not necessarily a kind of Ellipse type, depending of how you defined the Ellipse type; the criterion used is different - LSP). Jan 21, 2022 at 3:57
  • 2
    @FilipMilovanović I am really glad you wrote this comment. For years I've been pushing back against the square rectangle problem (or at least what people use it for) but never found the right words to explain my gripes with it.
    – Flater
    Jan 21, 2022 at 9:54
  • Circles & ellipses are sets of points. The only problem in the Circle-Ellipse Problem is thinking it's about circles & ellipses.
    – philipxy
    Dec 6, 2023 at 15:41

5 Answers 5

10

"Composition over inheritance" for the Circle-Ellipse problem means to implement a class Circle by making internally use of an Ellipse object, like this:

  class Circle
  {
     Ellipse e;

     public Circle(double midpoint, double radius)
     {
        double width=radius;
        double height=radius;

        // for the sake of simplicity,
        // lets assume this is just an axis-parallel ellipse,
        // not an arbitrary oriented one
        e=new Ellipse(midpoint, width, height); 
     }
     public double radius()
     {
         return e.width();
     }
     public void stretch(double factor)
     {
        e.stretchX(factor);
        e.stretchY(factor);
     }
     // ...
     public Ellipse asEllipse()
     {
        return new Ellipse(e);   // makes a copy!
     }
 }

This design allows a Circle to offer its own interface and invariants, decoupled from the invariants of Ellipse, even when Circle and Ellipse are mutable classes. Without inheritance, there is obviously no LSP violation, hence no Circle-Ellipse problem any more.

In the Wikipedia article, you find this approach under "Drop all inheritance relationships". It's drawback is that one cannot pass a Circle to functions which expect to get an Ellipse. This can often be circumvented by utilizing the asEllipse method scetched above.

Of course, using an Ellipse for implementing Circle may appear to be more complex than necessary at a first glance. By implementing it just by using a radius attribute and a midpoint attribute seems to be more simple and straightforward.

However, this depends a lot on which other methods the Circle class needs to provide, and how many methods of an existing Ellipse can be reused. Think of methods like double area(), drawOnCanvas(), calcIntersectionsWithLine(), for example. If a Circle object can delegate these methods to existing Ellipse methods, the design can be quite sensible.

9

The circle vs ellipse problem (as well as the square vs rectangle) are not an inheritance problem but a problem regarding the contracts and promises made about the initial class.

In the first place, why modifying circles? From a mathematical perspective there is no such thing as modifying a shape: there are different shapes.With immutable ellipses, there is no longer an issue regarding LSP, because in all its other operations, you may substitute a circle to an ellipse without problem.

Next, why to promise an independent change of x and y or of rhe two centers of the ellipse, when you know that some special cases of the shape link both? So if you redefine your contract by only promising that a change of x will yield a new x, you’re also out of trouble and have a more general ellipse than accepts also the special case of the circle.

Now if you go for composition, there is no inheritance anymore. So circle is no longer a subtype of ellipse. And there is no foundation for looking at LSP at all.

2

If you have a class implementing an ellipse, then it is quite easy to create a class implementing a circle, which uses an ellipse internally to do all the hard work, and that ellipse would always have two equal axes. So you never run into the problem. You have two different classes, and nobody would expect your "Circle" class to behave like your "Ellipse" class. Circle wouldn't have a "setHeight" and "setWidth" method, for example.

That doesn't solve the Ellipse/Circle problem, but it avoids it.

For C++, it would be an interesting language feature to have an “opaque” subclass, meaning that you could make Circle an opaque subclass of Ellipse, and not allowing a Circle be used where an Ellipse is expected or a Circle* where an Ellipse* is expected.

1

Composition cannot solve the circle-ellipsis problem. But there are other ways to solve it, for example by:

  • Have the shapes be immutable objects.
  • Have the shapes be represented by the same type

Composition is sometimes erroneously presented as the solution to all OOP problems, but to see why this isn't the case, note that classical inheritance in OOP is really two separate things which are often conflated:

  1. Subtyping: A type can be declared as a subtype of another type, and code that supports the supertype will transparently support the subtype.
  2. Implementation inheritance: Behavior can be shared between subtypes through inheritance (like virtual methods).

In Java, classes support both subtyping and implementation inheritance but interfaces support only subtyping. Some languages like Rust and Go support subtyping without supporting implementation inheritance.

Implementation inheritance causes a tight coupling between classes which is often undesirable. Composition is an alternative way to share implementation between subtypes, which is often preferred. So composition can be said to solve the problem of implementation inheritance.

But the circle-ellipsis is a problem of subtyping. Therefore composition is not a solution, and languages like Rust and Go are still subject to the circle-ellipsis problem through their support for subtyping.

So why is the circle-ellipsis problem so difficult to solve? It is difficult because it is a conceptual problem. It is like a round square - there is no programmatic solution to getting a round square, since it is a contradiction by definition. In the same way, the circle-ellipsis problem artise from contradictory ideas.

The ideas:

  • The types in code should reflect concepts and categories used in "the real world" ie. other domains than software.
  • Object have a type that cannot change type at runtime
  • Objects have properties that can be changed at runtime.

The circle-ellipsis problem shows you can't have all three. You have to reject at least one of them.

0

Circle is not a class, it is just a label a human may slap on an ellipse when he spots a certain state. Therefore at most it could be a property of Ellipse: IsCircle. The whole premise of a problem is false.

As for "you can do anything you can do with inheritence with composition", sure. You can do anything you can do with a shovel with a nail clipper. Knock yourself out.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.