¿Es una buena práctica tener logger como singleton?

81

Tenía la costumbre de pasar el registrador al constructor, como:

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

Pero eso es bastante molesto, así que lo he usado como una propiedad durante algún tiempo:

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

Esto también se está volviendo molesto, no está seco, necesito repetir esto en todas las clases. Podría usar la clase base, pero, de nuevo, estoy usando la clase Form, por lo que necesitaría FormBase, etc. Así que creo que, ¿cuál sería la desventaja de tener un singleton con ILogger expuesto, para que cualquiera sepa dónde obtener el registrador?

    Infrastructure.Logger.Info("blabla");

ACTUALIZACIÓN: Como Merlyn notó correctamente, debo mencionar que en el primer y segundo ejemplo estoy usando DI.

Giedrius
fuente
1
Por las respuestas que estoy viendo, parece que la gente no se dio cuenta de que está inyectando en ambos ejemplos. ¿Puede aclarar esto en la pregunta? He agregado etiquetas para encargarme de algo.
Merlyn Morgan-Graham
1
Lea los comentarios de este artículo Mito de inyección de dependencia: Aprobación de referencias por Miško Hevery de Google, que es bastante aficionado a las pruebas y la inyección de dependencia. Él dice que el registro es una especie de excepción en la que un singleton está bien a menos que necesite probar la salida del registrador (en cuyo caso use DI).
Usuario

Respuestas:

35

Esto también se está volviendo molesto, no está SECO

Es verdad. Pero hay mucho que puede hacer por una preocupación transversal que impregna todos los tipos que tiene. Usted tiene que utilizar el registrador en todas partes, por lo que debe tener la propiedad de esos tipos.

Así que veamos qué podemos hacer al respecto.

único

Los singleton son terribles <flame-suit-on>.

Recomiendo seguir con la inyección de propiedades como lo hizo con su segundo ejemplo. Este es el mejor factoring que puede hacer sin recurrir a la magia. Es mejor tener una dependencia explícita que ocultarla mediante un singleton.

Pero si los singleton le ahorran un tiempo significativo, incluida toda la refactorización que tendrá que hacer (¡tiempo de bola de cristal!), Supongo que podría vivir con ellos. Si alguna vez hubo un uso para un Singleton, este podría ser. Tenga en cuenta que el costo si alguna vez desea cambiar de opinión será tan alto como sea posible.

Si hace esto, revise las respuestas de otras personas usando el Registrypatrón (vea la descripción) y aquellos que registran una fábrica singleton (reiniciable) en lugar de una instancia de registrador singleton.

Hay otras alternativas que podrían funcionar igual de bien sin tanto compromiso, por lo que debe verificarlas primero.

Fragmentos de código de Visual Studio

Puede usar fragmentos de código de Visual Studio para acelerar la entrada de ese código repetitivo. Podrás escribir algo como loggertab, y el código aparecerá mágicamente para ti.

Usando AOP para SECAR

Puede eliminar un poco de ese código de inyección de propiedad utilizando un marco de programación orientada a aspectos (AOP) como PostSharp para generar automáticamente parte de él.

Podría verse algo así cuando haya terminado:

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

También puede usar su código de muestra de seguimiento de métodos para rastrear automáticamente el código de entrada y salida del método, lo que podría eliminar la necesidad de agregar algunas de las propiedades del registrador todas juntas. Puede aplicar el atributo a nivel de clase o en todo el espacio de nombres:

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

// or

#if DEBUG
[assembly: Trace( AttributeTargetTypes = "MyNamespace.*",
    AttributeTargetTypeAttributes = MulticastAttributes.Public,
    AttributeTargetMemberAttributes = MulticastAttributes.Public )]
#endif
Merlyn Morgan-Graham
fuente
3
+1 para Singletons son terribles. Intente usar singleton con múltiples cargadores de clases, es decir, un entorno de servidor de aplicaciones donde el despachador es el cliente donde he experimentado ejemplos horribles de doble registro (pero no tan horribles como declaraciones de registro que provienen del lugar equivocado del que me habló un programador de C ++)
Niklas R.
1
@NickRosencrantz +1 para la historia de terror de C ++; Me encanta pasar un par de minutos contemplando lo terrible de los singletons solo para desplazarme hacia abajo y decir "Jaja jajaja, al menos no tengo sus problemas".
Domenic
32
</flame-suit-on> No sé cómo viviste con un traje de fuego durante 5 años, pero espero que esto te ayude.
Brennen Sprimont
39

Pongo una instancia de registrador en mi contenedor de inyección de dependencia, que luego inyecta el registrador en las clases que lo necesitan.

Daniel Rose
fuente
8
¿Podrías ser más específico? Porque creo que eso es lo que hice / hice en 1 y 2 ejemplos de código en cuestión.
Giedrius
2
La clase "using" se vería básicamente igual a la que ya tienes. El valor real, sin embargo, no tiene que ser dado manualmente a su clase, ya que el contenedor DI lo hace por usted. Esto hace que sea mucho más fácil reemplazar el registrador en una prueba, en comparación con tener un registrador singleton estático.
Daniel Rose
2
No votar a favor, aunque esta es la respuesta correcta (OMI), porque el OP ya dijo que estaban haciendo esto. Además, ¿cuál es su razón fundamental para usar esto frente a usar un Singleton? ¿Qué pasa con los problemas que tiene el OP con el código repetitivo? ¿Cómo se aborda eso con esta respuesta?
Merlyn Morgan-Graham
1
@Merlyn El OP declaró que tiene un registrador como parte del constructor o como una propiedad pública. No indicó que tiene un contenedor DI inyectado con el valor correcto. Así que supongo que este no es el caso. Sí, hay un código repetido (pero con una propiedad automática atribuida, por ejemplo, es muy poco), pero en mi humilde opinión, es mucho mejor mostrar explícitamente la dependencia que ocultarla a través de un singleton (global).
Daniel Rose
1
@DanielRose: Y has cambiado de opinión con "es mucho mejor mostrar explícitamente la dependencia que ocultarla mediante un singleton (global)". +1
Merlyn Morgan-Graham
25

Buena pregunta. Creo que en la mayoría de los proyectos el registrador es un singleton.

Algunas ideas me vienen a la mente:

  • Use ServiceLocator (u otro contenedor de inyección de dependencia si ya está usando alguno) que le permite compartir el registrador entre los servicios / clases, de esta manera puede crear una instancia del registrador o incluso varios registradores diferentes y compartir a través de ServiceLocator que obviamente sería un singleton , algún tipo de Inversión de Control . Este enfoque le brinda mucha flexibilidad sobre un proceso de creación de instancias e inicialización del registrador.
  • Si necesita registrador en casi todas partes - poner en práctica los métodos de extensión para el Objecttipo de modo que cada clase sería capaz de llamar a los métodos del registrador gusta LogInfo(), LogDebug(),LogError()
sll
fuente
¿Podrías ser más específico, qué tenías en mente hablando de métodos de extensión? Con respecto al uso de ServiceLocator, me pregunto por qué sería mejor que la inyección de propiedades, como en la muestra 2.
Giedrius
1
@Giedrius: con respecto a los métodos de extensión, puede crear métodos de extensión como public static void LogInfo(this Object instance, string message)para que cada clase los recoja, con respecto a ServiceLocator: esto le permite tener el registrador como una instancia de clase regular, no como un singleton, por lo que le daría mucha flexibilidad
sll
@Giedrius si implementa IoC por inyección de constructor y el marco DI que está utilizando tiene la capacidad de escanear sus constructores e inyectar automáticamente sus dependencias (dado que configuró esas dependencias en su proceso de arranque de marco DI), entonces la forma en que solía hacerlo funcionaría bien. Además, la mayoría de los marcos de DI deberían permitirle establecer el alcance del ciclo de vida de cada objeto.
GR7
7
ServiceLocator no es un contenedor DI. Y se considera anti patrón.
Piotr Perak
1
El OP ya está usando DI con un contenedor DI. El primer ejemplo es la inyección de ctor, el segundo es la inyección de propiedades. Vea su edición.
Merlyn Morgan-Graham
16

