Я знаю, что 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>
Вы должны посмотреть на 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.
Ваш конкретный случай подробно объясняется здесь в документации по контекстному связыванию 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
, каждый цикл можно рассматривать как новый запрос. Поэтому при каждом запросе вам нужно будет выполнить новое решение (которое вы уже делаете, но теперь с ручным созданием). Кроме того, вам часто нужно создать некоторую область вокруг такого запроса, чтобы убедиться, что некоторые экземпляры, зарегистрированные с образцом "на запрос" или "по сфере", создаются только один раз. В этом случае вам придется извлечь эту логику из этого класса, чтобы предотвратить смешивание инфраструктуры с бизнес-логикой.