动态依赖注入

本文关键字:注入 依赖 动态 | 更新日期: 2023-09-27 18:35:55

第二种方法

我有一系列应用程序,它们提供了一组可扩展(即非固定)的变量,可供各种插件使用。

例如:

  1. 日志事件源
  2. 计算结果的来源
  3. 系统资源使用的来源
  4. 绩效指标的来源

插件可以使用这些的任意组合。

示例插件可以是:

  • 自定义错误记录器,使用 1。
  • 自定义统计模块,使用 2.
  • 使用 3. 和 4 的高性能工具。

我想要实现的是

  • 显示一个插件列表,给定此应用程序中存在的一组变量,可以使用这些插件(当没有日志事件源时,您应该无法选择自定义错误记录器)。
  • 获得一种简单安全的方法将变量传递给插件,这样就不会因缺少变量而导致运行时错误。

一个好处是允许插件选择性地需要一个变量,例如,一个需要 4. 的插件,并且可以选择使用 3. 如果可用(但除此之外也可用)。

第一种方法

我想实现某种"动态依赖注入"。让我用一个用例来解释它。

我正在构建一组将用于一系列应用程序的库。每个应用程序都可以提供一组不同的变量,这些变量可供需要这些变量的某些"处理程序"使用。根据具体的可用变量,必须确定可用处理程序的数量,因为只有在处理程序有权访问所有必需的变量时才能使用处理程序。我也在寻找一种方法使调用尽可能安全。编译时可能是不可能的,但"检查一次,之后永远不会失败"就可以了。

下面是第一个草图。在这个阶段,一切仍然可以改变。

class DynamicDependencyInjectionTest
{
    private ISomeAlwaysPresentClass a;
    private ISomeOptionalClass optionA;
    private ISomeOtherOptionalClass optionB;
    private ISomeMultipleOption[] multi;
    private IDependentFunction dependentFunction;
    void InvokeDependency()
    {
        // the number of available dependencies varies.
        // some could be guaranteed, others are optional, some maybe have several instances
        var availableDependencies = new IDependencyBase[] {a, optionA, optionB}.Concat(multi).ToArray();
        //var availableDependencies = new IDependencyBase[] { a  };
        //var availableDependencies = new IDependencyBase[] { a, optionA }.ToArray();
        //var availableDependencies = new IDependencyBase[] { a, optionB }.ToArray();
        //var availableDependencies = new IDependencyBase[] { a , multi.First() };
        //ToDo
        // this is what I want to do
        // since we checked it before, this must always succeed
        somehowInvoke(dependentFunction, availableDependencies);
    }
    void SetDependentFunction(IDependentFunction dependentFunction)
    {
        if (! WeCanUseThisDependentFunction(dependentFunction))
            throw new ArgumentException();
        this.dependentFunction = dependentFunction;
    }
    private bool WeCanUseThisDependentFunction(IDependentFunction dependentFunction)
    {
        //ToDo
        //check if we can fulfill the requested dependencies
        return true;
    }

    /// <summary>
    /// Provide a list which can be used by the user (e.g. selected from a combobox)
    /// </summary>
    IDependentFunction[] AllDependentFunctionsAvailableForThisApplication()
    {
        IDependentFunction[] allDependentFunctions = GetAllDependentFunctionsViaReflection();
        return allDependentFunctions.Where(WeCanUseThisDependentFunction).ToArray();
    }
    /// <summary>
    /// Returns all possible candidates
    /// </summary>
    private IDependentFunction[] GetAllDependentFunctionsViaReflection()
    {
        var types = Assembly.GetEntryAssembly()
            .GetTypes()
            .Where(t => t.IsClass && typeof (IDependentFunction).IsAssignableFrom(t))
            .ToArray();
        var instances = types.Select(t => Activator.CreateInstance(t) as IDependentFunction).ToArray();
        return instances;
    }

    private void somehowInvoke(IDependentFunction dependentFunction, IDependencyBase[] availableDependencies)
    {
        //ToDo
    }
}
// the interfaces may of course by changed!
/// <summary>
/// Requires a default constructor
/// </summary>
interface IDependentFunction
{
    void Invoke(ISomeAlwaysPresentClass a, IDependencyBase[] dependencies);
    Type[] RequiredDependencies { get; }
}
interface IDependencyBase { }
interface ISomeAlwaysPresentClass : IDependencyBase { }
interface ISomeOptionalClass : IDependencyBase { }
interface ISomeOtherOptionalClass : IDependencyBase { }
interface ISomeMultipleOption : IDependencyBase { }

