首页 > 解决方案 > Constraining generic types by extrinsic functionality

问题描述

Background:

I am working with an organization that has an ever-growing collection of data types that they need to extract data from. I have no ability to change these data types. Some are machine-generated from XML files provided by other organizations; some are controlled by intransigent internal dev teams; and some are so old that no one is willing to change them in any way for fear that it will destabilize the entire Earth and cause it to crash into the sun. These classes don't share any common interface, and don't derive from any common type other than object. A few sample classes are given below for illustration purposes:

    public class Untouchable
    {
        public string Data;
    }

    public class Unchangeable
    {
        public int Info;
    }

The good news is that most of the time, I can use canned functionality to get at least some of the data from instances of the various classes. Unfortunately, most of these classes also have weird and specialized data that needs class-specific logic to extract data from. Also, information often needs to persist inside of the data extractors because the data objects I'm pulling data from have "interactions" (don't ask).

I have created an abstract generic Extractor<T> class to serve as a repository of common methodology, and an IExtractor<T> interface to serve as a convenient handle to access functionality. I also have a few specific (de-generic?) implementations of this class that can extract information from the business objects built from some of the data types. Here's some sample code to illustrate:

    public interface IExtractor<T>
    {
        string ExtractionOne(T something);
        string ExtractionTwo(T something);
    }

    public abstract class Extractor<T> : IExtractor<T>
    {
        private string internalInfo; // Certain business logic requires us to keep internal info over multiple objects that we extract data from.
        protected Extractor() { internalInfo="stuff"; }

        public virtual string ExtractionOne(T something)
        {
            return "This manipulation is generally applicable to most conceivable types.";
        }

        public abstract string ExtractionTwo(T something); // This DEFINITELY needs to be overridden for each class T
    }

    public class UntouchableExtractor : Extractor<Untouchable>
    {
        public UntouchableExtractor() : base() { }

        public override string ExtractionTwo(Untouchable something)
        {
            return something.Data;
        }
    }

    public class UnchangeableExtractor : Extractor<Unchangeable>
    {
        public UnchangeableExtractor() : base() { }

        public override string ExtractionTwo(Unchangeable something)
        {
            return something.Info.ToString();
        }
    }

I don't yet support all of the available data types, but management wants to roll out the data extractor to end-users using a command-line interface. They're telling me that we should start extracting the data we can extract, and get to the rest later. Support for the many unmodifiable types will be added by me and by and other programmers as time permits, and the end-users are expected to work around our latency. This actually makes sense in our real-world setting, so just go with it.

The Problem:

The list of data types that we want to pull information from is very large. Maintaining an explicit list of supported types in code will be tricky and error prone -- especially if we find any problems with specific data extractors and need to revoke support until we fix some bugs.

I would like to support the large and changing list of supported data types from a single entry point that dynamically determines the "right version" of IExtractor<> to use based on a passed in dynamic dataObject. If there is no class that implements the IExtractor<> to support the given dataObject, than an error should be thrown.

What Doesn't Work:

I tried taking a dynamic thing and using typeof(Extractor<>).MakeGenericType(thing.GetType()) to create an instance of Extractor<Untouchable> or Extractor<Unchangeable>, but those are abstract classes, so I can't use Activator.CreateInstance() to build an instance of those classes. The core issue with this approach is that it's unfortunately looking for a class of the form Extractor<> instead of an interface of the form IExtractor<>.

I considered putting extension methods like IExtractor<T> BuildExtractor(this T something) in some class, but I'm nervous about running into some business logic called BuildExtractor that already exists in one of these untouchable classes. This may be an unhealthy level of paranoia, but that's where I'm at.

Where I need help:

I'd welcome any suggestions for how I can create a single entrypoint for an unconstrained collection of classes. Thanks in advance.

标签: c#.netgenericsdynamicinterface

解决方案


结合 StackOverflow 中的一些代码和我自己的测试,我建议使用反射来查找实现接口的所有类型:

