0

I have an abstract base class Shape with three derived classes: Arc, Circle, and Rectangle. I have implemented an equality check in these classes, and I want to consider Arcs equal to Circles if they have the same shape. To do this, I check the type of the Shape, downcast it, and then perform additional checks to determine if the two Shapes represent the same geometry. However, I am concerned that this approach may be a code smell and may not be the correct way to handle this situation. For example, if someone were to inherit from Shape in the future and create a class that can have a similar shape to an Arc, my current approach would not work. Is there a better way to handle this situation? My current code looks like this:

public abstract class Shape : IEquatable<Shape>
{
    public abstract bool Equals(Shape other);
}

public class Arc : Shape
{
    public override bool Equals(Shape other)
    {            
        if (other is Arc || other is Circle)
        {
            // Downcast and check for equality criteria!!
        }
        else
        {
            return false;
        }
    }
}

public class Circle : Shape
{
    public override bool Equals(Shape other)
    {
        if (other is Arc || other is Circle)
        {
            // Downcast and check for equality criteria!!
        }
        else
        {
            return false;
        }
    }
}

public class Rectangle : Shape
{
    public override bool Equals(Shape other)
    {
        if (other is Rectangle)
        {
            // Downcast and check for equality criteria!!
        }
        else
        {
            return false;
        }
    }
}

UPDATE:

After considering the answers and thinking further, I believe I need to change my design as follows: the Shape class should have methods like IsEqualToCircle(Circle circle) and IsEqualToArc(Arc arc). Then, in the equality check for the Arc class, I can write if (shape.IsEqualToArc(this)) return true; and so on. The example shown here is just a simplification to illustrate a problem I have encountered in many situations. Of course, this is not real code

Vahid
  • 5,144
  • 13
  • 70
  • 146
  • BTW in C#7 you can combine the test and the cast in one: `if (other is Rectangle rect) ...`. – Ian Mercer Oct 27 '18 at 15:19
  • 2
    @IanMercer Yes that is actually great, but does not solve this problem :) – Vahid Oct 27 '18 at 15:24
  • So what is your question? Yiu already know you may get problems here. – MakePeaceGreatAgain Oct 27 '18 at 15:26
  • @HimBromBeere The question is clear. What is the problem with this design. – Vahid Oct 27 '18 at 15:30
  • Couldn't you simply implement a user-defined conversion to convert a `Circle` to an `Arc`? Which should always be possible if I understood you right. Then you can do your equality check on the resulting Arc. – Joe Oct 27 '18 at 15:41
  • "How can I do this the right way" Is quite opinion-based and highly depends on your context. In particular there is no single "right" way, there may be many different ones. – MakePeaceGreatAgain Oct 27 '18 at 15:42
  • @Joe Then I need conditions to check if it is an `Arc` or `Circle`. That does not make my code bulletproof to future extensions. – Vahid Oct 27 '18 at 15:44
  • @Vahid comments are for comments and answers are for answers and "BTW" means "by the way", i.e. it's tangential not an answer. Welcome to SO. – Ian Mercer Oct 27 '18 at 19:44
  • @IanMercer I have been enough on SO to know that, I also know that I should never write irrelevant comments just for the sake of saying something. It is not useful, it is distracting and kills a lot of time. – Vahid Oct 28 '18 at 09:53

4 Answers4

4

This seems to stem from the fact that your representation is redundant. Instead of exposing the concrete shape constructors directly to your client classes, have them use factory methods instead. You could either have a CreateCircle method that just creates a 360° arc internally, and remove the circle class completely, or have a CreateArc method that actually returns a Circle instance if the arc is 360°, whichever is best. Then you will never have instances of different classes that are supposed to be equal.

Jonas Høgh
  • 10,358
  • 1
  • 26
  • 46
  • Thanks for the answer. I understand that can work for this particular example, but this was just a simple example to show the problem, for example let's say I want to compare a line to polyline and I really want them to have their own separate classes, – Vahid Oct 27 '18 at 15:20
  • 1
    Couldn't you just represent single segment polylines as lines in the same way? – Jonas Høgh Oct 27 '18 at 15:26
  • For example I have `Line CreateLine()` and `PolyLine CreatePolyLine()` methods in the my factory. They create a single segment shapes for me: Line and PolyLine both deriving from Shape. At some point I still need to downcast to check if they are equal. – Vahid Oct 27 '18 at 15:30
  • Not if you create single lines as PolyLines containing only one segment. Then you will always be comparing polylines. – Olivier Jacot-Descombes Oct 27 '18 at 16:49
3

I would go one step back and think about your classes. What have those in common? Obviously an arc is some linear feature, whereas a circle or a rectangle are areal ones. So in fact the have different dimensions, the former being 1-dimensional, while the latter is 2-dimensional. So why should comparing a line-feature with an area-feature return equality?

So better IMHO is to have some base-interface, e.g. IGeometry, for all geometry-types where you define an Equals-method. If two shapes have the same dimension, they may be compared any further. Otherwise they are not equal. Having said this an Arc is never equal to a Circle. However the circles Boundary (if you have such a property defined), which could be implemented as an instance of a 360° Arc, may be equal to an Arc, as both are 1-dimensional.

MakePeaceGreatAgain
  • 35,491
  • 6
  • 60
  • 111
3

The bells and whistles textbook solution for this kind of problem in OOP is the Visitor pattern. I'm not a big fan of this solution as it does carry a lot of overhead, but it will enable you to have multiple dispatch, that is, essentially one comparison method for every pair or shape types. This will obviously result in a lot of code where you have to state that a line is never equal to a circle by just returning false and so on, but if extensibility with low risk of errors is a major concern to you, it has the benefit of compile time checking that any newly introduced Shape subclass has a comparison method for all of the other subclasses.

Jonas Høgh
  • 10,358
  • 1
  • 26
  • 46
2

This whole set up is a code smell. Two objects of different types shouldn’t be equal even if they represent the same reality.

There are two ways you can go here:

  1. Implicit operators

    This is for instance how (1.0).Equals(1) returns true; the argument integer 1 is implicitly converted to floating point 1.0 and the equality check succeeds. It’s not that the class Double somehow knows how compare integers and doubles, it doesn’t, it only knows how to compare dohbles.

    Note that 1.Equals(1.0) will fail, because argument double 1.0 is not implicitly convertible to integer 1; different types... weird, huh? One way it’s true, the other isn’t...

  2. An explicit conversion mechanism the user can use safely. Arch could have a method ToCircle that could produce the equivalent circle when posible and fail otherwise. To make it safe you’d also need the property IsCircle to flag if the method is safe to call.

InBetween
  • 32,319
  • 3
  • 50
  • 90