Un singleton es una buena idea. Una idea aún mejor es usar el patrón de registro , que brinda un poco más de control sobre la creación de instancias. En mi opinión, el patrón singleton está demasiado cerca de las variables globales. Con un registro que maneja la creación o reutilización de objetos, hay espacio para cambios futuros en las reglas de creación de instancias.

El registro en sí puede ser una clase estática para proporcionar una sintaxis simple para acceder al registro:

Registry.Logger.Info("blabla");
Anders Abel
fuente
6
La primera vez que me encuentro con el patrón de registro. ¿Es una simplificación excesiva decir que básicamente todas las variables y funciones globales están bajo un mismo paraguas?
Joel Goodwin
En su forma más simple, es solo un paraguas, pero tenerlo en un lugar central hace posible cambiarlo a otra cosa (tal vez un grupo de objetos, instancia por uso, instancia local de subproceso) cuando cambian los requisitos.
Anders Abel
5
Registry y ServiceLocator son prácticamente lo mismo. La mayoría de los marcos de IoC son, en esencia, una implementación del Registro.
Michael Brown
1
@MikeBrown: Parece que la diferencia podría ser que un contenedor DI (internamente, patrón de localizador de servicios) tiende a ser una clase instanciada, mientras que el patrón de registro usa una clase estática. ¿Eso suena bien?
Merlyn Morgan-Graham
1
@MichaelBrown La diferencia es dónde usa el localizador de registro / servicio. Si está solo en la raíz de la composición, está bien; si lo usa en muchos lugares, es malo.
Onur
10

Un singleton simple no es una buena idea. Hace que sea difícil reemplazar el registrador. Tiendo a usar filtros para mis registradores (algunas clases "ruidosas" solo pueden registrar advertencias / errores).

Utilizo el patrón singleton combinado con el patrón proxy para la fábrica de registradores:

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

Esto me permite crear un FilteringLogFactoryo simplemente un SimpleFileLogFactorysin cambiar ningún código (y por lo tanto cumplir con el principio Abierto / Cerrado).

Extensión de muestra

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

Y usar la nueva fábrica

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

