Это хорошая практика - иметь логгер как синглтон?

81

У меня была привычка передавать логгер конструктору, например:

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

ОБНОВЛЕНИЕ: Как правильно заметил Мерлин, я должен упомянуть, что в первом и втором примерах я использую DI.

Гедрюс
источник
1
Судя по ответам, которые я вижу, похоже, что люди не осознавали, что вы делаете инъекции в обоих этих примерах. Можете ли вы пояснить это в вопросе? Я добавил теги, чтобы решить некоторые из них.
Мерлин Морган-Грэм
1
Прочтите комментарии к этой статье Миф о внедрении зависимостей: передача ссылок от Мишко Хевери из Google, который довольно хорошо разбирается в тестировании и внедрении зависимостей. Он говорит, что ведение журнала - это своего рода исключение, когда синглтон допустим, если вам не нужно тестировать вывод регистратора (в этом случае используйте DI).
Пользователь

Ответы:

35

Это тоже начинает раздражать - это не СУХОЕ

Это правда. Но вы можете сделать лишь так много для решения сквозной проблемы, которая пронизывает каждый ваш тип. Вы должны использовать регистратор везде, поэтому у вас должно быть свойство для этих типов.

Итак, давайте посмотрим, что мы можем с этим поделать.

Синглтон

Синглтоны ужасны <flame-suit-on>.

Я рекомендую придерживаться инъекции свойств, как вы это сделали со вторым примером. Это лучший факторинг, который вы можете сделать, не прибегая к магии. Лучше иметь явную зависимость, чем скрывать ее через синглтон.

Но если синглтоны сэкономят вам значительное время, включая весь рефакторинг, который вам когда-либо придется делать (время хрустального шара!), Я полагаю, вы сможете с ними жить. Если когда-либо был использован синглтон, то это могло быть оно. Имейте в виду, что цена, если вы когда-нибудь захотите передумать, будет максимально высокой.

Если вы сделаете это, проверьте ответы других людей , используя в Registryшаблон (см описание), и те , регистрируя (самовосстанавливающийся) одноплодной завод , а не экземпляр одноплодной регистратор.

Есть и другие альтернативы, которые могут работать так же хорошо без особых компромиссов, поэтому вам следует сначала проверить их.

Фрагменты кода Visual Studio

Вы можете использовать фрагменты кода Visual Studio, чтобы ускорить ввод этого повторяющегося кода. Вы сможете ввести что-то вродеloggertab , и код волшебным образом появится для вас.

Использование AOP для СУШКИ

Вы можете немного избавиться от этого кода внедрения свойств, используя платформу аспектно-ориентированного программирования (АОП), такую ​​как PostSharp, для автоматической генерации некоторых из них.

Когда вы закончите, это может выглядеть примерно так:

[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 для Синглтонов ужасны. Попробуйте использовать синглтон с несколькими загрузчиками классов, то есть среду сервера приложений, где диспетчер является клиентом, где я испытал ужасные примеры двойного ведения журнала (но не настолько ужасны, как операторы регистрации, поступающие из неправильного места, о котором мне сказал какой-то программист на C ++)
Никлас R.
1
@NickRosencrantz +1 за ужасную историю C ++; Мне нравится тратить пару минут на размышления об ужасах одиночек, а потом прокручивать вниз и говорить: «Ха-ха, лол, по крайней мере, у меня нет их проблем».
Доменик
32
</flame-suit-on> Не знаю, как вы жили в огненном костюме 5 лет, но надеюсь, что это поможет.
Brennen Sprimont
39

Я помещаю экземпляр регистратора в свой контейнер для внедрения зависимостей, который затем вводит регистратор в классы, которые в нем нуждаются.

Дэниел Роуз
источник
8
Не могли бы Вы уточнить? Потому что я думаю, что это то, что я делал / делал в рассматриваемых примерах кода 1 и 2?
Giedrius
2
Класс «использующий» будет в основном выглядеть так же, как и тот, что у вас уже есть. Однако фактическое значение не нужно указывать вашему классу вручную, поскольку контейнер DI делает это за вас. Это значительно упрощает замену регистратора в тесте по сравнению со статическим одноэлементным регистратором.
Daniel Rose
2
Не голосование за, хотя это правильный ответ (ИМО), потому что ОП уже сказал, что они это делают. Кроме того, каково ваше обоснование использования этого по сравнению с использованием синглтона? Что насчет проблем, которые возникают у OP с повторяющимся кодом - как это решается в этом ответе?
Мерлин Морган-Грэм
1
@Merlyn OP заявил, что у него есть регистратор как часть конструктора или как общедоступное свойство. Он не заявил, что у него есть контейнер DI, вводящий правильное значение. Так что я предполагаю, что это не так. Да, есть повторяющийся код (но с атрибутом auto, например, его очень мало), но IMHO гораздо лучше явно показать зависимость, чем скрыть ее через (глобальный) синглтон.
Daniel Rose
1
@DanielRose: И вы передумали, сказав, что «гораздо лучше явно показать зависимость, чем скрыть ее через (глобальный) синглтон». +1
Мерлин Морган-Грэм
25

Хороший вопрос. Я считаю, что в большинстве проектов логгер является синглтоном.

Мне приходят в голову некоторые идеи:

  • Используйте ServiceLocator (или другой контейнер для внедрения зависимостей, если вы уже используете его), который позволяет вам совместно использовать регистратор между службами / классами, таким образом вы можете создать экземпляр регистратора или даже несколько разных регистраторов и поделиться через ServiceLocator, который, очевидно, будет одиночным , своего рода инверсия управления . Такой подход дает вам большую гибкость в процессе создания и инициализации регистратора.
  • Если вам нужно Logger почти везде - реализовать методы расширения для Objectтипа , так что каждый класс будет иметь возможность вызывать методы лесоруба как LogInfo(), LogDebug(),LogError()
sll
источник
Не могли бы вы уточнить, что вы имели в виду, говоря о методах расширения? Что касается использования ServiceLocator, мне интересно, почему это будет лучше, чем внедрение свойств, как в примере 2.
Гедриус
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, второй - внедрение свойств. Смотрите их редактирование.
Мерлин Морган-Грэм,
16

Синглтон - хорошая идея. Еще лучшая идея - использовать шаблон реестра , который дает немного больше контроля над созданием экземпляров. На мой взгляд, шаблон singleton слишком близок к глобальным переменным. При создании или повторном использовании объекта, обрабатывающего реестр, есть место для будущих изменений правил создания экземпляров.

Сам реестр может быть статическим классом, чтобы предоставить простой синтаксис для доступа к журналу:

Registry.Logger.Info("blabla");
Андерс Абель
источник
6
Впервые наткнулся на шаблон реестра. Не будет ли чрезмерным упрощением сказать, что в основном все глобальные переменные и функции объединены под одним зонтом?
Джоэл Гудвин
В своей простейшей форме это просто зонтик, но его центральное расположение позволяет заменить его на что-то другое (например, пул объектов, экземпляр для каждого использования, экземпляр локального потока) при изменении требований.
Андерс Абель
5
Реестр и ServiceLocator в значительной степени одно и то же. Большинство фреймворков IoC по своей сути являются реализацией реестра.
Майкл Браун,
1
@MikeBrown: Похоже, разница может заключаться в том, что контейнер DI (внутренне, шаблон локатора службы) имеет тенденцию быть экземпляром класса, тогда как шаблон реестра использует статический класс. Звучит правильно?
Мерлин Морган-Грэм,
1
@MichaelBrown Разница в том, где вы используете локатор реестра / службы. Если это просто в корне композиции, это нормально; если использовать во многих местах - плохо.
Онур
10

Простой синглтон - не лучшая идея. Это затрудняет замену регистратора. Я обычно использую фильтры для своих регистраторов (некоторые «шумные» классы могут регистрировать только предупреждения / ошибки).

Я использую шаблон singleton в сочетании с шаблоном прокси для фабрики логгеров:

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в корне приложения, а я не знаю почему вы сбросили его)
Мерлин Морган-Грэм
1
@ MerlynMorgan-Graham: Маловероятно, что вы смените завод после того, как настроили его. Любые модификации будут производиться на заводе-исполнителе, и вы получите полный контроль над этим. Я бы не рекомендовал этот шаблон как универсальное решение для синглтонов, но он хорошо работает для фабрик, поскольку их API редко меняется. (так что вы могли бы назвать это проксированным абстрактным фабричным синглтоном, хе-хе,
смесью паттернов
3

Есть книга Dependency Injection in .NET. Исходя из того, что вам нужно, вы должны использовать перехват.

В этой книге есть диаграмма, помогающая решить, использовать ли внедрение конструктора, внедрение свойств, внедрение методов, окружающий контекст, перехват.

Вот как можно рассуждать, используя эту диаграмму:

  1. У вас есть зависимость или она вам нужна? - Нужно это
  2. Это сквозная проблема? - Да
  3. Вам нужен ответ? - нет

Использовать перехват

Петр Перак
источник
Я думаю, что это ошибка в первом вопросе рассуждения (я думаю, это должно быть «У вас есть зависимость или она вам нужна?»). Во всяком случае, +1 за упоминание Interception, изучение его возможностей в Виндзоре, и это выглядит очень интересно. Если бы вы могли дополнительно привести пример того, как вы себе представляете, как он здесь поместится, было бы здорово.
Giedrius
+1; Интересное предложение - в основном предоставляло бы функциональность, подобную АОП, без необходимости касаться типов. Мое мнение (основанное на очень ограниченном разоблачении) заключается в том, что перехват через генерацию прокси (тип, который может предоставить библиотека DI в отличие от библиотеки на основе атрибутов AOP) кажется черной магией и может несколько затруднить понимание того, что такое продолжается. Я неправильно понимаю, как это работает? У кого-нибудь был другой опыт с этим? Это не так страшно, как кажется?
Мерлин Морган-Грэм
2
Если вы чувствуете, что перехват - это слишком «волшебство», вы можете просто использовать шаблон проектирования декоратора и реализовать его самостоятельно. Но после этого вы, вероятно, поймете, что это пустая трата времени.
Петр Перак
Я предположил, что это предложение будет автоматически переносить каждый вызов в журнал, а не разрешать (создавать) сам журнал класса. Это правильно?
Мерлин Морган-Грэм
Да. Вот что делает декоратор.
Петр Перак
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

Если вы хотите найти хорошее решение для ведения журнала, я предлагаю вам взглянуть на движок приложений Google с python, где ведение журнала так же просто, как import loggingи тогда вы можете просто logging.debug("my message")илиlogging.info("my message") который действительно делает его настолько простым, насколько и должно.

У Java не было хорошего решения для ведения журнала, т.е. следует избегать log4j, поскольку он практически вынуждает вас использовать синглтоны, которые, как здесь ответили, «ужасны», и у меня был ужасный опыт попытки сделать вывод журнала одним и тем же оператором ведения журнала только один раз когда я подозреваю, что причиной двойного ведения журнала было то, что у меня есть один синглтон объекта ведения журнала в двух загрузчиках классов на одной виртуальной машине (!)

Прошу прощения за то, что я не настолько специфичен для C #, но из того, что я видел, решения с C # выглядят похожими на Java, где у нас был log4j, и мы также должны сделать его синглтоном.

Вот почему мне очень понравилось решение с GAE / python , оно настолько простое, насколько это возможно, и вам не нужно беспокоиться о загрузчиках классов, получении оператора двойной регистрации или каких-либо шаблонов проектирования вообще в этом отношении.

Я надеюсь, что часть этой информации может иметь отношение к вам, и я надеюсь, что вы захотите взглянуть на решение для ведения журнала, которое я рекомендую вместо этого, я запугиваю, насколько проблема синглтона подозревается из-за невозможности иметь настоящий синглтон, когда он должен быть запущен в нескольких загрузчиках классов.

Никлас Р.
источник
не является ли эквивалентный код в C # синглтоном (или статическим классом, который потенциально такой же, потенциально хуже, поскольку он предлагает еще меньшую гибкость)? using Logging; /* ... */ Logging.Info("my message");
Мерлин Морган-Грэм,
И вы можете избежать синглтонов с шаблоном log4j, если вы используете инъекцию зависимостей для внедрения ваших логгеров. Это делают многие библиотеки. Вы также можете использовать оболочки, которые предоставляют общие интерфейсы, чтобы вы могли заменить свою реализацию ведения журнала позже, например netcommon.sourceforge.net или одно из расширений, предоставляемых библиотеками контейнеров DI, такими как Castle.Windsor или Ninject
Мерлин Морган-Грэм
Это проблема! Никто и никогда не использует logging.info("my message")программу более сложную, чем hello world. Обычно вы делаете довольно много шаблонной инициализации регистратора - настраивая форматтер, уровень, настраивая обработчики файлов и консоли. Никогда logging.info("my message")!
Крестный отец