Czy dobrą praktyką jest posiadanie loggera jako singletona?

81

Miałem zwyczaj przekazywania loggera do konstruktora, na przykład:

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

Ale to dość denerwujące, więc od jakiegoś czasu używam tej właściwości:

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

To też robi się irytujące - nie wysycha, muszę to powtarzać na każdych zajęciach. Mógłbym użyć klasy bazowej, ale z drugiej strony - używam klasy Form, więc potrzebowałbym FormBase itp. Myślę więc, jaka byłaby wada posiadania singletona z ujawnionym ILoggerem, więc każdy wiedziałby, skąd wziąć rejestrator:

    Infrastructure.Logger.Info("blabla");

AKTUALIZACJA: Jak słusznie zauważył Merlyn, powinienem wspomnieć, że w pierwszym i drugim przykładzie używam DI.

Giedrius
źródło
1
Z odpowiedzi, które widzę, wynika, że ​​ludzie nie zdawali sobie sprawy, że wstrzykujesz w obu tych przykładach. Czy możesz to wyjaśnić w pytaniu? Dodałem tagi, aby zająć się niektórymi z nich.
Merlyn Morgan-Graham
1
Przeczytaj komentarze do tego artykułu Dependency Injection Myth: Reference Passing autorstwa Miško Hevery z Google, który jest bardzo zaangażowany w testowanie i wstrzykiwanie zależności. Mówi, że rejestrowanie jest rodzajem wyjątku, w którym singleton jest w porządku, chyba że musisz przetestować wyjście rejestratora (w takim przypadku użyj DI).
Użytkownik

Odpowiedzi:

35

To też staje się denerwujące - nie jest SUCHE

To prawda. Ale tylko tyle możesz zrobić dla przekrojowego problemu, który przenika każdy twój typ. Musisz używać loggera wszędzie, więc musisz mieć właściwość na tych typach.

Zobaczmy więc, co możemy z tym zrobić.

Singel

Singletony są okropne <flame-suit-on>.

Zalecam trzymać się wstrzykiwania właściwości, tak jak zrobiłeś to na drugim przykładzie. To najlepszy faktoring, jaki możesz zrobić bez uciekania się do magii. Lepiej jest mieć wyraźną zależność niż ukrywać ją za pomocą singletona.

Ale jeśli singletony zaoszczędzą ci dużo czasu, w tym wszystkie refaktoryzacje, które będziesz musiał kiedykolwiek wykonać (czas kryształowej kuli!), Przypuszczam, że możesz z nimi żyć. Jeśli Singleton kiedykolwiek miałby zastosowanie, to może być to. Pamiętaj, że koszt, jeśli kiedykolwiek zechcesz zmienić zdanie, będzie tak wysoki, jak to tylko możliwe.

Jeśli to zrobisz, sprawdź odpowiedzi cudzych korzystania z Registrywzoru (patrz opis), a ci rejestracji (Reset możliwy) singleton fabryki zamiast instancji Singleton rejestratora.

Istnieją inne alternatywy, które mogą działać równie dobrze bez większego kompromisu, więc powinieneś je najpierw sprawdzić.

Fragmenty kodu programu Visual Studio

Możesz użyć fragmentów kodu programu Visual Studio, aby przyspieszyć wprowadzanie tego powtarzalnego kodu. Będziesz mógł wpisać coś takiego loggertab, a kod magicznie pojawi się dla Ciebie.

Używanie AOP do SUSZENIA

Możesz wyeliminować trochę tego kodu iniekcji właściwości, używając struktury programowania zorientowanego na aspekty (AOP), takiej jak PostSharp, aby automatycznie wygenerować część z nich.

Kiedy skończysz, może to wyglądać mniej więcej tak:

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

Możesz również użyć ich przykładowego kodu śledzenia metod, aby automatycznie śledzić kod wejścia i wyjścia metody, co może wyeliminować potrzebę dodawania wszystkich właściwości programu rejestrującego. Możesz zastosować atrybut na poziomie klasy lub całej przestrzeni nazw:

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

// or

#if DEBUG
[assembly: Trace( AttributeTargetTypes = "MyNamespace.*",
    AttributeTargetTypeAttributes = MulticastAttributes.Public,
    AttributeTargetMemberAttributes = MulticastAttributes.Public )]
#endif
Merlyn Morgan-Graham
źródło
3
+1 dla singletonów są straszne. Spróbuj użyć singletona z wieloma programami ładującymi klasy, tj. Środowisko serwera aplikacji, w którym dyspozytor jest klientem, w którym doświadczyłem okropnych przykładów podwójnego logowania (ale nie tak strasznych, jak instrukcje logowania pochodzące z niewłaściwego miejsca, o którym powiedział mi jakiś programista C ++)
Niklas R.
1
@NickRosencrantz +1 dla horroru C ++; Uwielbiam spędzać kilka minut na kontemplowaniu okropności singletonów tylko po to, by przewinąć w dół i powiedzieć „Haha lol przynajmniej nie mam ich problemów”.
Domenic
32
</flame-suit-on> Nie wiem, jak żyłeś w kombinezonie płomieni przez 5 lat, ale mam nadzieję, że to pomoże.
Brennen Sprimont
39

Umieszczam wystąpienie rejestratora w moim kontenerze iniekcji zależności, który następnie wstrzykuje rejestrator do klas, które go potrzebują.

Daniel Rose
źródło
8
możesz być bardziej dokładny? Ponieważ myślę, że to właśnie zrobiłem / zrobiłem w przypadku 1 i 2 próbek kodu?
Giedrius
2
Klasa „using” wyglądałaby zasadniczo tak samo, jak ta, którą już masz. Jednak rzeczywista wartość nie musi być ręcznie podawana Twojej klasie, ponieważ kontener DI robi to za Ciebie. To znacznie ułatwia wymianę rejestratora w teście, w porównaniu z posiadaniem statycznego pojedynczego rejestratora.
Daniel Rose,
2
Nie popieram, mimo że jest to właściwa odpowiedź (IMO), ponieważ PO już powiedział, że to robią. Jakie jest twoje uzasadnienie dla używania tego w porównaniu z używaniem Singletona? A co z problemami, które OP ma z powtarzającym się kodem - jak rozwiązuje to ta odpowiedź?
Merlyn Morgan-Graham
1
@Merlyn OP stwierdził, że ma rejestrator jako część konstruktora lub jako własność publiczną. Nie stwierdził, że ma pojemnik DI wstrzykuje prawidłową wartość. Więc zakładam, że tak nie jest. Tak, istnieje jakiś powtarzający się kod (ale z przypisaną właściwością auto, na przykład jest to bardzo mało), ale IMHO znacznie lepiej jest jawnie pokazać zależność niż ukryć ją za pomocą (globalnego) singletona.
Daniel Rose
1
@DanielRose: I zmieniłeś zdanie, mówiąc: „O wiele lepiej jest jawnie pokazać zależność niż ukryć ją za pomocą (globalnego) singletona”. +1
Merlyn Morgan-Graham
25

Dobre pytanie. Uważam, że w większości projektów rejestrator jest singletonem.

Przychodzi mi do głowy kilka pomysłów:

  • Użyj ServiceLocator (lub innego kontenera Dependency Injection, jeśli już go używasz), który pozwala na udostępnianie rejestratora w usługach / klasach, w ten sposób możesz utworzyć instancję rejestratora lub nawet wiele różnych rejestratorów i udostępniać za pośrednictwem ServiceLocator, który oczywiście byłby singletonem , jakiś rodzaj Odwrócenia Kontroli . Takie podejście zapewnia dużą elastyczność w zakresie tworzenia instancji rejestratora i procesu inicjalizacji.
  • Jeśli potrzebujesz logger niemal wszędzie - wdrożenie metody rozszerzenie dla Objecttypu więc każda klasa będzie w stanie wywołać metody rejestratora podoba LogInfo(), LogDebug(),LogError()
sll
źródło
Czy mógłbyś być bardziej szczegółowy, co miałeś na myśli, mówiąc o metodach rozszerzających? Jeśli chodzi o użycie ServiceLocator, zastanawiam się, dlaczego byłoby lepsze niż zastrzyk właściwości, jak w próbce 2.
Giedrius
1
@Giedrius: Jeśli chodzi o metody rozszerzające - możesz tworzyć metody rozszerzające w taki public static void LogInfo(this Object instance, string message)sposób, aby każda klasa je odebrała, Odnośnie ServiceLocator- pozwala to na posiadanie loggera jako zwykłej instancji klasy, a nie singletona, więc dałbyś dużą elastyczność
sll
@Giedrius, jeśli implementujesz IoC przez iniekcję konstruktora, a używana struktura DI ma możliwość skanowania twoich konstruktorów i automatycznego wstrzykiwania twoich zależności (zakładając, że skonfigurowałeś te zależności w procesie bootstrap struktury DI), wtedy sposób, w jaki zrób to działałoby dobrze. Ponadto większość frameworków DI powinna umożliwiać ustawienie zakresu cyklu życia każdego obiektu.
GR7
7
ServiceLocator nie jest DI Container. I jest uważany za anty-wzór.
Piotr Perak
1
OP używa już DI z kontenerem DI. Pierwszy przykład to wstrzyknięcie ctor, drugi to wstrzyknięcie właściwości. Zobacz ich edycję.
Merlyn Morgan-Graham
16