public static class TypeExt {
    public static bool IsBuiltin(this Type aType) => new[] { "/dotnet/shared/microsoft", "/windows/microsoft.net" }.Any(p => aType.Assembly.CodeBase.ToLowerInvariant().Contains(p));
    public static IEnumerable<Type> ImplementingTypes(this Type interfaceType, bool includeAbstractClasses = false, bool includeStructs = false, bool includeSystemTypes = false, bool includeInterfaces = false) =>
        AppDomain.CurrentDomain.GetAssemblies()
                               .SelectMany(a => a.GetLoadableTypes())
                               .Distinct()
                               .Where(aType => (includeAbstractClasses || !aType.IsAbstract) &&
                                               (includeInterfaces ? aType != interfaceType : !aType.IsInterface) &&
                                               (includeStructs || !aType.IsValueType) &&
                                               (includeSystemTypes || !aType.IsBuiltin()) &&
                                               interfaceType.IsAssignableFrom(aType) &&
                                               aType.GetInterfaces().Contains(interfaceType));
}

public static class AssemblyExt {
    //https://stackoverflow.com/a/29379834/2557128
    public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly) {
        if (assembly == null)
            throw new ArgumentNullException("assembly");
        try {
            return assembly.GetTypes();
        } catch (ReflectionTypeLoadException e) {
            return e.Types.Where(t => t != null);
        }
    }

}

反射可能很慢,在我的测试中,获取所有加载的类型是最慢的部分,所以我添加了加载类型和实现类型的缓存,但这确实意味着如果你动态加载程序集,你将需要刷新加载的类型:

public static class TypeExt {
    public static bool IsBuiltin(this Type aType) => new[] { "/dotnet/shared/microsoft", "/windows/microsoft.net" }.Any(p => aType.Assembly.CodeBase.ToLowerInvariant().Contains(p));

    static Dictionary<Type, HashSet<Type>> FoundTypes = null;
    static List<Type> LoadableTypes = null;

    public static void RefreshLoadableTypes() {
        LoadableTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetLoadableTypes()).ToList();
        FoundTypes = new Dictionary<Type, HashSet<Type>>();
    }

    public static IEnumerable<Type> ImplementingTypes(this Type interfaceType, bool includeAbstractClasses = false, bool includeStructs = false, bool includeSystemTypes = false, bool includeInterfaces = false) {
        if (FoundTypes != null && FoundTypes.TryGetValue(interfaceType, out var ft))
            return ft;
        else {
            if (LoadableTypes == null)
                RefreshLoadableTypes();

            var ans = LoadableTypes
                       .Where(aType => (includeAbstractClasses || !aType.IsAbstract) &&
                                       (includeInterfaces ? aType != interfaceType : !aType.IsInterface) &&
                                       (includeStructs || !aType.IsValueType) &&
                                       (includeSystemTypes || !aType.IsBuiltin()) &&
                                       interfaceType.IsAssignableFrom(aType) &&
                                       aType.GetInterfaces().Contains(interfaceType))
                       .ToHashSet();

            FoundTypes[interfaceType] = ans;

            return ans;
        }
    }
}

public static class AssemblyExt {
    //https://stackoverflow.com/a/29379834/2557128
    public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly) {
        if (assembly == null)
            throw new ArgumentNullException("assembly");
        try {
            return assembly.GetTypes();
        }
        catch (ReflectionTypeLoadException e) {
            return e.Types.Where(t => t != null);
        }
    }
}

一旦你有了其中一个,你就可以创建一个采用动态对象的工厂方法:

public static class ImplementingFactory {
    public static Type ExtractorType(dynamic anObject) {
        Type oType = anObject.GetType();
        var iType = typeof(IExtractor<>).MakeGenericType(oType);
        var ans = iType.ImplementingTypes().FirstOrDefault();
        if (ans == null)
            throw new Exception($"Unable to find IExtractor<{oType.Name}>");
        else
            return ans;
    }
}

推荐阅读