Каковы варианты получения обычно внедренного регистратора в класс, который создается вне контейнера DI?

1

Я знаю, что Service Locator картина немилость. Когда у вас есть что-то вроде Global.Kernel.Get<IService>() в вашем коде, консультант обнаружит запах кода. Поэтому, где это возможно, мы должны использовать конструктор/свойство или метод инъекции.

Теперь рассмотрим общую задачу регистрации в Ninject. Существует несколько расширений, таких как ninject.extensions.logging и Ninject.Extensions.Logging.Log4net, которые "просто работают". Вы ссылаетесь на них в своем проекте, а затем всякий раз, когда вы ILogger вы получаете завернутый экземпляр log4net logger, названный в честь типа, в который вы вводите. У вас даже нет зависимости от log4net в любом месте вашего кода, как раз там, где вы предоставляете ему конфигурацию. Большой!

Но что, если вам нужен логгер в классе, который не создан в контейнере DI? Как вы его получите? Он не может быть введен автоматически, потому что класс создается вне DI, вы можете делать Global.Kernel.Inject(myObject), потому что было бы неправильно полагаться на DI не в корне композиции, как описано в статье, приведенной выше, так какие у вас варианты?

Ниже моя попытка, но мне это не нравится, потому что он чувствует себя очень неловко. Я фактически ввожу ILoggerFactory вместо ILogger в класс, который создает объект вне объекта DI, а затем передает ILoggerFactory объекту вне DI в конструкторе.

Другим способом достижения такого же результата было бы введение зависимости log4net в класс вне класса DI, но это также ощущается назад.

Как бы вы решили это?

И вот пример кода:

using System.Threading;
using Ninject;
using Ninject.Extensions.Logging;

namespace NjTest
{
    // This is some application configuration information
    class Config
    {
        public string Setting1 { get; set; }
    }

    // This represent messages the application receives
    class Message
    {
        public string Param1 { get; set; }
        public string Param2 { get; set; }
        public string Param3 { get; set; }
    }

    // This class represents a worker that collects and process information
    // in our example this information comes partially from the message and partially
    // driven by configuration
    class UnitCollector
    {
        private readonly ILogger _logger;
        // This field is not present in the actual class it's
        // here just to give some exit condition to the ProcessUnit method
        private int _defunct;
        public UnitCollector(string param1, string param2, ILoggerFactory factory)
        {
            _logger = factory.GetCurrentClassLogger();
            // param1 and param2 are used during processing but for simplicity we are
            // not showing this
            _logger.Debug("Creating UnitCollector {0},{1}", param1, param2);
        }

        public bool ProcessUnit(string data)
        {
            _logger.Debug("ProcessUnit {0}",_defunct);
            // In reality the result depends on the prior messages content
            // For simplicity we have a simple counter here
            return _defunct++ < 10;
        }
    }

    // This is the main application service
    class Service
    {
        private readonly ILogger _logger;
        private readonly Config _config;

        // This is here only so that it can be passed to UnitCollector
        // and this is the crux of my question. Having both
        // _logger and _loggerFactory seems redundant
        private readonly ILoggerFactory _loggerFactory;

        public Service(ILogger logger, ILoggerFactory loggerFactory, Config config)
        {
            _logger = logger;
            _loggerFactory = loggerFactory;
            _config = config;
        }

        //Main processing loop
        public void DoStuff()
        {
            _logger.Debug("Hello World!");
            UnitCollector currentCollector = null;
            bool lastResult = false;
            while (true)
            {
                Message message = ReceiveMessage();
                if (!lastResult)
                {
                    // UnitCollector is not injected because it created and destroyed unknown number of times
                    // based on the message content. Still it needs a logger. I'm here passing the logger factory
                    // so that it can obtain a logger but it does not feel clean
                    // Another way would be to do kernel.Get<ILogger>() inside the collector
                    // but that would be wrong because kernel should be only used at composition root
                    // as per http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/
                    currentCollector = new UnitCollector(message.Param2, _config.Setting1,_loggerFactory);
                }
                lastResult = currentCollector.ProcessUnit(message.Param1);

                //message.Param3 is also used for something else
            }
        }
        private Message ReceiveMessage()
        {          
            _logger.Debug("Waiting for a message");
            // This represents receiving a message from somewhere
            Thread.Sleep(1000);
            return new Message {Param1 = "a",Param2 = "b",Param3 = "c"};
        }
    }

