jeudi 18 mai 2017

How to order classes by inheritence?

I'm using reflection to retrieve a list of available classes and I want to adapt the input object to the most specific class that matches; i.e. I want to do something like:

RawData input = new RawData() { ... };
Type targetAdapter = AdapterSelector.SelectAdapterFor(input);
AdapterSelector.TryGetAdaptMethod(targetAdapter, out Func<RawData, Adapter> fAdapt);
Adapter adapted = fAdapt(input);

Each applicable class has the following signature:

public class Adapter
{
    public static Adapter Adapt(RawData input) { ... }
    public static bool Matches(RawData input) { ... }
}

Retrieving the list of classes with the required methods can be done as follows using reflection1:

public partial class AdapterSelector
{
    public static IEnumerable<Type> GetApplicableAdapterTypes()
    {
        IEnumerable<Type> allTypes = typeof(AdapterSelector).Assembly.GetTypes();
        IEnumerable<Type> applicableTypes = from t in allTypes
                                            where TryGetAdaptMethod(t, _) &&
                                                  TryGetMatchMethod(t, _)
                                            select t;
        return applicableTypes;
    }

    private static bool TryGetAdaptMethod(Type type, out Func<RawData, Adapter> adapt)
    {
        MethodInfo adaptMethod = type.GetMethod("Adapt", BindingFlags.Static|BindingFlags.Public, null, new Type[] { typeof(RawData) }, null);
        if (adaptMethod != null)
        {
            adapt = (Func<RawData, Adapter>)Delegate.CreateDelegate(typeof(Func<RawData, Adapter>), null, adapterMethod);
            return true;
        }
        else
        {
            adapt = null;
            return false;
        }
    }

    private static bool TryGetMatchMethod(Type type, out Func<RawData, bool> match)
    {
        MethodInfo matchMethod = type.GetMethod("Match", BindingFlags.Static|BindingFlags.Public, null, new Type[] { typeof(RawData) }, null);
        if (matchMethod != null)
        {
            match = (Func<RawData, bool>)Delegate.CreateDelegate(typeof(Func<RawData, bool>), null, matchMethod);
            return true;
        }
        else
        {
            match = null;
            return false;
        }
    }
}

I assumed that I could use the following IComparer<Type> to order this list of adapters:

public class TypeHierarchyComparer: IComparer<Type>
{
    public int Compare(Type lhs, Type rhs)
    {
        if (rhs.IsSubclassOf(lhs))
        {
            return -1;
        }
        else if (lhs.IsSubclassOf(rhs))
        {
            return +1;
        }
        else
        {
            // Return arbitrary but fixed value for siblings, cousins, etc.
            return lhs.FullName.CompareTo(rhs.FullName);
        }
    }
}

Note that this is intended to be a partial ordering: if one type is a subclass of the other that type should be ordered after2 the latter. If neither type is a subclass of the other, the order is undefined.

With this ordering I hoped that the following hierarchy of adapters would be sorted correctly3:

  • Adapter
    • N
    • F
    • H
    • I
    • K
      • E
      • G
      • L
      • M
      • Q
      • R
    • O
    • B
    • C
    • J
    • P
      • AA
      • AB
      • D
      • S
      • T

However, the results of:

AdapterSelector.GetApplicableAdapterTypes().OrderBy((x) => x, new TypeHierarchyComparer());

are:

Adapter, AA, AB, B, C, D, N, F, H, I, J, K, P, E, G, L, M, O, P, Q, R, S, T

Is the correct order O and P should be ordered before AA, AB, D, S, and T and O should be before B, C, and J. It appears that subclasses are ordered after one of their parents but not necessarily after all their parents.

How can I sort classes by specificity? (I.e. ordering the subclasses before/after all their parents)


1) See Assembly.GetTypes(), Type.GetMethod(string, BindingFlags, Binder, Type[], ParameterModifier[]), Can you get a Func<T> (or similar) from a MethodInfo object?, and Delegate.CreateDelegate(Type, object, methodInfo)

2) I'm using Enumerable.OrderByDescending<TSource, TKey>(this IEnumerable<TSource>, Func<TSource, TKey>, IComparer<TKey>) to get the matcher to apply first in the front of the list.

3) The class hierarchy as shown by Visual Studio's Solution Explorer. I cannot disclose the names of the adapters, therefore I used AA to T, I preserved the alphabetical order of the names.





Aucun commentaire:

Enregistrer un commentaire