【问题标题】:Is it a good practice to have logger as a singleton?将记录器作为单身人士是一个好习惯吗?
【发布时间】:2011-12-12 10:11:02
【问题描述】:

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

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。

【问题讨论】:

  • 从我看到的答案看来,人们似乎没有意识到你在这两个例子中都在 injecting。你能在问题中更清楚地说明这一点吗?我添加了标签来处理其中的一些问题。
  • 阅读这篇文章 Dependency Injection Myth: Reference Passing 的 cmets,作者是 Google 的 Miško Hevery,他非常擅长测试和依赖注入。他说日志记录是一种例外,除非您需要测试记录器的输出(在这种情况下使用 DI),否则单例是可以的。

标签: c# .net logging dependency-injection singleton


【解决方案1】:

我在我的依赖注入容器中放置了一个记录器实例,然后将记录器注入到需要的类中。

【讨论】:

  • 你能说得更具体点吗?因为我认为这就是我在有问题的 1 和 2 个代码示例中所做/所做的事情?
  • “使用”类看起来与您已有的基本相同。然而,实际值不必手动提供给您的类,因为 DI 容器会为您执行此操作。与使用静态单例记录器相比,这使得在测试中替换记录器变得更加容易。
  • 不赞成,即使这是正确的答案(IMO),因为 OP 已经说过他们正在这样做。另外,您使用这个与使用单例的理由是什么? OP 在重复代码方面遇到的问题呢?这个答案是如何解决的?
  • @Merlyn OP 表示他有一个记录器作为构造函数的一部分或作为公共财产。他没有说明他有一个 DI 容器注入正确的值。所以我假设情况并非如此。是的,有一些重复的代码(但具有属性的自动属性,例如,它很少),但恕我直言,显式显示依赖关系比通过(全局)单例隐藏它要好得多。
  • @DanielRose:你改变了我的想法,“显式显示依赖比通过(全局)单例隐藏它要好得多”。 +1
【解决方案2】:

这也越来越烦人了——不是DRY

确实如此。但是,对于遍及您拥有的每种类型的横切关注点,您能做的只有这么多。您必须在任何地方使用记录器,因此您必须拥有这些类型的属性。

让我们看看我们能做些什么。

单例

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

我建议您坚持使用属性注入,就像您在第二个示例中所做的那样。这是您可以在不诉诸魔法的情况下进行的最佳分解。有一个显式的依赖比通过单例隐藏它更好。

但是,如果单例可以为您节省大量时间,包括您将不得不做的所有重构(水晶球时间!),我想您也许可以忍受它们。如果单例有什么用处,可能就是这样。请记住,如果您曾经想要改变主意,所付出的代价将是最高的。

如果您这样做,请使用the Registry pattern(参见说明)查看其他人的答案,以及那些注册(可重置)单例工厂而不是单例记录器实例的人。

还有其他替代方案也可以在没有太大妥协的情况下发挥同样的作用,因此您应该先检查一下。

Visual Studio 代码 sn-ps

您可以使用Visual Studio code snippets 来加快进入该重复代码的速度。您将能够输入类似loggertab 的内容,代码会神奇地出现在您面前。

使用 AOP 来 DRY 关闭

您可以通过使用an Aspect Oriented Programming (AOP) framework like PostSharp 自动生成一些属性注入代码来消除一些属性注入代码。

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

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

您还可以使用their method tracing sample code 自动跟踪方法入口和退出代码,这可能会消除将一些记录器属性一起添加的需要。您可以在类级别或命名空间范围内应用该属性:

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

// or

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

【讨论】:

  • 单身人士的 +1 很糟糕。尝试将单例与多个类加载器一起使用,即调度程序是客户端的应用程序服务器环境,我在其中经历过可怕的双重日志记录示例(但不像某些 C++ 程序员告诉我的来自错误位置的日志记录语句那么可怕)跨度>
  • @NickRosencrantz +1 用于 C++ 恐怖故事;我喜欢花几分钟时间思考单身人士的可怕之处,然后向下滚动并说“哈哈,至少我没有他们的问题。”
  • </flame-suit-on> 我不知道你是怎么穿着火焰服生活了 5 年的,但我希望这能有所帮助。
【解决方案3】:

好问题。我相信在大多数项目中记录器是一个单身人士。

我突然想到了一些想法:

  • 使用ServiceLocator(或其他Dependency Injection容器,如果您已经使用任何容器)允许您在服务/类之间共享记录器,这样您可以实例化记录器甚至多个不同的记录器并通过ServiceLocator共享显然是一个单身人士,某种Inversion of Control。这种方法在记录器实例化和初始化过程中为您提供了很大的灵活性。
  • 如果您几乎在任何地方都需要记录器 - 为Object 类型实现扩展方法,这样每个类都可以调用记录器的方法,例如LogInfo()、LogDebug()、LogError()

【讨论】:

  • 您能否更具体一点,您在谈论扩展方法时的想法是什么?关于使用 ServiceLocator,我想知道为什么它比示例 2 中的属性注入更好。
  • @Giedrius :关于扩展方法 - 您可以创建像 public static void LogInfo(this Object instance, string message) 这样的扩展方法,这样每个类都会选择它,关于 ServiceLocator - 这允许您将记录器作为常规类实例而不是单例,所以你会给予很大的灵活性
  • @Giedrius 如果您通过构造函数注入实现 IoC,并且您使用的 DI 框架能够扫描您的构造函数并自动注入您的依赖项(假设您在 DI 框架引导过程中配置了这些依赖项) ),那么你以前的做法就可以了。此外,大多数 DI 框架应该允许您设置每个对象的生命周期范围。
  • ServiceLocator 不是 DI 容器。它被认为是反模式。
  • OP 已经将 DI 与 DI 容器一起使用。第一个例子是ctor注入,第二个是属性注入。查看他们的编辑。
