将正常注入的记录器获取到在DI容器外部创建的类的选项有哪些?



我知道服务定位器模式已经过时了。当您的代码中有类似Global.Kernel.Get<IService>()的东西时,顾问将检测到代码气味。所以在可能的情况下,我们应该使用构造函数/属性或方法注入。

现在让我们考虑一个使用Ninject进行日志记录的常见任务。有几个扩展,如ninject.extensions.logging和ninject.extensions.logging . log4net这"只是工作"。您在项目中引用它们,然后每当您注入ILogger时,您就会得到log4net记录器的包装实例,该实例以您注入的类型命名。在代码中的任何地方都不需要依赖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's 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();
        }
    }
}

应用程序。配置:

<?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>

您应该查看一下ninjec .extensions.factory。您可以创建具有相应绑定的接口工厂:

internal interface IUnitCollectorFactory
{
    IUnitCollector Create(Param2Type param2);
}
Bind<IUnitCollectorFactory>().ToFactory();

或注射和使用一个func_factory Func<Param2Type>

这样,IUnitCollectorFactory将由IoC实例化,您也可以将Config注入其中,而不是传递参数。

另一种选择是编写自己的工厂来执行new() -ing,但我个人认为不可测试的代码通常不值得这样做。您正在编写组件测试或规范测试,不是吗?:)然而,这正是Mark Seeman所推荐的(参见Can't combination of Factory/DI)——如果你阅读了博客文章的所有评论,你就会看到这一点。抽象工厂的优点是,它使您更接近真正的组合根,在该根中,您可以"在应用程序启动时"实例化尽可能多的内容,而不是将应用程序的部分实例化推迟到以后的某个时间。延迟实例化的缺点是,启动程序时不会告诉您整个对象树是否可以实例化——可能会出现绑定问题,这些问题只会在以后发生。

另一种选择是调整您的设计,使IUnitCollector是无状态的-即不依赖于获得Param2作为参数,而是作为方法参数。然而,这可能有其他更重要的缺点,因此您可以选择保持您的设计。

要将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,而是像这样实例化它:

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

现在您可以将UnitCollector构造函数中的ILoggerFactory替换为ILogger,它将被愉快地注入。

不要忘记,要使其工作,您需要引用ninject.extensions.factory.

在Ninject上下文绑定文档中清楚地解释了您的具体情况。您可以使用以下注册:

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

这将根据它被注入的类型注入一个ILog抽象,这正是你所需要的。

这允许你将工厂的调用保留在你的组合根中,并使你的代码免受任何工厂或邪恶的Service Location反模式的影响。

在仔细看了你所有的代码之后,我现在看到了导致这些问题的根本设计"缺陷"。根本问题是,在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中现在有一个UnitCollector的工厂(Func<T>)。由于整个应用程序一直在该DoStuff方法中循环,因此每个循环都可以视为一个新的请求。因此,对于每个请求,您都必须执行一个新的解析(您已经这样做了,但是现在使用手动创建)。此外,您经常需要围绕这样的请求创建一些作用域,以确保以"每个请求"或"每个作用域"的方式注册的一些实例只创建一次。在这种情况下,您将不得不从该类中提取逻辑,以防止基础设施与业务逻辑混合。

最新更新