Singleton to dobry pomysł. Jeszcze lepszym pomysłem jest użycie wzorca Registry , który daje nieco większą kontrolę nad instancjami. Moim zdaniem wzorzec singletona jest zbyt blisko zmiennych globalnych. Dzięki rejestrowi obsługującemu tworzenie lub ponowne wykorzystywanie obiektów jest miejsce na przyszłe zmiany reguł tworzenia instancji.

Sam rejestr może być klasą statyczną, która zapewnia prostą składnię dostępu do dziennika:

Registry.Logger.Info("blabla");
Anders Abel
źródło
6
Pierwszy raz natknąłem się na wzorzec rejestru. Czy stwierdzenie, że w zasadzie wszystkie globalne zmienne i funkcje są umieszczone pod jednym parasolem, jest zbytnim uproszczeniem?
Joel Goodwin
W swojej najprostszej formie jest to tylko parasol, ale umieszczenie go w centralnym miejscu umożliwia zmianę go na coś innego (np. Pulę obiektów, instancję na użycie, instancję lokalną wątku), gdy zmieniają się wymagania.
Anders Abel
5
Registry i ServiceLocator to jedno i to samo. Większość frameworków IoC jest w swej istocie implementacją Rejestru.
Michael Brown
1
@MikeBrown: Wydaje się, że różnica może polegać na tym, że kontener DI (wewnętrznie, wzorzec lokalizatora usług) jest zwykle klasą instancji, podczas gdy wzorzec rejestru używa klasy statycznej. Czy to brzmi dobrze?
Merlyn Morgan-Graham
1
@MichaelBrown Różnica polega na tym, gdzie używasz lokalizatora rejestru / usługi. Jeśli znajduje się tylko w katalogu głównym kompozycji, wszystko jest w porządku; jeśli używasz w wielu miejscach, jest źle.
Onur
10

Zwykły singleton nie jest dobrym pomysłem. Utrudnia to wymianę rejestratora. Zwykle używam filtrów dla moich rejestratorów (niektóre „hałaśliwe” klasy mogą rejestrować tylko ostrzeżenia / błędy).

Używam wzorca singleton w połączeniu ze wzorcem proxy dla fabryki loggera:

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

To pozwala mi na tworzenie FilteringLogFactorylub po prostu SimpleFileLogFactorybez zmiany kodu (a zatem zgodnie z zasadą Open / Closed).

Przykładowe rozszerzenie

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

I do korzystania z nowej fabryki

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

W Twojej klasie, która powinna rejestrować:

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

    public void SomeMethod()
    {
        _logger.Debug("Welcome world!");
    }
}
jgauffin
źródło
Nieważne, mój poprzedni komentarz. Myślę, że rozumiem, co tu robisz. Czy możesz podać przykład klasy faktycznie logującej się do tego?
Merlyn Morgan-Graham
+1; Zdecydowanie bije nagiego singletona :) Nadal ma pewne niewielkie sprzężenie i potencjalne problemy z zakresem statycznym / wątkami z powodu używania obiektów statycznych, ale wyobrażam sobie, że są one rzadkie (chciałbyś ustawić Instancew katalogu głównym aplikacji i nie wiem dlaczego to zresetowałeś)
Merlyn Morgan-Graham
1
@ MerlynMorgan-Graham: Jest mało prawdopodobne, że zmienisz fabrykę po jej ustawieniu. Wszelkie modyfikacje byłyby dokonywane w fabryce wdrażającej i masz nad tym pełną kontrolę. Nie polecałbym tego wzorca jako uniwersalnego rozwiązania dla singletonów, ale działa dobrze w fabrykach, ponieważ ich API rzadko się zmienia. (więc można to nazwać proxied abstract factory singleton, hehe, pattern mash-up)
jgauffin
3

Istnieje książka Dependency Injection w .NET. W oparciu o to, czego potrzebujesz, powinieneś użyć przechwytywania.

W tej książce znajduje się diagram pomagający zdecydować, czy użyć iniekcji konstruktora, iniekcji właściwości, iniekcji metody, kontekstu otoczenia, przechwytywania.

Oto jeden powód używania tego diagramu:

  1. Czy jesteś uzależniony lub potrzebujesz tego? - Potrzebuję tego
  2. Czy jest to problem przekrojowy? - Tak
  3. Potrzebujesz odpowiedzi? - Nie

Użyj przechwytywania

