Чи є гарною практикою мати реєстратора як синглтона?


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
З відповідей, які я бачу, здається, що люди не усвідомлювали, що ви вводите ін’єкцію в обох цих прикладах. Чи можете ви пояснити це у питанні? Я додав теги, щоб подбати про це.
Merlyn Morgan-Graham

1
Прочитайте коментарі до цієї статті Міф про ін’єкцію залежності: Посилання передає Мішко Хевері з Google, який досить сильно займається тестуванням та ін’єкцією залежностей. Він каже, що ведення журналу є своєрідним винятком, коли з одиничним сценарієм все гаразд, якщо вам не потрібно перевірити вихідні дані реєстратора (у цьому випадку використовуйте DI).
Користувач

Відповіді:


35

Це теж дратує - це не СУХО

Це правда. Але ви можете зробити так багато для наскрізних проблем, які охоплюють усі ваші типи. Ви повинні використовувати реєстратор скрізь, тому ви повинні мати властивість для цих типів.

Тож давайте подивимося, що ми можемо з цим зробити.

Сінглтон

Одинокі страшні <flame-suit-on>.

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

Але якщо одинаки заощадять вам значний час, включаючи весь процес рефакторингу, який вам коли-небудь доведеться зробити (час кришталевої кулі!), Я думаю, ви могли б жити з ними. Якщо коли-небудь було використано Синглтона, це може бути саме цим. Майте на увазі, якщо ви коли-небудь захочете передумати, вартість буде приблизно такою ж високою, наскільки вона буде.

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

Є й інші альтернативи, які можуть працювати так само добре без особливих компромісів, тому спочатку слід їх перевірити.

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

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

Використання AOP для СУШЕННЯ

Ви можете усунути трохи цього коду введення властивостей, використовуючи структуру Aspect Oriented Programming (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 років у костюмі полум’я, але, сподіваюся, це допоможе.
Бреннен Спрімонт

39

Я поміщаю екземпляр реєстратора в мій контейнер для введення залежностей, який потім вводить реєстратор у класи, яким він потрібен.


8
Ви можете бути більш конкретним? Тому що я думаю, що це те, що я робив / робив у 1 і 2 зразках коду, про які йдеться?
Гедріус

2
Клас "використання" виглядав би в основному так само, як і той, який ви вже маєте. Однак фактичне значення не потрібно вручати вашому класу вручну, оскільки контейнер DI робить це за вас. Це значно полегшує заміну реєстратора в тесті порівняно із наявністю статичного одиночного реєстратора.
Даніель Роуз

2
Не голосуючи, хоча це правильна відповідь (ІМО), оскільки ОП вже заявила, що вони це роблять. Крім того, яке ваше обгрунтування для використання цього проти використання Singleton? Що стосується проблем, що виникають у ОП з повторюваним кодом - як це вирішується цією відповіддю?
Merlyn Morgan-Graham

1
@Merlyn ОП заявив, що він має реєстратор як частину конструктора або як публічну власність. Він не заявив, що має контейнер DI, щоб ввести правильне значення. Тож я припускаю, що це не так. Так, є деякий повторюваний код (але з віднесеною властивістю auto, наприклад, це дуже мало), але IMHO набагато краще явно показати залежність, ніж приховувати її за допомогою (глобального) сингтона.
Даніель Роуз

1
@DanielRose: І ви передумали: "набагато краще явно показати залежність, ніж приховувати її через (глобальний) синглтон". +1
Мерлін Морган-Грем

25

Гарне питання. Я вважаю, що в більшості проектів реєстратор є одностороннім.

Деякі ідеї мені просто спадають на думку:

  • Використовуйте ServiceLocator (або інший контейнер інжекції залежностей, якщо ви вже використовуєте такий), який дозволяє вам ділитися реєстратором між службами / класами, таким чином, ви можете створити екземпляр реєстратора або навіть декількох різних реєстраторів та надавати спільний доступ через ServiceLocator, що, очевидно, було б єдиним , якась інверсія контролю . Цей підхід надає вам велику гнучкість у процесі створення та створення ініціалізації реєстратора.
  • Якщо вам потрібно Logger майже всюди - реалізувати методи розширення для Objectтипу , так що кожен клас буде мати можливість викликати методи лісоруба як LogInfo(), LogDebug(),LogError()

Чи можете ви бути більш конкретними, що ви мали на увазі, говорячи про методи розширення? Щодо використання ServiceLocator, мені цікаво, чому це було б краще, ніж введення властивостей, як у зразку 2.
Гедріус

1
@Giedrius: Щодо методів розширення - ви можете створити методи розширення, наприклад, public static void LogInfo(this Object instance, string message)щоб кожен клас його взяв , Що стосується ServiceLocator- це дозволяє вам мати реєстратор як звичайний екземпляр класу, а не синглтон, тому ви надасте велику гнучкість
sll

@Giedrius, якщо ви впроваджуєте IoC шляхом введення конструктора, і використовуваний вами фреймворк DI має можливість сканувати ваші конструктори та автоматично вводити ваші залежності (враховуючи, що ви налаштували ці залежності у процесі завантаження вашої фреймворкової системи DI), тоді як раніше зробити це могло б спрацювати чудово. Також більшість фреймворків DI повинні дозволяти вам встановлювати обсяг життєвого циклу кожного об'єкта.
GR7

7
ServiceLocator не є DI-контейнером. І це вважається антивізером.
Piotr Perak

1
OP вже використовує DI з контейнером DI. Перший приклад - це вприскування ctor, другий - вприскування властивостей. Перегляньте їх редагування.
Merlyn Morgan-Graham

16

Сінглтон - це гарна ідея. Ще кращою ідеєю є використання шаблону реєстру , який дає трохи більше контролю над створенням екземплярів. На мою думку, синглтон-шаблон є занадто близьким до глобальних змінних. Завдяки створенню або повторному використанню реєстру, який обробляє реєстр, є місце для майбутніх змін правил створення екземплярів.

Сам реєстр може бути статичним класом для надання простого синтаксису доступу до журналу:

Registry.Logger.Info("blabla");

6
Вперше я зіткнувся з шаблоном реєстру. Чи занадто спрощеним є твердження, що в основному всі глобальні змінні та функції покладені під одну парасольку?
Джоел Гудвін,

У найпростішій формі це просто парасолька, але наявність його в центральному місці дозволяє змінити його на щось інше (можливо, пул об’єктів, екземпляр за використання, локальний екземпляр потоку), коли вимоги змінюються.
Андерс Абель

5
Реєстр та ServiceLocator - це майже одне і те ж. Більшість фреймворків IoC є своєю суттю реалізацією реєстру.
Michael Brown

1
@MikeBrown: Здається, різниця може полягати в тому, що контейнер DI (внутрішньо, шаблон локатора служби), як правило, є екземпляром класу, тоді як шаблон реєстру використовує статичний клас. Це правильно звучить?
Merlyn Morgan-Graham

1
@MichaelBrown Різниця полягає в тому, де ви використовуєте локатор реєстру / служби. Якщо справа просто в корені композиції, це нормально; якщо ви використовуєте в багатьох місцях, це погано.
Онур,

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

Це дозволяє мені створити FilteringLogFactoryабо просто 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в корені програми, і я не знаю чому ви це скинули)
Мерлін Морган-Грем

