将正常注入的记录器获取到在DI容器外部创建的类的选项有哪些?
本文关键字:创建 外部 选项 DI 常注入 注入 获取 记录器 | 更新日期: 2023-09-27 18:18:39
我知道服务定位器模式已经过时了。当您的代码中有类似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
方法中循环,因此每个循环都可以视为一个新的请求。因此,对于每个请求,您都必须执行一个新的解析(您已经这样做了,但是现在使用手动创建)。此外,您经常需要围绕这样的请求创建一些作用域,以确保以"每个请求"或"每个作用域"的方式注册的一些实例只创建一次。在这种情况下,您将不得不从该类中提取逻辑,以防止基础设施与业务逻辑混合。