Piotr Perak
źródło
Myślę, że w pierwszym pytaniu dotyczącym rozumowania jest błąd (myślę, że powinno brzmieć „Czy masz zależność lub potrzebujesz tego?”). W każdym razie +1 za wspomnienie o Przechwyceniu, zbadanie jego możliwości w Windsorze i wygląda to bardzo interesująco. Gdybyś mógł dodatkowo podać przykład, jak wyobrażasz sobie, że pasowałoby tutaj, byłoby świetnie.
Giedrius
+1; Ciekawa sugestia - zasadniczo zapewniłaby funkcjonalność podobną do AOP bez konieczności dotykania typów. Moim zdaniem (opartym na bardzo ograniczonej ekspozycji) przechwytywanie przez generowanie proxy (typ, który może zapewnić biblioteka DI w przeciwieństwie do biblioteki opartej na atrybutach AOP) wydaje się być czarną magią i może utrudniać zrozumienie, co to jest dziać się. Czy moje zrozumienie, jak to działa, jest nieprawidłowe? Czy ktoś miał z tym inne doświadczenia? Czy to mniej przerażające, niż się wydaje?
Merlyn Morgan-Graham
2
Jeśli uważasz, że Intercepcja jest zbyt „magiczna”, możesz po prostu użyć wzoru dekoratora i zaimplementować go samodzielnie. Ale potem prawdopodobnie zdasz sobie sprawę, że to strata czasu.
Piotr Perak
Założyłem, że ta sugestia automatycznie zapisze każde wywołanie w logowaniu, w przeciwieństwie do zezwalania (tworzenia) samego dziennika klasy. Czy to jest poprawne?
Merlyn Morgan-Graham
Tak. To właśnie robi dekorator.
Piotr Perak
0

Innym rozwiązaniem, które osobiście uważam za najłatwiejsze, jest użycie statycznej klasy Logger. Możesz go wywołać z dowolnej metody klasy bez konieczności zmiany klasy, np. Dodawania iniekcji właściwości itp. Jest dość prosty i łatwy w użyciu.

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
Erik Kalkoken
źródło
-1

Jeśli chcesz spojrzeć na dobre rozwiązanie do logowania, proponuję przyjrzeć się silnikowi aplikacji Google z Pythonem, gdzie logowanie jest tak proste, jak import loggingi wtedy możesz po prostu logging.debug("my message")lublogging.info("my message") który naprawdę sprawia, że ​​jest tak proste, jak powinno.

Java nie miała dobrego rozwiązania do rejestrowania, tj. Należy unikać log4j, ponieważ praktycznie zmusza cię to do używania singletonów, które, jak tu podano, są "okropne" i miałem okropne doświadczenie z próbą uczynienia wyjścia logowania tą samą instrukcją logowania tylko raz kiedy podejrzewam, że powodem podwójnego logowania było to, że mam jeden obiekt logowania w dwóch klasach ładujących w tej samej maszynie wirtualnej (!)

Przepraszam, że nie jest tak specyficzny dla C #, ale z tego, co widziałem, rozwiązania z C # wyglądają podobnie do Javy, gdzie mieliśmy log4j i powinniśmy również uczynić go singletonem.

Dlatego bardzo podobało mi się rozwiązanie z GAE / pythonem , jest tak proste, jak to tylko możliwe i nie musisz martwić się o ładowanie klas, otrzymywanie instrukcji podwójnego logowania lub w ogóle żadnego wzoru projektu.

Mam nadzieję, że niektóre z tych informacji mogą być dla Ciebie istotne i mam nadzieję, że zechcesz rzucić okiem na moje rozwiązanie do logowania, które polecam, zamiast tego zastraszam, jak duży problem podejrzewa się Singletona z powodu niemożności posiadania prawdziwego singletona, gdy musi być instancjowany w kilku programach ładujących.

Niklas R.
źródło
czy równoważny kod w C # nie jest singletonem (lub klasą statyczną, która jest potencjalnie taka sama, potencjalnie gorsza, ponieważ oferuje jeszcze mniejszą elastyczność)? using Logging; /* ... */ Logging.Info("my message");
Merlyn Morgan-Graham
I możesz uniknąć singletonów za pomocą wzorca log4j, jeśli użyjesz iniekcji zależności do wstrzyknięcia rejestratorów. Robi to wiele bibliotek. Możesz również użyć opakowań, które zapewniają ogólne interfejsy, dzięki czemu możesz zamienić implementację rejestrowania w późniejszym terminie, na przykład netcommon.sourceforge.net lub jedno z rozszerzeń udostępnianych przez biblioteki kontenerów DI, takie jak Castle.Windsor lub Ninject
Merlyn Morgan-Graham
To jest problem! Nikt nigdy nie używa logging.info("my message")w programie bardziej skomplikowanym niż Hello world. Zwykle wykonujesz wiele standardowych czynności inicjujących rejestrator - ustawiając program formatujący, poziom, konfigurując zarówno obsługę plików, jak i konsoli. To nigdy przenigdy logging.info("my message")!
The Godfather