    class Program
    {
        static void Main()
        {
            log4net.Config.XmlConfigurator.Configure();
            IKernel kernel = new StandardKernel();
            kernel.Bind<Config>().ToMethod(ctx => new Config {Setting1 = "hey"});
            Service service = kernel.Get<Service>();
            service.DoStuff();

        }
    }
}

App.Config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net, Version=1.2.13.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a" />
  </configSections>
  <startup> 
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <log4net>
    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
      </layout>
    </appender>
    <root>
      <level value="ALL" />
      <appender-ref ref="ConsoleAppender" />
    </root>
  </log4net>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-1.2.13.0" newVersion="1.2.13.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>

</configuration>
  • 0
    Для меня неясно, почему вы думаете, что не можете позволить контейнеру DI вводить объекты, которые он сам не создал. Зарегистрировать делегатов вполне нормально. Так что вы можете уточнить вашу конкретную ситуацию и объяснить, почему вы не можете сделать это?
  • 0
    @steven извините, не уверен, что вы подразумеваете под "вполне нормально регистрировать делегатов". Инъекция происходит либо как часть создания объекта, либо путем явного вызова kernel.inject. Я объяснил, почему ни один из них не работает для меня. Мы также обнаружили третий вариант, как описано в ответе BatteryBackupUnit. Если вы имеете в виду еще один способ, пожалуйста, объясните, что вы имеете в виду.
Теги:
dependency-injection
logging
ninject

2 ответа

2
Лучший ответ

Вы должны посмотреть на ninject.extensions.factory. Вы можете создать фабрику интерфейса с соответствующей привязкой:

internal interface IUnitCollectorFactory
{
    IUnitCollector Create(Param2Type param2);
}

Bind<IUnitCollectorFactory>().ToFactory();

или вводить и использовать Func-Factory Func<Param2Type>.

Таким образом, IUnitCollectorFactory будет создаваться с помощью IoC, и вы можете также ввести в него Config а не передавать параметр.

Альтернативой было бы написать свою собственную фабрику, которая делает new() -ing, но я лично считаю, что неустойчивый код часто не стоит этого. Вы пишете component- или спецификации-тесты, не так ли? :) Однако то, что Марк Симан в основном рекомендует (см. Невозможно объединить Factory/DI) - что вы видели, если бы вы прочитали все комментарии в блоге. Преимущество абстрактной фабрики заключается в том, что она приближает вас к истинному корню композиции, где вы как можно больше создаете экземпляр "при запуске приложения", вместо того чтобы отложить процесс создания частей приложения до более позднего времени. Недостатком позднего создания является то, что запуск программы не укажет, может ли быть создано все дерево объектов - могут быть проблемы с привязкой, которые возникают только позже.

Другой альтернативой было бы настроить ваш дизайн таким образом, чтобы IUnitCollector был без гражданства - iE не зависит от получения параметра Param2 как параметра ctor, а скорее как параметра метода. Тем не менее, это может иметь другие, более значительные недостатки, поэтому вы можете сохранить свой дизайн как есть.

Чтобы применить использование Func-Factory к вашему примеру, измените начало класса Service следующим образом:

private readonly Func<string, string, UnitCollector> _unitCollertorFactory;        
public Service(ILogger logger, Config config, Func<string, string, UnitCollector> unitCollertorFactory)
{
    _logger = logger;
    _config = config;
    _unitCollertorFactory = unitCollertorFactory;
}

Обратите внимание, что вам больше не нужно _loggerFactory поля и ILoggerFactory параметра конструктора. Затем, вместо того, чтобы UnitCollector, UnitCollector его следующим образом:

currentCollector = _unitCollertorFactory(message.Param2, _config.Setting1);

Теперь вы можете заменить ILoggerFactory в UnitCollector конструктор ILogger и он будет получать счастливо впрыскивается.

И не забывайте, что для этого вам нужно обратиться к ninject.extensions.factory.

  • 0
    Спасибо, что нашли время, чтобы прочитать и понять вопрос. Похоже, это именно то, что мне нужно, однако мне понадобится еще немного времени, чтобы переварить, прежде чем я приму ответ. Могу ли я отредактировать ваш ответ, добавив фрагменты кода, чтобы проиллюстрировать, чем я закончил?
  • 0
    Конечно, давай. Мне бы этого хотелось, но я немного сомневаюсь, одобрят ли рецензенты правку? ...
Показать ещё 2 комментария
1

Ваш конкретный случай подробно объясняется здесь в документации по контекстному связыванию Ninject. Вы можете использовать следующую регистрацию:

Bind<ILog>().ToMethod(context => LogFactory.CreateLog(context.Request.Target.Type));

Это приведет к абстракции ILog основанной на типе, в который он вводится, что вам точно нужно.

Это позволяет сохранить вызов фабрики внутри вашего корня композиции и не позволяет вашему коду освободиться от каких-либо фабрик или злонамеренного сервиса.

ОБНОВИТЬ

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

Если вы перемещаете параметр параметра param1 из конструктора в метод, вы полностью решаете проблему, упрощаете конфигурацию и позволяете проверить конфигурацию при запуске. Это будет выглядеть следующим образом:

kernel.Bind<UnitCollector>().ToMethod(c => new UnitCollector(
    c.Get<Config>().Setting1,
    LogFactory.CreateLog(typeof(UnitCollector))));

class UnitCollector {
    private readonly string _param2;
    private readonly ILogger _logger;
    public UnitCollector(string param2, ILogger logger) {
        _param2 = param2;
        _logger = logger;
    }

    public bool ProcessUnit(string data, string param1) {
        // Perhaps even better to extract both parameters into a
        // Parameter Object: https://bit.ly/1AvQ6Yh
    }
}

class Service {
    private readonly ILogger _logger;
    private readonly Func<UnitCollector> _collectorFactory;

    public Service(ILogger logger, Func<UnitCollector> collectorFactory) {
        _logger = logger;
        _collectorFactory = collectorFactory;
    }

    public void DoStuff() {
        UnitCollector currentCollector = null;
        bool lastResult = false;
        while (true) {
            Message message = ReceiveMessage();
            if (!lastResult) {
                currentCollector = this.collectorFactory.Invoke();
            }
            lastResult = currentCollector.ProcessUnit(message.Param1, message.Param2);
        }
    }
}    

Обратите внимание, что в Service теперь есть фабрика (Func<T>) для UnitCollector. Поскольку все ваше приложение продолжает цикл в этом методе DoStuff, каждый цикл можно рассматривать как новый запрос. Поэтому при каждом запросе вам нужно будет выполнить новое решение (которое вы уже делаете, но теперь с ручным созданием). Кроме того, вам часто нужно создать некоторую область вокруг такого запроса, чтобы убедиться, что некоторые экземпляры, зарегистрированные с образцом "на запрос" или "по сфере", создаются только один раз. В этом случае вам придется извлечь эту логику из этого класса, чтобы предотвратить смешивание инфраструктуры с бизнес-логикой.

  • 0
    В моем примере это не будет работать для UnitCollector, потому что UnitCollector обновляется и не создается контейнером DI. Если вы полагаете, что ваш пример все еще полезен в моем случае, не могли бы вы объяснить, как экземпляр ILogger сделал бы это с параметром конструктора UnitCollector.
  • 0
    Ааа, я вижу, посмотреть мое обновление.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню