将记录器作为单例是一个好习惯吗?


80

我习惯将记录器传递给构造函数,例如:

public class OrderService : IOrderService {
     public OrderService(ILogger logger) {
     }
}

但这很烦人,因此我已经将它用作属性一段时间了:

private ILogger logger = NullLogger.Instance;
public ILogger Logger
{
    get { return logger; }
    set { logger = value; }
}

这也很烦人-它并不干燥,我需要在每节课中重复。我可以使用基类,但是再说一次-我使用的是Form类,因此需要FormBase等。所以我认为,暴露ILogger的单例将带来什么负面影响,所以一个人就会知道在哪里获取记录器:

    Infrastructure.Logger.Info("blabla");

更新:正如Merlyn正确注意到的那样,我应该提到,在第一和第二个示例中,我正在使用DI。



1
从我看到的答案,人们似乎没有意识到你是注入在这两个例子。您可以在问题中更清楚吗?我添加了标签来解决其中的一些问题。
Merlyn Morgan-Graham

1
阅读本文的评论Dependency Injection Myth:Reference Passing来自Google的MiškoHevery,他在测试和依赖注入方面相当重要。他说,除非您需要测试记录器的输出(在这种情况下使用DI),否则单例就可以进行记录是一种例外。
用户

Answers:


35

这也变得很烦人-它不是干的

确实如此。但是,对于跨领域的问题您无能为力,这种问题遍及您的每种类型。您必须在各处使用记录器,因此必须在这些类型上具有该属性。

因此,让我们看看我们能做些什么。

辛格尔顿

单身人士太可怕了<flame-suit-on>

我建议像第二个示例一样,坚持使用属性注入。这是您不诉诸魔术即可做到的最佳因素。具有显式依赖关系比通过单例隐藏它更好。

但是,如果单身人士为您节省了大量时间,包括您将不得不做的所有重构工作(水晶球时间!),我想您也许可以与他们共处。如果曾经有一个Singleton的用途,可能就是这样。记住的成本,如果你曾经想改变你的思想会约高,因为它得到。

如果你这样做,看看别人的答案使用Registry模式(见说明书),而那些注册(可复位)单工厂,而不是一个单独的记录器实例。

还有其他选择也可以在不做太多妥协的情况下同样有效,因此您应该先检查一下它们。

Visual Studio代码段

您可以使用Visual Studio代码段来加快重复代码的输入速度。您将可以输入类似loggertab,并且代码会为您神奇地出现。

使用AOP进行干燥

您可以通过使用诸如PostSharp之类的面向方面的编程(AOP)框架来自动生成其中的一些属性注入代码。

完成后,可能看起来像这样:

[InjectedLogger]
public ILogger Logger { get; set; }

您还可以使用他们的方法跟踪示例代码来自动跟踪方法的入口和出口代码,这可能消除了将某些记录器属性全部添加在一起的需要。您可以在类级别或命名空间范围内应用属性:

[Trace]
public class MyClass
{
    // ...
}

// or

#if DEBUG
[assembly: Trace( AttributeTargetTypes = "MyNamespace.*",
    AttributeTargetTypeAttributes = MulticastAttributes.Public,
    AttributeTargetMemberAttributes = MulticastAttributes.Public )]
#endif

3
单身人士的+1非常糟糕。尝试将Singleton与多个类加载器一起使用,即在应用程序服务器环境中,调度程序是客户端,在该环境中,我经历了可怕的双重日志记录示例(但并不像来自某些C ++程序员告诉我的错误位置的日志记录语句那样可怕)
Niklas R.11年

1
@NickRosencrantz +1表示C ++恐怖故事;我喜欢花几分钟的时间考虑单身人士的可怕性,只是向下滚动然后走下去,“哈哈,哈哈,至少我没有他们的问题。”
多米尼克

32
</flame-suit-on> 我不知道你在火焰服中住了5年,但我希望这会有所帮助。
布伦嫩·斯普里蒙特

39

我在自己的依赖项注入容器中放入了一个logger实例,然后将其注入到需要一个类的类中。


8
你可以再详细一点吗?因为我认为这就是我在所讨论的1个和2个代码示例中所做的事情?
Giedrius

2
“ using”类看起来与您已经拥有的基本相同。但是,由于DI容器会为您执行此操作,因此不必手动将实际值提供给您的班级。与使用静态单例记录器相比,这使得在测试中更换记录器更加容易。
丹尼尔·罗斯

2
即使这是正确的答案(IMO),也不能拒绝,因为OP已经表示他们正在这样做。另外,使用此方法与使用Singleton的原理是什么?OP的重复代码存在什么问题-这个答案如何解决?
Merlyn Morgan-Graham

