2016-02-24 6 views
3

Второго подхода

У меня есть семейство приложений, которые предлагают расширяемые (т.е. не фиксировано) множество переменных, которые могут быть использованы различными модулями.инъекция Динамической зависимости

Примеров являются:

  1. источника для журнала событий
  2. Источника вычисления результатов
  3. источника для использования системных ресурсов
  4. источника для показателей Performace
  5. ...

Плагины могут использовать любую комбинацию как таковые.

Примеры плагинов могут быть:

  • Регистратор пользовательских ошибок, используя 1.
  • A модуль пользовательских статистики, используя 2.
  • Инструмент Performace используя 3. и 4.

Чего я хочу достичь:

  • presen t список плагинов, которые можно использовать, учитывая набор переменных, присутствующих в этом приложении (при отсутствии источника событий журнала вы не сможете выбрать собственный журнал ошибок).
  • получить простой и безопасный способ передачи переменных в плагины, чтобы избежать ошибок времени выполнения из-за отсутствия переменных.

Бонус будет заключаться в том, чтобы позволить плагинам необязательно требовать переменную, например. плагин, который требует 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[]) }; } 
    } 
} 
+0

Как следует из ответа Марка, вы должны уважать принцип KISS и не пытаться изобретать колесо с каким-то неясным DI. Подумайте о тех, кто прочитает ваш код. Даже вы через несколько месяцев можете получить головные боли, чтобы понять, что вы намереваетесь в первую очередь ... – Fab

ответ

2

Храните его простым. Пусть плагины полагаются на Конструктор Injection, который имеет то преимущество, что конструкторы статически объявляют зависимости каждого класса. Затем используйте Reflection, чтобы выяснить, что вы можете создать.

Предположим, например, что у вас есть три услуги:

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 требует IBar и IBaz. Третий класс, 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)); 
    } 
} 

Как вы можете видеть, настроенными a Composer экземпляр с набором доступных Сервисов, и вы можете затем вызвать метод GetAvailableClients со списком кандидатов, чтобы получить последовательность доступных плагинов.

Вы можете легко расширить класс Composer, чтобы также могли создавать экземпляры желаемых плагинов, а не только сообщать вам, какие из них доступны.

Возможно, эта функция доступна уже в некоторых контейнерах DI. IIRC, Castle Windsor предоставляет Tester/Doer API, и я не удивлюсь, если MEF также поддерживает такую ​​функцию.

Следующий xUnit.net Parametrised Test демонстрирует, что работает вышеописанный 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(); 
    } 
} 
+0

Спасибо за подробный ответ и отличную книгу. Тем временем я нашел решение, зависящее от какой-то инъекции свойств, но в этом случае превосходство конструктора является превосходным. «Композитор» помогает мне понять, какой плагин можно использовать, но мне все равно нужно выполнить фактическое создание. Могу ли я достичь этого, используя стандартный контейнер DI и используя - остаться с Castle Windsor - зарегистрировав 'IFoo' с помощью' Component.For () .Instance (myClass.instanceOfIFoo) '? И если я это сделаю, могу ли я также заменить композитор на функцию «тест» «Виндзорский замок»? – Onur

+0

@Onur Было бы разумно использовать существующую библиотеку, например Castle Windsor, для реализации функций «CanCreate» и «Create»; В основном я добавил класс «Композитор» к моему ответу, чтобы продемонстрировать принцип. Пока контейнер DI, такой как Castle Windsor, может ответить на вопрос *, можете ли вы создать объект такого типа? * Вы должны иметь возможность реализовать желаемую функциональность. Прошло некоторое время с тех пор, как я серьезно использовал любой контейнер DI, поэтому я не могу запомнить детали API, но IIRC несколько контейнеров DI могут ответить на такой вопрос. –

+0

Мне удалось запустить прототип с Castle.Windsor на основе ваших идей (см. Мой ответ). – Onur

0

уроки:

  1. Первоначально я думал, что нужно за вызов инъекции метод, который является сложным, потому что:

    • Труднее сказать, если зависимости могут быть выполнены до создание экземпляра класса
    • Подпись метода не является полностью типичной или не является частью (неродного) интерфейса.
    • Хотя возможно это нестандартное и, таким образом, легко не поддерживается существующих системами
  2. Потом я переключился на своего род инъекцию собственности путем добавления требуемых зависимостей в качестве свойств, каждый привязан к интерфейсу, так что требуемое зависимости могут быть легко обнаружены. Сам вызов был без параметров.

    • Это облегчало определение того, какой плагин можно использовать.
    • Цепочка «если этот плагин реализует интерфейс для установки свойства этой зависимости, установите его». Операторы могут заполнять зависимости перед вызовом метода без параметров.
    • Если один вызов был опущен, зависимость не была бы установлена.
  3. Возможно, правильным способом является использование инъекции конструктора, поэтому можно использовать стандартные инструменты. Сам вызов также без параметров, поэтому он прекрасно вписывается в интерфейс.

Ниже приведена более полная версия решения Марка, включая разрешение компонента. Он использует атрибуты 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(); 
    } 


} 
Смежные вопросы