Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Designing an abstract base class. What types to use, abstract or concrete?

Tags:

java

c#

I am just jumping into C# from Java on the recommendation of my uncle. The Java geometry lib seems more complete than C#'s Drawing lib, so I am working on a simple bit of porting with a small bit of added functionality to begin into C#.

However, I have run into a design issue and cannot discern which would be the better choice. To have multiple methods in the abstract base class that take concrete datatypes OR to have less methods that take the abstract base as its argument?

public abstract bool isOverlapping(GeometricObject2D o) {}

OR

public abstract bool isOverlapping(Rectangle2D rect) {}
public abstract bool isOverlapping(Circle2D circ) {}
etc...

The argument I am having in my head tells me concrete arguments prevent logic errors, BUT, I have been taught to always use abstract datatypes if the use fits.

like image 354
kicks Avatar asked Dec 05 '22 13:12

kicks


2 Answers

If you want to put the operation in the base class, use the abstract type. You don't want to have to modify the base class every time you decide to add a new subclass.

An alternative is to use something like the visitor pattern and have each concrete class dispatch in turn to the visitor. An intersection visitor would then contain all the knowledge of how to compute the intersection of each pair of object types.

Here's how the visitor pattern can be used for this. (I'll use Java syntax since I'm not a C# programmer). First, using the visitor pattern here is more complicated than the usual case because you have to modify the operation based on the types of two arguments. In effect, you need triple dispatch. Languages like Clojure support this directly, but in Java (and probably C#) you need to simulate triple dispatch by using two levels of visitor. It's ugly, but the great benefits are that it keeps your geometry hierarchy clean and maintainable, and it centralizes all intersection logic in one compilation unit.

public interface IGeometry {
    void accept(IGeometryVisitor visitor);
}

public interface IGeometryVisitor {
    void visitCircle2D(Circle2D circle);
    void visitBox2D(Box2D box);
    // a method for each concrete type
}

public class Circle2D implements IGeometry {
    public void accept(IGeometryVisitor visitor) {
        visitor.visitCircle2D(this);
    }
}

public class Box2D implements IGeometry {
    public void accept(IGeometryVisitor visitor) {
        visitor.visitBox2D(this);
    }
}

public class IntersectionVisitor implements IGeometryVisitor {
    private boolean mResult;
    private IGeometry mGeometry2;

    public static boolean isOverlapping(IGeometry geometry1, IGeometry geometry2) {
        return new IntersectionVisitor(geometry1, geometry2).mResult;
    }

    private IntersectionVisitor(IGeometry geometry1, IGeometry geometry2) {
        mGeometry2 = geometry2;
        // now start the process
        mGeometry1.accept(this);
    }

    public void visitCircle2D(Circle2D circle) {
        mGeometry2.accept(new Circle2DIntersector(circle));
    }

    private class Circle2DIntersector implements IGeometryVisitor {
        private Circle2D mCircle;
        Circle2DIntersector(Circle2D circle) {
            mCircle = circle;
        }
        public void visitCircle2D(Circle2D circle) {
            mResult = isOverlapping(mCircle, circle);
        }
        public void visitBox2D(Box2D box) {
            mResult = isOverlapping(mCircle, box);
        }
    }

    private class Box2DIntersector implements IGeometryVisitor {
        private Box2D mBox;
        Box2DIntersector(Box2D box) {
            mBox = box;
        }
        public void visitCircle2D(Circle2D circle) {
            mResult = isOverlapping(circle, mBox);
        }
        public void visitBox2D(Box2D box) {
            mResult = isOverlapping(mBox, box);
        }
    }

    // static methods to compute overlap of concrete types
    // For N concrete types there will be N*(N+1)/2 methods
    public static boolean isOverlapping(Circle2D circle1, Circle2D circle2) {
        return /* intersection of 2 circles */;
    }

    public static boolean isOverlapping(Circle2D circle, Box2D box) {
        return . . .;
    }

    public static boolean isOverlapping(Box2D box1, Box2D box2) {
        return . . .;
    }
}
like image 135
Ted Hopp Avatar answered Jan 10 '23 19:01

Ted Hopp


Welcome to the double dispatch land! The issue that you are seeing is a classic illustration of the shortcomings of languages with virtual dispatch. Ideally, you are looking for a function that is virtual with respect to more than one object, because the algorithm to determine if two shapes overlap or not depends on both shapes.

Your second code snippet (with multiple concrete classes) is a start toward one common solution to the double dispatch problem, known as the visitor pattern. It works better than a chain of if-then-elses, but it has a couple of shortcomings:

  • Every time you add a new shape, all shapes must be extended with a method to check the overlap with the newly added shape
  • It is not clear where to look for the definitive algorithm of, say, Rectangle2D overlapping Circle2D - in Rectangle2D's IsOverlapping(Circle2D), or in Circle2D's IsOverlapping(Rectangle2D)

One common solution is to introduce type IDs, and make a 2D array of delegates that process overlaps of geometric shapes. This suffers from the first problem of the visitor, but fixes the second by centralizing the decision making.

like image 35
Sergey Kalinichenko Avatar answered Jan 10 '23 19:01

Sergey Kalinichenko