Найкраща практика обгортки журналу


91

Я хочу використовувати nlogger у своїй програмі, можливо, у майбутньому мені доведеться змінити систему реєстрації. Тому я хочу використовувати фасад лісозаготівлі.

Чи знаєте ви якісь рекомендації щодо існуючих прикладів, як їх писати? Або просто дайте мені посилання на найкращі практики в цій галузі.



Ви перевіряли проект "Простий журнал фасадів" на Codeplex ?
JefClaes

Відповіді:


207

Раніше я використовував фасади реєстрації, такі як Common.Logging (навіть для того, щоб приховати власну бібліотеку CuttingEdge.Logging ), але сьогодні я використовую шаблон введення залежності, і це дозволяє мені приховувати реєстратори за власною (простою) абстракцією, яка дотримується обох залежностей Принцип інверсії та Принцип сегрегації інтерфейсу(ISP), оскільки він має одного члена і тому, що інтерфейс визначається моєю програмою; не зовнішня бібліотека. Тим менше, мінімізуючи знання основних частин вашої програми про існування зовнішніх бібліотек; навіть якщо ви не маєте наміру ніколи замінювати свою бібліотеку реєстрації. Важка залежність від зовнішньої бібліотеки ускладнює тестування коду та ускладнює програму за допомогою API, який ніколи не був розроблений спеціально для вашої програми.

Ось так часто виглядає абстракція в моїх додатках:

public interface ILogger
{
    void Log(LogEntry entry);
}

public enum LoggingEventType { Debug, Information, Warning, Error, Fatal };

// Immutable DTO that contains the log information.
public class LogEntry 
{
    public readonly LoggingEventType Severity;
    public readonly string Message;
    public readonly Exception Exception;

    public LogEntry(LoggingEventType severity, string message, Exception exception = null)
    {
        if (message == null) throw new ArgumentNullException("message");
        if (message == string.Empty) throw new ArgumentException("empty", "message");

        this.Severity = severity;
        this.Message = message;
        this.Exception = exception;
    }
}

За бажанням, цю абстракцію можна розширити за допомогою деяких простих методів розширення (що дозволяє інтерфейсу залишатися вузьким і дотримуватися Інтернету). Це робить код для споживачів цього інтерфейсу набагато простішим:

public static class LoggerExtensions
{
    public static void Log(this ILogger logger, string message) {
        logger.Log(new LogEntry(LoggingEventType.Information, message));
    }

    public static void Log(this ILogger logger, Exception exception) {
        logger.Log(new LogEntry(LoggingEventType.Error, exception.Message, exception));
    }

    // More methods here.
}

Оскільки інтерфейс містить лише один метод, ви можете легко створити ILoggerреалізацію, яка проксі-сервери log4net , Serilog , Microsoft.Extensions.Logging , NLog або будь-яку іншу бібліотеку ведення журналів та налаштувати свій контейнер DI, щоб вводити його в класи, які мають ILoggerу своєму конструктор.

Зверніть увагу, що наявність методів статичного розширення поверх інтерфейсу з одним методом цілком відрізняється від інтерфейсу з багатьма членами. Методи розширення - це лише допоміжні методи, які створюють LogEntryповідомлення та передають його через єдиний метод на ILoggerінтерфейсі. Методи розширення стають частиною коду споживача; не є частиною абстракції. Це не тільки дозволяє методам розширення еволюціонувати без необхідності змінювати абстракцію, методи розширення таLogEntryконструктор завжди виконується, коли використовується абстракція реєстратора, навіть коли цей реєстратор заглушений / знущається. Це дає більшу впевненість у правильності викликів реєстратора під час запуску в наборі тестів. Одночленний інтерфейс також значно полегшує тестування; Наявність абстракції з багатьма членами ускладнює створення реалізацій (таких як макети, адаптери та декоратори).

Коли ви робите це, навряд чи колись виникає потреба в якійсь статичній абстракції, яку можуть запропонувати фасади реєстрації (або будь-яка інша бібліотека).


4
@GabrielEspinoza: Це повністю залежить від простору імен. Ви розміщуєте методи розширення. Якщо ви розміщуєте його в тому ж просторі імен, що і інтерфейс, або в кореневому просторі імен вашого проекту, проблема не існуватиме.
Стівен

2
@ user1829319 Це лише приклад. Я впевнений, що ви можете придумати реалізацію на основі цієї відповіді, яка відповідає вашим конкретним потребам.
Стівен

2
Я все ще не розумію ... де перевага мати 5 методів реєстрації як розширення ILogger, а не бути членами ILogger?
Елізабет

3
@Elisabeth Перевага полягає в тому, що ви можете адаптувати фасадний інтерфейс до БУДЬ-ЯКОЇ системи ведення журналу, просто реалізувавши одну функцію: "ILogger :: Log". Методи розширення гарантують, що ми маємо доступ до "зручних" API (таких як "LogError", "LogWarning" тощо), незалежно від того, яку структуру ви вирішили використовувати. Це круговий спосіб додати загальну функціональність "базового класу", незважаючи на роботу з інтерфейсом C #.
BTownTKD

2
Мені потрібно ще раз підкреслити причину, чому це чудово. Перетворення вашого коду з DotNetFramework на DotNetCore. У проектах, де я це робив, мені потрібно було написати лише один конкретний конкретний варіант. Ті, де я не .... гааааааааааааааааааааааааааааааааааааааааааааааааааааа! Я радий, що знайшов цей "шлях назад".
granadaCoder