1
@Merlyn OP表示他有一个记录器,该记录器是构造函数的一部分或作为公共财产。他没有声明自己有一个DI容器注入了正确的值。所以我假设情况并非如此。是的,有一些重复的代码(但是带有属性auto属性,例如,很少),但是恕我直言,显式显示依赖关系比通过(全局)单例隐藏它要好得多。
Daniel Rose

1
@DanielRose:并且您改变了主意,“显式显示依赖比通过(全局)单例隐藏它要好得多”。+1
Merlyn Morgan-Graham

25

好问题。我相信大多数项目记录器都是单例的。

我想到了一些想法:

  • 使用ServiceLocator(或其他依赖项注入)容器,如果您已经使用过的话),允许您在服务/类之间共享记录器,这样您就可以实例化记录器甚至多个不同的记录器,并通过ServiceLocator进行共享,这显然是单例,某种控制反转。这种方法为您提供了记录器实例化和初始化过程的更多灵活性。
  • 如果你需要记录几乎无处不在-实现扩展方法Object类型,这样每一类将能够调用记录器的方法,例如LogInfo()LogDebug()LogError()

您能否更具体地讲一下扩展方法?关于使用ServiceLocator,我想知道为什么它会比属性注入更好,如示例2所示
。– Giedrius

1
@Giedrius:关于扩展方法-你可以创建扩展方法一样public static void LogInfo(this Object instance, string message)所以每个类将它捡起来,关于ServiceLocator-这可以让你有记录仪作为一个普通类的实例不是单身,那么你会得到更大的灵活性
SLL

@Giedrius,如果您通过构造函数注入实现IoC,并且所使用的DI框架具有扫描构造函数并自动注入依赖项的能力(假设您已在DI框架引导过程中配置了这些依赖项),则采用了以前的方式这样做会很好。同样,大多数DI框架都应允许您设置每个对象的生命周期范围。
GR7

7
ServiceLocator不是DI容器。它被认为是反模式。
霹雳霹雳州

1
OP已将DI与DI容器一起使用。第一个示例是ctor注入,第二个示例是属性注入。看到他们的编辑。
Merlyn Morgan-Graham

16

单身人士是个好主意。更好的主意是使用注册表模式,该模式对实例化提供了更多控制。我认为单例模式过于接近全局变量。通过注册表处理对象的创建或重用,将来可以更改实例化规则。

注册表本身可以是一个静态类,以提供简单的语法来访问日志:

Registry.Logger.Info("blabla");

6
第一次遇到注册表模式。说基本上是所有全局变量和函数都放在一个保护伞下是否过于简单?
乔尔·古德温

在最简单的形式中,它只是一把雨伞,但是将其放在中央位置可以在需求更改时将其更改为其他对象(可能是对象池,每次使用的实例,线程本地实例)。
安德斯·亚伯

5
Registry和ServiceLocator几乎是相同的。大多数IoC框架都是注册管理机构的核心。
迈克尔·布朗

1
@MikeBrown:似乎不同之处可能是DI容器(内部是服务定位器模式)往往是实例化类,而注册表模式使用静态类。听起来对吗?
Merlyn Morgan-Graham

1
@MichaelBrown区别在于您使用注册表/服务定位器的位置。如果它只是在合成的根目录中,那很好。如果您在许多地方使用,那是不好的。
Onur 2015年

10

简单的单例并不是一个好主意。这使得很难更换记录仪。我倾向于为记录器使用过滤器(某些“嘈杂的”类可能仅记录警告/错误)。

我将单例模式与代理模式结合用于记录器工厂:

public class LogFactory
{
    private static LogFactory _instance;

    public static void Assign(LogFactory instance)
    {
        _instance = instance;
    }

    public static LogFactory Instance
    {
        get { _instance ?? (_instance = new LogFactory()); }
    }

    public virtual ILogger GetLogger<T>()
    {
        return new SystemDebugLogger();
    }
}

这使我无需更改任何代码即可创建aFilteringLogFactory或a SimpleFileLogFactory(因此符合Open / Closed原理)。

样品扩展

public class FilteredLogFactory : LogFactory
{
    public override ILogger GetLogger<T>()
    {
        if (typeof(ITextParser).IsAssignableFrom(typeof(T)))
            return new FilteredLogger(typeof(T));

        return new FileLogger(@"C:\Logs\MyApp.log");
    }
}

并使用新工厂

// and to use the new log factory (somewhere early in the application):
LogFactory.Assign(new FilteredLogFactory());

在您的班级中应记录以下内容:

public class MyUserService : IUserService
{
    ILogger _logger = LogFactory.Instance.GetLogger<MyUserService>();

