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?

drkvogel
  • 147
  • 3
  • 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 '22 at 01: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. – Filip Milovanović Jan 21 '22 at 03:38
  • 9
    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). – Filip Milovanović Jan 21 '22 at 03: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 '22 at 09:54

4 Answers4

8

"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 already implement Ellipse methods, the design starts to look not that much overcomplicated any more.

Doc Brown
  • 199,015
  • 33
  • 367
  • 565
8

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.

Christophe
  • 74,672
  • 10
  • 115
  • 187
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.

gnasher729
  • 42,090
  • 4
  • 59
  • 119
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.

Martin Maat
  • 18,218
  • 3
  • 30
  • 57