-1

I have a calculation program which uses a number of different vertex types. Currently I have euclidean and geo, and I have decided to create an interface. Now I am wondering if that is a good idea.

interface Vertex: Comparable<Vertex> {
    var id: Int

    /** returns then distance to another vertex. Vertex is expected to be of exact same type */
    fun distanceTo(vertex: Vertex): Double

    override fun compareTo(other: Vertex): Int {
        return id.compareTo(other.id)
    }
}

class Vertex2d(override var id: Int, val x: Double, val y: Double) : Vertex {
    override fun distanceTo(vertex: Vertex): Double {
        vertex as Vertex2d
        val distX: Double = x - vertex.x
        val distY: Double = y - vertex.y
        return (sqrt(distX * distX + distY * distY) * 100000.0).roundToInt() / 100000.0 // hypot takes 3 times as long
    }
}

class VertexGeo(override var id: Int, val latitudeRadians: Double, val longitudeRadians: Double) : Vertex {
    override fun distanceTo(vertex: Vertex): Double {
        vertex as VertexGeo
        // some complex calculation
        return distance
    }
}

The issue is, I always need a cast in distanceTo and this only work if the type is correct. I should actually check additionally isInstanceof, but no need in my progam, since I either work with only EUC or only GEO vertices.

Since the calculation is run billions of times (actually faster than storing the result in a memory table) performance is also an issue, but do not weigh is too much. Clean design is more a priority.

Instead I could do just one Vertex object with a type. Having the same signature I can use factory methods, but that is a minor detail.

enum class VertexType { EUC, GEO }

/**
 * @param x: x in EUC or longitude in radians
 */
class VertexInOne(var id: Int, val x: Double, val y: Double, val type: VertexType): Comparable<VertexInOne> {

    val latitudeRadians
        get() = y
    
    val longitudeRadians
        get() = x
    

    fun distanceTo(other: VertexInOne):Double {
        when (type) {
            VertexType.EUC -> {
                // calc distance
                return distance
            }
            VertexType.GEO -> {
                // calc distance
                return distance
            }
        }
    }

    override fun compareTo(other: VertexInOne): Int {
        return id.compareTo(other.id)
    }

    companion object {
        fun createVertexEuc(id: Int, x: Double, y: Double): VertexInOne {
            return VertexInOne(id, x, y, VertexType.EUC)
        }
        fun createVertexGeo(id: Int, latRadians: Double, longRadians: Double): VertexInOne {
            return VertexInOne(id, longRadians, latRadians, VertexType.GEO)
        }
    }
}

What would be your choice and why? Options are

  • Two different classes for 2d and Geo
  • Vertex as an abstract class
  • Vertex as an interface used by 2d and Geo
  • Vertex as a minimal class extended by 2d and Geo classes

I have been told, they are too different. The issue is, I have a lot of functionality that is irrelevant for the position, but distance matters.

For example a tour through a defined list of vertices. While the distanceTo calculation is very different, the use is identical. This is for me the core functionality of interfaces.

class Tour(val tourVertices: List<Vertex>) {

    fun tourLength(isRoundTrip: Boolean) {

        var length = 0.0
        for (i in 0 until tourVertices.size - 1) 
            length += tourVertices[i].distanceTo(tourVertices[i + 1])
        
        if (isRoundTrip)
            length += tourVertices.last().distanceTo(tourVertices.first())

    }

}
1
  • Since this is apparently a highly opinion bases question, I am deleting it soon. Thanks to Kain0_0 for your insights. Commented Dec 4, 2020 at 18:41

3 Answers 3

1

I think I have a solution your problem using Generics. Shouldn't impact performance at runtime:

interface Vertex<T : Vertex<T>> {
    fun distance(other: T): Double
}
class VertexGeo : Vertex<VertexGeo> {
    override fun distance(other: VertexGeo): Double {
        /* VertexGeo specific implementation */
    }
}
class Vertex2D : Vertex<Vertex2D> {
    override fun distance(other: Vertex2D): Double {
        /* Vertex2D specific implementation */
    }
}

VertexGeo().distance(VertexGeo())
VertexGeo().distance(Vertex2D()) //won't compile

Working with collections of Generics can be more cumbersome though. If you have mixed instances of VertexGeo and Vertex2D in the same collection, you will still need to cast.

listOf<Vertex<*>>(VertexGeo(), Vertex2D()).map { when(it) {
    is VertexGeo -> it.distance(VertexGeo())
    is Vertex2D -> it.distance(Vertex2D())
    else -> throw IllegalArgumentException()
} }
1
  • This is the Kotlin implementation to the solution @johnwu proposed. p.s. anonymous instantiations of Vertex aren't possible with this solution.
    – timj11dude
    Commented Jan 5, 2021 at 10:16
0

They are not the same.

The first co-ordinate system is for the Cartesian plane. A Flat surface. It has a number of interesting properties, particularly that:

  • two lines seperated by a line which forms right angles with both of them are parallel and will never intersect.

The second co-ordinate system is for a Spherical plane. eg the surface of a sphere. Among its interesting properties is that:

  • all lines if followed along the plane intersect.

So while yes they are both points in space, they are points in two very different spaces. I would be keeping their types and their implementations separate and even in different namespaces.


Secondly distance calculation is an aspect of the surface/volume the points are on/in.

The reasoning follows from the first point about Cartesian vs Spherical planes. The address may be composed of two vectors, but interpreting what the shortest vector between them is plane dependent.

  • In cartesian system its always (x2 - x1, y2 - y1)
  • In a spherical plane its not so simple, because all points have an infinite number of connecting straight lines. Which one did you want to return, the shortest? the Longest?

More interesting planes make it even more difficult to figure out.

9
  • True. While they are different, they still share some common features. Using distanceTo(vertex) I can use the same method on both vertex types. This will not work for a lot of methods, as they differ in many ways. But I would like to avoid duplicating the methods. Commented Dec 4, 2020 at 12:40
  • distanceTo(other vertex) is just the shortest distance assuming the earth is perfectly round with a diameter of 6378.388 km. But that calculation is not the point here. Commented Dec 4, 2020 at 12:50
  • And of course they are not the same. That is the reasoning for interfaces, is it not. Cat, Dog and Robot are very different, but could share a walkable interface. I could separate that, but I like x,y as values, because I still can use the same file load and save procedures. Commented Dec 4, 2020 at 12:53
  • 1
    LSP (Liskov Substitution Principal). True Cat, Dog, and Robot are different but can share a common PhysicalEntity interface, which might have move(coord) on it. In this case move(coord) is defined as moves to position coord, or throws because it can't. This works because regardless of implementation, the behaviour is respected in all cases, no surprises. Now Thought also has a move(coord) method. But it doesn't move through the same space as PhysicalEntity it moves through brain space. 1/2
    – Kain0_0
    Commented Dec 5, 2020 at 2:25
  • 1
    If they shared a common interface with move on it then the caller must be aware of the specific implementation to understand the behaviour in any realistic usage scenario. Because it must pass the right kind of co-ordinate into it. Similarly the implementation must be aware of more kinds of space than the one it is built for. Thought doesn't care about being on top of mount Everest, but it does care about being in my head. So when asked to move it would need to understand that it can't move to a physical space. This makes a leaky interface. And leaky interfaces are anti-modular. 2/2
    – Kain0_0
    Commented Dec 5, 2020 at 2:29
0

You actually have two questions.

  1. Do I use an interface?

  2. Do I use more than one class?

The former has to do with how you expect people to use your class(es). The latter has to do with the implementation details and how you decide what is common logic.

If you want to expose an interface that allows the caller to compute a distance, you could do it the same way the CLR does it with Equals and CompareTo:

interface IDistanceable 
{
    double DistanceTo(object other);
}

If you want to be type-safe you can use this sort of pattern (CRTP):

interface IDistanceable<T> where T: IDistanceable<T>
{
    double DistanceTo(T other);
}

The question whether to use a single class or more than one class depends on your implementation; you can use inheritance, composition, etc. the decision of which is up to you and is obviously a very broad question.

1
  • 1
    While this is C#, the concept is interesting. But does not work either. I cannot define a generic tour working with all types as it then requires the parameterized type: public double calcTourLength(List<DistanceAble<?>> vertices) {double d = 0; for (int i = 0; i < vertices.size() - 1; i++) {d += vertices.get(i).distanceTo(vertices.get(i + 1)); } return d;} Commented Dec 10, 2020 at 7:17

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