1
@ MerlynMorgan-Graham: Навряд чи ви зміните завод після того, як встановите його. Будь-які модифікації будуть зроблені на заводі-реалізаторі, і ви отримаєте повний контроль над цим. Я б не рекомендував цей шаблон як універсальне рішення для одиноких, але він добре працює для заводів, оскільки їх API рідко змінюється. (щоб ви могли це назвати проксі-анотацією абстрактного фабричного синглтона, хе-хе, візерунок)
jgauffin

3

У .NET є книга Ін’єкція залежності. Виходячи з того, що вам потрібно, вам слід використовувати перехоплення.

У цій книзі є схема, яка допомагає вирішити, чи використовувати інжектор конструктора, введення властивостей, введення методу, зовнішній контекст, перехоплення.

Ось одні з причин використання цієї діаграми:

  1. У вас є залежність чи вона вам потрібна? - Потрібно це
  2. Це наскрізна стурбованість? - Так
  3. Вам потрібна відповідь із нього? - Ні

Використовуйте перехоплення


Я думаю, що в першому міркуванні є помилка (я думаю, це має бути "Чи є у вас залежність чи вона потрібна?"). У будь-якому випадку, +1 за згадку про Перехоплення, вивчення його можливостей у Віндзорі, і це виглядає дуже цікаво. Якби ви могли додатково навести приклад того, як ви уявляєте, що він тут пасував би, було б чудово.
Гедрій

+1; Цікава пропозиція - в основному надасть функціональність, схожу на AOP, без необхідності торкатися типів. Моя думка (яка базується на дуже обмеженій експозиції), перехоплення шляхом генерації проксі (типу, який може надати бібліотека DI на відміну від бібліотеки на основі атрибутів AOP) здається чорною магією і може дещо ускладнити розуміння того, що є продовжувати. Я розумію, як це працює неправильно? Хтось мав інший досвід із цим? Це менш страшно, ніж це звучить?
Merlyn Morgan-Graham

2
Якщо ви відчуваєте, що перехоплення є великою "магією", ви можете просто використати шаблон дизайну декоратора та реалізувати його самостійно. Але після цього ви, мабуть, зрозумієте, що це марна трата часу.
Piotr Perak

Я припускав, що ця пропозиція буде автоматично обертати кожен виклик в журналі, на відміну від дозволу (створення) самого журналу класу. Це правильно?
Мерлін Морган-Грем

Так. Ось що робить декоратор.
Piotr Perak

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 # не є 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")в програмі, більш складній, ніж привіт світ. Зазвичай ви виконуєте досить багато зразків для ініціалізації реєстратора - налаштування форматування, рівня, налаштування обробників файлів та консолі. Це ніколи ніколи logging.info("my message")!
Хрещений батько
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.