简体   繁体   中英

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

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.

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 . . .;
    }
}

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 - else s, 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.

What I would do:

public interface IGeometry
{
    bool IsOverlapping(IGeometry geometry);
}

public class Circle2D : IGeometry
{
    public bool IsOverlapping(IGeometry geometry)
    {
        dynamic dyn = geometry;
        return Overlapper.Overlap(this, dyn);
    }
}

public class Box2D : IGeometry
{
    public bool IsOverlapping(IGeometry geometry)
    {
        dynamic dyn = geometry;
        return Overlapper.Overlap(this, dyn);
    }
}

public static class Overlapper
{
    public static bool Overlap(Box2D box1, Box2D box2)
    {
        // logic goes here
    }

    public static bool Overlap(Box2D box1, Circle2D circle1)
    {
        // logic goes here
    }

    public static bool Overlap(Circle2D circle1, Box2D box1)
    {
        return Overlap(box1, circle1); // No need to rewrite it twice
    }

    public static bool Overlap(Circle2D circle1, Circle2D circle2)
    { 
        // logic goes here
    }
}

God, my answer is stupid. In this case, you wouldn't need to call the other object anyway, you could just send the pair directly to the static class. Anyway... My guess is that there isn't an impressively easy way to do it.

I don't think that you can implemented generic logic to determine if two shapes are overlapping, so I would suggest overloading isOverlapping with all the types.

If you do use the abstract type as an argument then you will still need to check the concrete type in question and perform the relevant maths. The problem here is that the solution is less explicit - you could pass in a concrete GeometricObject2D type which has no implementation in isOverlapping . Then what? Throwing an exception is not great here because your isOverlapping(GeometricObject2D o) call is technically welcomed by definition. It defeats the point of OOP to say "We accept almost allGeometricObject2D types!".

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM