Measuring Type Distance

Imagine this: you are building an API that uses conventions. A convention applies to a type. There can be several conventions registered, but the API needs to figure out what convention applies to any given class. The problem is, you can have conventions associated with base classes, concrete classes or even interfaces. For example, consider these:

public interface IInterface
{
  //something
}
 
public abstract class Base : IInterface
{
  //something
}
 
public class Derived : Base
{
  //something
}

You register one convention for each of these types:

 
AddConvention<Base>(new SomeConvention());
 
AddConvention<Derived>(new AnotherConvention());
 
AddConvention<IInterface>(new YetAnotherConvention());
 
AddConvention<object>(new EvenAnotherConvention());

And, at some point, you have this:

var convention = FindConventionFor(someObject);

If someObject is of type Derived, AnotherConvention should be returned; if it is of any other class inheriting from Base, it should be SomeConvention; and if it is neither but happens implement IInterface, then the result should be YetAnotherConvention. For any other case, the convention would be EvenAnotherConvention. Of course, the FindConventionFor method should always return the most specific convention that applies to that class.

Another option would be to return all conventions, ordered from more generic to more specific:

var conventions = FindConventionsFor(someObject);

In this case, it would return [ YetAnotherConvention, EvenAnotherConvention, SomeConvention, AnotherConvention ]. Why do we want to order it like this? Well, so that we can apply conventions, starting from the most generic and ending on the most specific.

And now the question is: how do we order the types? Let’s say we have a target type, which is the type of someObject. We can write a method like this one:

public static int Distance(this Type source, Type target)
{
    if (target.IsAssignableFrom(source) == false)
    {
        return int.MaxValue;
    }
 
    if (source.GetTypeInfo().IsInterface == true)
    {
        return int.MaxValue / 2;
    }
 
    var current = source;
    var distance = 0;
 
    while (current != typeof(object))
    {
        if (target == current)
        {
            break;
        }
 
        current = current.GetTypeInfo().BaseType;
        distance++;
    }
 
    return distance;
}

Basically, if the types are related, add 1 per each inheritance level, and interface inheritance yields int.MaxValue / 2; it could be any value big enough to account for deep inheritance, or I could take into account the inheritance level in which it is being implemented – not what I’m doing. Whenever we receive int.MaxValue, we know for sure that the types are not related between themselves.

Using it, we get the following results:

var someObject = new Derived();
var target = someObject.GetType();
 
var d1 = target.Distance(typeof(Derived));       //0
var d2 = target.Distance(typeof(Base));          //1
var d3 = target.Distance(typeof(object));        //2
var d4 = target.Distance(typeof(IInterface));    //int.MaxValue / 2
var d5 = target.Distance(typeof(string));        //int.MaxValue
var d6 = target.Distance(typeof(int));           //int.MaxValue
 
 

And if you want to find all conventions in distance order:

var conventions = new Dictionary<Type, Convention>();
 
var conventions = (from c in conventions
                let d = Distance(c.Key, target)
                where d < int.MaxValue
                orderby d
                select c.Value);

Have you found yourselves in this situation? What do you think of this solution? Let me hear your thoughts!

                             

2 Comments

Add a Comment

As it will appear on the website

Not displayed

Your website