8

На сьогоднішній день найкращим варіантом є використання пакета Microsoft.Extensions.Logging ( як зазначив Джуліан ). З цим можна використовувати більшість фреймворків журналювання.

Визначення власного інтерфейсу, як пояснюється у відповіді Стівена, є нормальним для простих випадків, але воно пропускає кілька речей, які я вважаю важливими:

  • Структуровані об'єкти ведення журналу та деструктуризації (позначення @ у Serilog та NLog)
  • Затримка побудови / форматування рядків: оскільки вона приймає рядок, вона повинна оцінити / відформатувати все при виклику, навіть якщо врешті-решт подія не буде зареєстрована, оскільки вона нижче порогової (вартість продуктивності, див. Попередній пункт)
  • Умовні перевірки, подібні до того, IsEnabled(LogLevel)які вам можуть знадобитися, ще раз з міркувань ефективності

Можливо, ви можете реалізувати все це у своїй власній абстракції, але в цей момент ви будете винаходити колесо.


4

Як правило, я вважаю за краще створювати такий інтерфейс, як

public interface ILogger
{
 void LogInformation(string msg);
 void LogError(string error);
}

а під час виконання я ввожу конкретний клас, який реалізований з цього інтерфейсу.


11
І не забувайте, LogWarningі LogCriticalметоди , і всі їхні перевантаження. Роблячи це, ви порушите Принцип розділення інтерфейсу . Віддайте перевагу визначенню ILoggerінтерфейсу одним Logметодом.
Стівен

2
Я справді шкодую, це не був мій намір. Не потрібно соромитися. Я бачу цей дизайн дуже часто, тому що багато розробників використовують як приклад популярний log4net (який використовує саме цей дизайн). На жаль, такий дизайн насправді не дуже хороший.
Стівен

2
Я вважаю за краще це відповіді @ Steven. Він вводить залежність від LogEntryі, отже, залежність від LoggingEventType. ILoggerРеалізація повинна мати справу з цим LoggingEventTypes, ймовірно , хоча case/switch, що це код запах . Навіщо приховувати LoggingEventTypesзалежність? Реалізація повинна обробляти рівні реєстрації в будь-якому випадку , тому було б краще чітко визначити, що має робити реалізація, а не приховувати це за одним методом із загальним аргументом.
DharmaTurtle

1
Як крайній приклад, уявіть, ICommandякий має a, Handleякий приймає object. Реалізації мають бути case/switchнад можливими типами, щоб виконати контракт інтерфейсу. Це не ідеально. Не майте абстракції, яка приховує залежність, з якою все одно потрібно боротися. Натомість має інтерфейс, який чітко вказує, що очікується: "Я очікую, що всі реєстратори оброблятимуть Попередження, Помилки, Фатали тощо". Це переважно, ніж "Я очікую, що всі реєстратори оброблятимуть повідомлення, які включають попередження, помилки, суми тощо".
DharmaTurtle

Я якось погоджуюсь з @Steven та @DharmaTurtle. Крім того, їх LoggingEventTypeслід викликати LoggingEventLevelяк типи - це класи, і вони повинні кодуватися як такі в ООП. Для мене немає різниці між невикористанням методу інтерфейсу та невикористанням відповідного enumзначення. Натомість використовуйте ErrorLoggger : ILogger, InformationLogger : ILoggerде кожен реєстратор визначає свій рівень. Потім DI потрібно вводити необхідні реєстратори, ймовірно, за допомогою ключа (enum), але цей ключ більше не є частиною інтерфейсу. (Ви зараз ТВЕРДІ).
Wouter

4

Чудове рішення цієї проблеми з’явилося у вигляді проекту LibLog .

LibLog - це абстракція журналів із вбудованою підтримкою основних реєстраторів, включаючи Serilog, NLog, Log4net та Enterprise logger. Він встановлюється через менеджер пакунків NuGet у цільову бібліотеку як вихідний (.cs) файл замість посилання .dll. Цей підхід дозволяє включити абстракцію журналу, не змушуючи бібліотеку приймати зовнішню залежність. Це також дозволяє автору бібліотеки включати ведення журналу, не змушуючи програму-споживача явно надавати реєстратор бібліотеці. LibLog використовує рефлексію, щоб зрозуміти, який конкретний реєстратор використовується, і підключити до нього без явного коду підключення в проекті бібліотеки.

Отже, LibLog - чудове рішення для ведення журналу в бібліотечних проектах. Просто наведіть посилання та налаштуйте конкретний реєстратор (Serilog для перемоги) у вашому основному додатку чи службі та додайте LibLog до своїх бібліотек!


Я використав це, щоб подолати проблему з порушеннями log4net (yuck) ( wiktorzychla.com/2012/03/pathetic-breaking-change-between.html ) Якщо ви отримаєте це з nuget, він фактично створить файл .cs у вашому коді, а не додавати посилання на попередньо скомпільовані dll. Файл .cs має простір імен для вашого проекту. Отже, якщо у вас різні рівні (csprojs), у вас буде кілька версій, або вам потрібно об’єднатись у спільний csproj. Ви зрозумієте це, коли спробуєте ним скористатися. Але, як я вже сказав, це було порятунком, оскільки проблема log4net порушувала зміни.
granadaCoder


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.