class BasicDependentFunction : IDependentFunction
{
    public void Invoke(ISomeAlwaysPresentClass a, IDependencyBase[] dependencies)
    {
        ;
    }
    public Type[] RequiredDependencies
    {
        get { return new[] {typeof(ISomeAlwaysPresentClass)}; }
    }
}
class AdvancedDependentFunction : IDependentFunction
{
    public void Invoke(ISomeAlwaysPresentClass a, IDependencyBase[] dependencies)
    {
        ;
    }
    public Type[] RequiredDependencies
    {
        get { return new[] { typeof(ISomeAlwaysPresentClass), typeof(ISomeOptionalClass) }; }
    }
}
class MaximalDependentFunction : IDependentFunction
{
    public void Invoke(ISomeAlwaysPresentClass a, IDependencyBase[] dependencies)
    {
        ;
    }
    public Type[] RequiredDependencies
    {
        // note the array in the type of ISomeMultipleOption[]
        get { return new[] { typeof(ISomeAlwaysPresentClass), typeof(ISomeOptionalClass), typeof(ISomeOtherOptionalClass), typeof(ISomeMultipleOption[]) }; }
    }
}

动态依赖注入

保持简单。让插件依赖于构造函数注入,它的优点是构造函数静态宣布每个类的依赖项。然后使用"反射"来确定您可以创建的内容。

例如,假设您有三个服务:

public interface IFoo { }
public interface IBar { }
public interface IBaz { }

此外,假设存在三个插件:

public class Plugin1
{
    public readonly IFoo Foo;
    public Plugin1(IFoo foo)
    {
        this.Foo = foo;
    }
}
public class Plugin2
{
    public readonly IBar Bar;
    public readonly IBaz Baz;
    public Plugin2(IBar bar, IBaz baz)
    {
        this.Bar = bar;
        this.Baz = baz;
    }
}
public class Plugin3
{
    public readonly IBar Bar;
    public readonly IBaz Baz;
    public Plugin3(IBar bar)
    {
        this.Bar = bar;
    }
    public Plugin3(IBar bar, IBaz baz)
    {
        this.Bar = bar; ;
        this.Baz = baz;
    }
}

很明显,Plugin1需要IFoo,而Plugin2需要IBarIBaz。第三个类 Plugin3 更特殊一些,因为它具有可选的依赖项。虽然它需要IBar,但如果可用,它也可以使用IBaz

您可以定义一个 Composer,它使用一些基本反射来检查是否可以根据可用服务创建各种插件的实例:

public class Composer
{
    public readonly ISet<Type> services;
    public Composer(ISet<Type> services)
    {
        this.services = services;
    }
    public Composer(params Type[] services) :
        this(new HashSet<Type>(services))
    {
    }
    public IEnumerable<Type> GetAvailableClients(params Type[] candidates)
    {
        return candidates.Where(CanCreate);
    }
    private bool CanCreate(Type t)
    {
        return t.GetConstructors().Any(CanCreate);
    }
    private bool CanCreate(ConstructorInfo ctor)
    {
        return ctor.GetParameters().All(p => 
            this.services.Contains(p.ParameterType));
    }
}

如您所见,您可以使用一组可用服务配置Composer实例,然后可以使用候选项列表调用 GetAvailableClients 方法以获取一系列可用插件。

您可以轻松扩展 Composer 类,以便能够创建所需插件的实例,而不是只告诉您哪些插件可用。

您可能会在某些 DI 容器中找到此功能。IIRC,Castle Windsor公开了一个Tester/Doer API,如果MEF也支持这样的功能,我不会感到惊讶。

以下 xUnit.net 参数测试表明上述Composer有效。

public class Tests
{
    [Theory, ClassData(typeof(TestCases))]
    public void AllServicesAreAvailable(
        Type[] availableServices,
        Type[] expected)
    {
        var composer = new Composer(availableServices);
        var actual = composer.GetAvailableClients(
            typeof(Plugin1), typeof(Plugin2), typeof(Plugin3));
        Assert.True(new HashSet<Type>(expected).SetEquals(actual));
    }
}
internal class TestCases : IEnumerable<Object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[] {
            new[] { typeof(IFoo), typeof(IBar), typeof(IBaz) },
            new[] { typeof(Plugin1), typeof(Plugin2), typeof(Plugin3) }
        };
        yield return new object[] {
            new[] { typeof(IBar), typeof(IBaz) },
            new[] { typeof(Plugin2), typeof(Plugin3) }
        };
        yield return new object[] {
            new[] { typeof(IFoo), typeof(IBaz) },
            new[] { typeof(Plugin1) }
        };
        yield return new object[] {
            new[] { typeof(IFoo), typeof(IBar) },
            new[] { typeof(Plugin1), typeof(Plugin3) }
        };
        yield return new object[] {
            new[] { typeof(IFoo) },
            new[] { typeof(Plugin1) }
        };
        yield return new object[] {
            new[] { typeof(IBar) },
            new[] { typeof(Plugin3) }
        };
        yield return new object[] {
            new[] { typeof(IBaz) },
            new Type[0]
        };
        yield return new object[] {
            new Type[0],
            new Type[0]
        };
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

经验教训:

  1. 最初我认为我需要每次调用方法注入,这很复杂,因为:

    • 实例化类之前,很难判断是否可以满足依赖项
    • 方法签名不是完全类型安全的,或者不是(非泛型)接口的一部分。
    • 尽管它可能是非标准的,因此不容易被现有系统支持
  2. 然后,我通过将所需的依赖项添加为属性来切换到一种属性注入,每个依赖项都绑定到一个接口,以便可以轻松发现所需的依赖项。调用本身是无参数的。

    • 这样可以更轻松地找出可以使用哪个插件。
    • "如果此插件实现了用于设置此依赖项属性的接口,请设置它"语句链可以在调用无参数方法之前填充依赖项。
    • 如果省略一个调用,则不会设置依赖项。
  3. 可能正确的方法是使用构造函数注入,因此可以使用标准工具。调用本身也是无参数的,因此它将非常适合接口。

下面是 Mark 解决方案的更完整版本,包括解析组件。它使用Castle.Windsor,xUnit,Shouldly和Resharper的NotNull, CanBeNull属性。

需要进一步的工作来消除对 Castle.Windsor 的直接依赖,方法是注入解析设施工厂(因为它必须接受来自主机的实例,我们不能直接传入解析器)。

public interface IFoo { }
public interface IBar { }
public interface IBaz { }
/// <summary>
/// Needed to invoke the plugin
/// </summary>
public interface IPlugin
{
    void Invoke();
}
public class Plugin1 : IPlugin
{
    public readonly IFoo Foo;
    public Plugin1([NotNull] IFoo foo)
    {
        if (foo == null) throw new ArgumentNullException("foo");
        this.Foo = foo;
    }
    public void Invoke()
    {
        ;
    }
}
public class Plugin2 : IPlugin
{
    public readonly IBar Bar;
    public readonly IBaz Baz;
    public Plugin2([NotNull] IBar bar, [NotNull] IBaz baz)
    {
        if (bar == null) throw new ArgumentNullException("bar");
        if (baz == null) throw new ArgumentNullException("baz");
        this.Bar = bar;
        this.Baz = baz;
    }
    public void Invoke()
    {
        ;
    }
}
public class Plugin3 : IPlugin
{
    public readonly IBar Bar;
    public readonly IBaz Baz;
    public Plugin3([NotNull] IBar bar, [CanBeNull] IBaz baz = null)
    {
        if (bar == null) throw new ArgumentNullException("bar");
        this.Bar = bar; ;
        this.Baz = baz;
    }
    public void Invoke()
    {
        ;
    }
}
public class Bar : IBar
{
}
public class SampleHostTest
{

    [Fact]
    void SampleHostCanResolvePlugin3ButNot1And2()
    {
        var bar = new Bar();
        var plugins = Assembly.GetAssembly(typeof(SampleHost))
                .GetTypes()
                .Where(t => t.IsClass && typeof(IPlugin).IsAssignableFrom(t))
                .ToArray();
        var sut = new SampleHost(bar, plugins);
        sut.IsPluginSupported(typeof(Plugin1)).ShouldBeFalse();
        sut.IsPluginSupported(typeof(Plugin2)).ShouldBeFalse();
        sut.IsPluginSupported(typeof(Plugin3)).ShouldBeTrue();
    }
    [Fact]
    void ResolvePlugin3()
    {
        var bar = new Bar();
        var plugins = Assembly.GetAssembly(typeof(SampleHost))
                .GetTypes()
                .Where(t => t.IsClass && typeof(IPlugin).IsAssignableFrom(t))
                .ToArray();
        var sut = new SampleHost(bar, plugins);
        sut.IsPluginSupported(typeof(Plugin3)).ShouldBeTrue();
        sut.CreateAndInvokePlugin(typeof(Plugin3));
        // no exception => succeeded
    }

}
public class SampleHost
{
    private readonly IBar bar;
    private readonly IWindsorContainer container;
    private Type[] plugins;
    public SampleHost(IBar bar, IEnumerable<Type> plugins)
    {
        this.bar = bar;
        this.plugins = plugins.ToArray();
        this.container = new WindsorContainer();
        container.Register(Component.For<IBar>().Instance(this.bar));
        foreach (var plugin in this.plugins)
        {
            container.Register(Component.For(plugin).ImplementedBy(plugin).LifestyleTransient());
        }
    }
    public bool IsPluginSupported(Type type)
    {
        var result = container.Kernel.HasComponent(type) &&
                     container.Kernel.GetHandler(type).CurrentState == HandlerState.Valid;
        return result;
    }
    public void CreateAndInvokePlugin(Type type)
    {
        Assert.True(IsPluginSupported(type));
        var plugin = container.Resolve(type)as IPlugin;
        Debug.Assert(plugin != null, "plugin != null");
        plugin.Invoke();
    }

}