En su clase que debería registrar:

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

    public void SomeMethod()
    {
        _logger.Debug("Welcome world!");
    }
}
jgauffin
fuente
No importa mi comentario anterior. Creo que veo lo que estás haciendo aquí. ¿Puede proporcionar un ejemplo de una clase que realmente se registre en esto?
Merlyn Morgan-Graham
+1; Definitivamente supera a un singleton desnudo :) Todavía tiene un ligero acoplamiento y potencial para problemas de alcance estático / subprocesos debido al uso de objetos estáticos, pero me imagino que son raros (querría establecer Instanceen la raíz de la aplicación y no sé por qué lo restablecería)
Merlyn Morgan-Graham
1
@ MerlynMorgan-Graham: Es poco probable que cambie una fábrica después de configurarla. Cualquier modificación se haría en la fábrica de implementación y usted tendrá el control total sobre eso. No recomendaría este patrón como una solución universal para singleton, pero funciona bien para las fábricas ya que su API rara vez cambia. (por lo que podría llamarlo singleton de fábrica abstracta proxiado, jeje,
combinación de
3

Hay un libro Inyección de dependencias en .NET. Según lo que necesite, debe utilizar la intercepción.

En este libro hay un diagrama que ayuda a decidir si usar la inyección de constructor, inyección de propiedad, inyección de método, contexto ambiental, intercepción.

Así es como se razona usando este diagrama:

  1. ¿Tienes dependencia o la necesitas? - Necesito
  2. ¿Es una preocupación transversal? - Si
  3. ¿Necesitas una respuesta? - No

Usar intercepción

Piotr Perak
fuente
Creo que hay un error en la primera pregunta de razonamiento (creo que debería ser "¿Tiene dependencia o la necesita?"). De todos modos, +1 por mencionar Interception, estudiar sus posibilidades en Windsor y parece muy interesante. Si además pudiera dar un ejemplo de cómo imagina que encajaría aquí, sería genial.
Giedrius
+1; Sugerencia interesante: básicamente daría una funcionalidad similar a AOP sin tener que tocar los tipos. Mi opinión (que se basa en una exposición muy limitada) es que la interceptación a través de la generación de proxy (el tipo que una biblioteca DI puede proporcionar en lugar de una biblioteca basada en atributos AOP) se siente como magia negra y puede hacer que sea algo difícil de entender qué es pasando. ¿Mi entendimiento sobre cómo funciona esto es incorrecto? ¿Alguien tuvo una experiencia diferente con eso? ¿Da menos miedo de lo que parece?
Merlyn Morgan-Graham
2
Si sientes que Interception es demasiado "mágico", entonces puedes usar el patrón de diseño del decorador e implementarlo tú mismo. Pero después de eso probablemente se dará cuenta de que es una pérdida de tiempo.
Piotr Perak
Supuse que esta sugerencia envolvería automáticamente cada llamada en el registro, en lugar de dejar (hacer) que la clase se registre. ¿Es eso correcto?
Merlyn Morgan-Graham
Si. Eso es lo que hace el decorador.
Piotr Perak
0

Otra solución que personalmente encuentro la más fácil es usar una clase Logger estática. Puede llamarlo desde cualquier método de clase sin tener que cambiar la clase, por ejemplo, agregar una inyección de propiedad, etc. Es bastante simple y fácil de usar.

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
fuente
-1

Si desea buscar una buena solución para el registro, le sugiero que mire el motor de aplicaciones de Google con Python, donde el registro es tan simple como import loggingy luego puede simplemente logging.debug("my message")o logging.info("my message")que realmente lo mantiene tan simple como debería.

Java no tenía una buena solución para el registro, es decir, debe evitarse log4j, ya que prácticamente lo obliga a usar singletons que, como se responde aquí, es "terrible" y he tenido una experiencia horrible al tratar de hacer que la salida de registro sea la misma declaración de registro solo una vez cuando sospecho que el motivo del doble registro fue que tengo un Singleton del objeto de registro en dos cargadores de clases en la misma máquina virtual (!)

Le pido perdón por no ser tan específico para C #, pero por lo que he visto, las soluciones con C # se ven similares a Java donde teníamos log4j y también deberíamos convertirlo en un singleton.

Por eso me gustó mucho la solución con GAE / python , es tan simple como puede ser y no tiene que preocuparse por los cargadores de clases, obtener una declaración de registro doble o cualquier patrón de diseño en absoluto.

Espero que parte de esta información pueda ser relevante para usted y espero que desee echar un vistazo a la solución de registro que recomiendo, en lugar de eso, intimido sobre la cantidad de problemas que se sospecha de Singleton debido a la imposibilidad de tener un singleton real cuando debe estar instanciando en varios cargadores de clases.

Niklas R.
fuente
¿No es el código equivalente en C # un Singleton (o una clase estática, que es potencialmente la misma, potencialmente peor, ya que ofrece incluso menos flexibilidad)? using Logging; /* ... */ Logging.Info("my message");
Merlyn Morgan-Graham
Y puede evitar singletons con el patrón log4j si usa la inyección de dependencia para inyectar sus registradores. Muchas bibliotecas hacen esto. También puede utilizar envoltorios que proporcionan interfaces genéricas para que pueda cambiar su implementación de registro en una fecha posterior, como netcommon.sourceforge.net o una de las extensiones proporcionadas por bibliotecas de contenedores DI como Castle.Windsor o Ninject
Merlyn Morgan-Graham
¡Ese es el problema! Nadie nunca usa logging.info("my message")en un programa más complicado que hola mundo. Por lo general, se realizan muchas tareas repetidas al inicializar el registrador: configurar el formateador, el nivel, configurar los controladores de archivos y de consola. ¡Nunca jamás logging.info("my message")!
El Padrino