【解决方案4】:

单例是个好主意。一个更好的主意是使用Registry 模式,它可以更好地控制实例化。在我看来,单例模式太接近全局变量了。使用注册表处理对象的创建或重用,将来可以更改实例化规则。

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

Registry.Logger.Info("blabla");

【讨论】:

  • 我第一次遇到注册表模式。说它基本上是所有全局变量和函数都放在一个保护伞下是不是过于简单化了?
  • 在其最简单的形式中,它只是一把雨伞,但将它放在中心位置可以将其更改为其他东西(可能是对象池、每次使用实例、线程本地实例) 当需求发生变化时。
  • Registry 和 ServiceLocator 几乎是一回事。大多数 IoC 框架的核心是 Registry 的实现。
  • @MikeBrown:似乎不同之处在于 DI 容器(内部,服务定位器模式)往往是实例类,而注册表模式使用静态类。听起来对吗?
  • @MichaelBrown 不同之处在于您使用注册表/服务定位器的位置。如果它只是在组合根中,那很好;如果你在很多地方使用,那就不好了。
【解决方案5】:

普通的单例不是一个好主意。这使得更换记录器变得困难。我倾向于为我的记录器使用过滤器(一些“嘈杂”的类可能只记录警告/错误)。

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

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();
    }
}

这使我可以在不更改任何代码的情况下创建FilteringLogFactory 或仅创建SimpleFileLogFactory(因此符合开放/封闭原则)。

示例扩展

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!");
    }
}

【讨论】:

  • 不要介意我之前的评论。我想我明白你在这里做什么。你能提供一个实际记录到这个的类的例子吗?
  • +1;绝对胜过裸体单例:) 由于使用静态对象,仍然有一些轻微的耦合和潜在的静态范围/线程问题,但我想这些很少见(你想在应用程序根目录中设置 Instance 而我不这样做'不知道你为什么要重置它)
  • @MerlynMorgan-Graham:设置工厂后不太可能更改工厂。任何修改都将在实施工厂中完成,您可以完全控制它。我不推荐这种模式作为单例的通用解决方案,但它适用于工厂,因为它们的 API 很少更改。 (所以你可以称它为代理抽象工厂单例,呵呵,模式混搭)
【解决方案6】:

.NET 中有一本书 Dependency Injection。根据您的需要,您应该使用拦截。

本书中有一张图帮助决定是否使用构造函数注入、属性注入、方法注入、环境上下文、拦截。

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

  1. 你有依赖还是需要它? - 需要它
  2. 是横切关注点吗? - 是的
  3. 您需要它的答案吗? - 没有

使用拦截

【讨论】:

  • 我认为第一个推理问题有一个错误(我认为应该是“你有依赖还是需要它?”)。无论如何,提到拦截,+1,在温莎研究它的可能性,它看起来很有趣。如果您还可以举例说明您认为它如何适合这里,那就太好了。
  • +1;有趣的建议 - 基本上会提供类似 AOP 的功能,而无需接触类型。我的意见(基于非常有限的曝光)是通过代理生成(DI库可以提供的类型,而不是基于AOP属性的库)的拦截感觉就像黑魔法一样,并且可以让人有点难以理解正在发生的事情。我对这是如何工作的理解不正确吗?有人对它有不同的体验吗?它没有听起来那么可怕吗?
  • 如果你觉得拦截太“神奇”,那么你可以使用装饰器设计模式并自己实现它。但在那之后你可能会意识到这是在浪费时间。
  • 我认为这个建议会自动将每个调用包含在日志中,而不是让(制作)类日志本身。对吗?
  • 是的。这就是装饰器所做的。
【解决方案7】:

我个人认为最简单的另一个解决方案是使用静态 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

【讨论】:

    【解决方案8】:

    如果您想寻找一个好的日志记录解决方案,我建议您查看带有 python 的 google app engine,其中日志记录与 import logging 一样简单,然后您可以只使用 logging.debug("my message") 或 logging.info("my message"),它确实保持为应该很简单。

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

    请您原谅我对 C# 没有那么具体,但从我所看到的 C# 解决方案看起来与我们有 log4j 的 Java 类似,我们也应该将其设为单例。

    这就是我真正喜欢solution with GAE / python 的原因,它非常简单,您不必担心类加载器、获取双重日志语句或任何设计模式。

    我希望其中一些信息与您相关,我希望您想看看我推荐的日志记录解决方案,而不是因为无法拥有一个单例而被怀疑有多少问题。当它必须在多个类加载器中实例化时是真正的单例。

    【讨论】:

    • 不是 C# 中的等效代码单例(或静态类,可能相同,可能更糟,因为它提供的灵活性更低)? using Logging; /* ... */ Logging.Info("my message");
    • 如果你使用依赖注入来注入你的记录器,你可以避免使用 log4j 模式的单例。很多图书馆都是这样做的。您还可以使用提供通用接口的包装器,以便您可以在以后更换日志记录实现,例如 netcommon.sourceforge.net 或 DI 容器库(如 Castle.Windsor 或 Ninject)提供的扩展之一
    • 这就是问题所在!从来没有人在比 hello world 更复杂的程序中使用过logging.info("my message")。通常你会做很多样板来初始化记录器——设置格式化程序、级别、设置文件和控制台处理程序。从来没有logging.info("my message")!
    猜你喜欢
    • 2012-08-11
    • 2016-01-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-23
    • 2014-12-22
    • 2020-08-25
    • 2015-05-08
    相关资源
    最近更新 更多