    public void SomeMethod()
    {
        _logger.Debug("Welcome world!");
    }
}

没关系,我以前的评论。我想我明白你在这里做什么。您能否提供实际记录到该类的示例?
Merlyn Morgan-Graham

+1; 绝对胜过赤裸裸的单身人士:)由于使用了静态对象,仍然有一些轻微的耦合和潜在的静态作用域/线程问题,但是我想那些很少见(您想Instance在应用程序根目录中设置,我不知道为什么您要重置它)
Merlyn Morgan-Graham

1
@ MerlynMorgan-Graham:设置工厂后不太可能更改工厂。任何修改都将在实施工厂中进行,您可以完全控制它。我不建议将此模式作为单例的通用解决方案,但由于工厂的API很少更改,因此它对于工厂非常有效。(所以您可以将其称为抽象工厂单例代理,呵呵,模式混搭)
jgauffin 2011年

3

.NET中有一本书“依赖注入”。根据需要,您应该使用拦截。

本书中的图表有助于确定是否使用构造函数注入,属性注入,方法注入,环境上下文,拦截。

这就是使用此图的原因之一:

  1. 您是否有依赖关系?- 需要它
  2. 是跨领域的关注点吗?-是的
  3. 您需要答案吗?-没有

使用拦截


我认为第一个推理问题有一个错误(我认为应该是“您有依赖性还是需要依赖性?”)。无论如何,+ 1代表拦截,在温莎学习它的可能性,这看起来非常有趣。如果您还可以举个例子,如何想象它适合这里,那就太好了。
Giedrius

+1; 有趣的建议-基本上可以提供类似AOP的功能,而无需接触类型。我的观点(基于非常有限的公开程度)是,通过代理生成(DI库可以提供的类型,而不是基于AOP属性的库的类型)进行拦截的感觉就像是黑魔法,并且可能使理解什么有些困难。继续。我对这如何工作的理解不正确吗?任何人都有不同的经历吗?它比听起来少吓人吗?
Merlyn Morgan-Graham

2
如果您觉得Interception非常“神奇”,那么您可以使用装饰器设计模式并自己实现。但是之后,您可能会意识到这是浪费时间。
霹雳霹雳州

我认为该建议将自动将每个调用包装在日志记录中,而不是让(自己)记录类本身。那是对的吗?
Merlyn Morgan-Graham

是。这就是装饰器所做的。
霹雳霹雳州

0

我个人认为最简单的另一种解决方案是使用静态Logger类。您可以从任何类方法中调用它,而无需更改类,例如添加属性注入等。它非常简单易用。

Logger::initialize ("filename.log", Logger::LEVEL_ERROR); // only need to be called once in your application

Logger::log ("my error message", Logger::LEVEL_ERROR); // to be used in every method where needed

-1

如果您想寻找一个很好的日志记录解决方案,建议您使用python来查看google app引擎,其中日志记录非常简单import logging,然后就可以做到,logging.debug("my message")或者logging.info("my message")实际上可以使日志尽可能简单。

Java没有一个很好的日志记录解决方案,即应避免使用log4j,因为它实际上迫使您使用单例,这在这里回答是“可怕的”,并且我在尝试使一次日志记录输出相同的日志记录语句方面经历了可怕的经历。当我怀疑重复记录的原因是我在同一虚拟机的两个类加载器中有一个Singleton的记录对象时(!)

不好意思,我对C#并不是那么专一,但是从我看到的C#解决方案来看,Java看上去与Java类似,因为我们有log4j,也应该将其做成单例。

这就是为什么我真的很喜欢GAE / python解决方案,它是如此简单,而且您不必为此担心类加载器,获取双记录语句或任何设计模式。

我希望其中一些信息与您相关,并且希望您看一看我建议的日志记录解决方案,而不是因为我不可能欺负Singleton导致多少问题,因为它必须在多个类加载器中实例化。


C#中的等效代码难道不是一个Singleton(或一个静态类,因为它提供的灵活性甚至更低,可能相同,甚至更糟)吗? using Logging; /* ... */ Logging.Info("my message");
Merlyn Morgan-Graham

如果使用依赖项注入来记录器,则可以避免使用log4j模式的单例。许多图书馆都这样做。您还可以使用提供通用接口的包装器,以便日后换出日志记录实现,例如netcommon.sourceforge.net或DI容器库提供的​​扩展之一,例如Castle.Windsor或Ninject
Merlyn Morgan-Graham

那就是问题所在!没有人会logging.info("my message")在比hello world更复杂的程序中使用过。通常,您会做很多初始化记录器的样板工作-设置格式化程序,级别,设置文件和控制台处理程序。从来没有logging.info("my message")
教父
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.