A mis colegas les gusta decir "el registro / almacenamiento en caché / etc. es una preocupación transversal" y luego continúan usando el singleton correspondiente en todas partes. Sin embargo, aman IoC y DI.
¿Es realmente una excusa válida para romper el principio SOLI D ?
Respuestas:
No.
SOLID existe como pautas para dar cuenta del cambio inevitable. ¿ Realmente nunca va a cambiar su biblioteca de registro, o destino, o filtrado, o formateo, o ...? ¿ Realmente no va a cambiar su biblioteca de almacenamiento en caché, o su objetivo, o estrategia, o alcance, o ...?
Por supuesto que lo eres. Por lo menos , querrás burlarte de estas cosas de una manera sensata para aislarlas para la prueba. Y si va a querer aislarlos para la prueba, es probable que se encuentre con una razón comercial en la que desea aislarlos por razones de la vida real.
Y luego obtendrá el argumento de que el propio registrador manejará el cambio. "¡Oh, si el objetivo / filtrado / formato / estrategia cambia, entonces solo cambiaremos la configuración!" Eso es basura. Ahora no solo tiene un Objeto de Dios que maneja todas estas cosas, sino que está escribiendo su código en XML (o similar) donde no obtiene análisis estático, no obtiene errores de tiempo de compilación y no ' Realmente obtengo pruebas unitarias efectivas.
¿Hay casos para violar las pautas de SOLID? Absolutamente. A veces las cosas no cambiarán (sin requerir una reescritura completa de todos modos). A veces, una ligera violación de LSP es la solución más limpia. A veces, hacer una interfaz aislada no proporciona ningún valor.
Pero el registro y el almacenamiento en caché (y otras preocupaciones transversales ubicuas) no son esos casos. Por lo general, son excelentes ejemplos de los problemas de acoplamiento y diseño que obtienes cuando ignoras las pautas.
fuente
String
oInt32
inclusoList
fuera de su módulo. Solo hay una medida en que es razonable y sensato planificar un cambio. Y, más allá de los tipos "básicos" en su mayoría obvios, discernir lo que probablemente cambiarás es realmente solo una cuestión de experiencia y juicio.Sí
Este es el punto central del término "preocupación transversal": significa algo que no encaja perfectamente en el principio SÓLIDO.
Aquí es donde el idealismo se encuentra con la realidad.
Las personas semi-nuevas a SOLID y transversales a menudo se encuentran con este desafío mental. Está bien, no te asustes. Esfuércese por poner todo en términos de SOLID, pero hay algunos lugares como el registro y el almacenamiento en caché donde SOLID simplemente no tiene sentido. El corte transversal es el hermano de SOLID, van de la mano.
fuente
HttpContextBase
(que se introdujo exactamente por este motivo). Sé con certeza que mi realidad sería realmente amarga sin esta clase.Para iniciar sesión, creo que es. El registro es generalizado y generalmente no está relacionado con la funcionalidad del servicio. Es común y bien entendido utilizar el marco de registro de patrones únicos. Si no lo hace, está creando e inyectando registradores en todas partes y no quiere eso.
Un problema de lo anterior es que alguien dirá 'pero ¿cómo puedo probar el registro?' . Mis pensamientos son que normalmente no pruebo el registro, más allá de afirmar que realmente puedo leer los archivos de registro y comprenderlos. Cuando he visto el registro probado, generalmente es porque alguien necesita afirmar que una clase realmente ha hecho algo y están usando los mensajes de registro para obtener esa retroalimentación. Preferiría registrar algún oyente / observador en esa clase y afirmar en mis pruebas que eso se llama. Luego puede colocar su registro de eventos dentro de ese observador.
Sin embargo, creo que el almacenamiento en caché es un escenario completamente diferente.
fuente
Mis 2 centavos ...
Si y no.
Nunca deberías realmente violar los principios que adoptas; pero, sus principios siempre deben ser matizados y adoptados en servicio a un objetivo superior. Entonces, con una comprensión adecuadamente condicionada, algunas violaciones aparentes pueden no ser violaciones reales del "espíritu" o del "conjunto de principios en su conjunto".
Los principios SÓLIDOS en particular, además de requerir muchos matices, están en última instancia subordinados al objetivo de "entregar software funcional y mantenible". Por lo tanto, adherirse a cualquier principio SOLID en particular es contraproducente y contradictorio cuando hacerlo entra en conflicto con los objetivos de SOLID. Y aquí, a menudo me doy cuenta de que entregar la capacidad de mantenimiento triunfa .
Entonces, ¿qué pasa con la D en SÓLIDO ? Bueno, contribuye a una mayor capacidad de mantenimiento al hacer que su módulo reutilizable sea relativamente independiente de su contexto. Y podemos definir el "módulo reutilizable" como "una colección de código que planea usar en otro contexto distinto". Y eso se aplica a funciones individuales, clases, conjuntos de clases y programas.
Y sí, cambiar las implementaciones del registrador probablemente coloque su módulo en "otro contexto distinto".
Entonces, permítanme ofrecerles mis dos grandes advertencias :
Primero: Dibujar las líneas alrededor de los bloques de código que constituyen "un módulo reutilizable" es una cuestión de juicio profesional. Y su juicio se limita necesariamente a su experiencia.
Si actualmente no planea usar un módulo en otro contexto, probablemente esté bien que dependa indefensamente de él. La advertencia a la advertencia: sus planes probablemente estén equivocados, pero eso también está bien. Cuanto más tiempo escriba módulo tras módulo, más intuitivo y preciso será su sentido de si "necesitaré esto de nuevo algún día". Pero, probablemente nunca podrá decir retrospectivamente: "He modularizado y desacoplado todo en la mayor medida posible, pero sin excesos ".
Si se siente culpable por sus errores de juicio, confiese y continúe ...
En segundo lugar: invertir el control no equivale a inyectar dependencias .
Esto es especialmente cierto cuando comienzas a inyectar dependencias hasta la saciedad . La inyección de dependencia es una táctica útil para la estrategia global de IoC. Pero, diría que la DI es de menor eficacia que otras tácticas, como el uso de interfaces y adaptadores, puntos únicos de exposición al contexto desde dentro del módulo.
Y centrémonos realmente en esto por un segundo. Porque, incluso si inyecta una
Logger
náusea ad , necesita escribir código en laLogger
interfaz. No podría comenzar a usar una nuevaLogger
de un proveedor diferente que tome parámetros en un orden diferente. Esa capacidad proviene de la codificación, dentro del módulo, contra una interfaz que existe dentro del módulo y que tiene un solo submódulo (Adaptador) para administrar la dependencia.Y si está codificando contra un Adaptador, si el
Logger
inyectado en ese Adaptador o descubierto por el Adaptador generalmente es bastante insignificante para el objetivo general de mantenimiento. Y lo más importante, si tiene un adaptador de nivel de módulo, probablemente sea absurdo inyectarlo en cualquier cosa. Está escrito para el módulo.tl; dr : deja de preocuparte por los principios sin tener en cuenta por qué los estás utilizando. Y, más prácticamente, solo construya un
Adapter
para cada módulo. Use su criterio al decidir dónde dibuja los límites del "módulo". Desde cada módulo, siga adelante y consulte directamente elAdapter
. Y claro, inyecte el registrador real en elAdapter
, pero no en cada pequeña cosa que pueda necesitarlo.fuente
La idea de que el registro siempre debe implementarse como un singleton es una de esas mentiras que se ha dicho con tanta frecuencia que ha ganado fuerza.
Durante el tiempo que los sistemas operativos modernos se han ocupado, se ha reconocido que es posible que desee iniciar sesión en varios lugares dependiendo de la naturaleza de la salida .
Los diseñadores de sistemas deberían cuestionarse constantemente la eficacia de las soluciones pasadas antes de incluirlas ciegamente en otras nuevas. Si no están llevando a cabo tal diligencia, entonces no están haciendo su trabajo.
fuente
El registro genuino es un caso especial.
@Telastyn escribe:
Si anticipa que podría necesitar cambiar su biblioteca de registro, entonces debería estar usando una fachada; es decir, SLF4J si estás en el mundo de Java.
En cuanto al resto, una biblioteca de registro decente se encarga de cambiar dónde va el registro, qué eventos se filtran, cómo se formatean los eventos de registro utilizando archivos de configuración del registrador y (si es necesario) clases de complementos personalizados. Hay una serie de alternativas disponibles.
En resumen, estos son problemas resueltos ... para iniciar sesión ... y, por lo tanto, no hay necesidad de usar la inyección de dependencia para resolverlos.
El único caso en el que DI podría ser beneficioso (sobre los enfoques de registro estándar) es si desea someter el registro de su aplicación a pruebas unitarias. Sin embargo, sospecho que la mayoría de los desarrolladores dirían que el registro no es parte de la funcionalidad de una clase, y no es algo que deba probarse.
@Telastyn escribe:
Me temo que es un ripost muy teórico. En la práctica, a la mayoría de los desarrolladores e integradores de sistemas les gusta el hecho de que puede configurar el registro a través de un archivo de configuración. Y les GUSTA el hecho de que no se espera que prueben unitariamente el registro de un módulo.
Claro, si rellena las configuraciones de registro, entonces puede tener problemas, pero se manifestarán ya sea como la aplicación falla durante el inicio o demasiado o muy poco registro. 1) Estos problemas se solucionan fácilmente solucionando el error en el archivo de configuración. 2) La alternativa es un ciclo completo de compilación / análisis / prueba / implementación cada vez que realice un cambio en los niveles de registro. Eso no es aceptable.
fuente
Sí y No !
Sí: creo que es razonable que diferentes subsistemas (o capas semánticas o bibliotecas u otras nociones de agrupación modular) acepten (el mismo o) registrador potencialmente diferente durante su inicialización en lugar de que todos los subsistemas confíen en el mismo singleton compartido común .
Sin embargo,
No: al mismo tiempo no es razonable parametrizar el registro en cada pequeño objeto (por constructor o método de instancia). Para evitar la hinchazón innecesaria e inútil, las entidades más pequeñas deben usar el registrador singleton de su contexto de cierre.
Esta es una razón entre muchas otras para pensar en la modularidad en los niveles: los métodos se agrupan en clases, mientras que las clases se agrupan en subsistemas y / o capas semánticas. Estos paquetes más grandes son valiosas herramientas de abstracción; debemos dar diferentes consideraciones dentro de los límites modulares que al cruzarlos.
fuente
Primero, comienza con una memoria caché de singleton fuerte, lo siguiente que ve son los singletons fuertes para la capa de base de datos que introduce estado global, API no descriptivas de
class
es y código no comprobable.Si decide no tener un singleton para una base de datos, probablemente no sea una buena idea tener un singleton para una memoria caché, después de todo, representan un concepto muy similar, el almacenamiento de datos, que solo usa mecanismos diferentes.
El uso de un singleton en una clase convierte una clase que tiene una cantidad específica de dependencias a una clase que tiene, teóricamente , un número infinito de ellas, porque nunca se sabe qué está realmente oculto detrás del método estático.
En la última década que pasé programando, solo hubo un caso en el que presencié un esfuerzo para cambiar la lógica de registro (que luego se escribió como singleton). Entonces, aunque me encantan las inyecciones de dependencia, el registro no es realmente una gran preocupación. Caché, por otro lado, definitivamente siempre lo haría como una dependencia.
fuente
Sí y no, pero principalmente no
Supongo que la mayor parte de la conversación se basa en una instancia estática vs inyectada. ¿Nadie propone que el registro rompa el SRP, supongo? Estamos hablando principalmente del "principio de inversión de dependencia". Tbh, estoy de acuerdo con la respuesta de Telastyn.
¿Cuándo está bien usar estadísticas? Porque claramente a veces está bien. El punto de respuestas afirmativas de los beneficios de la abstracción y las respuestas de "no" indican que son algo por lo que usted paga. Una de las razones por las que su trabajo es difícil es que no hay una respuesta que pueda escribir y aplicar a todas las situaciones.
Tomar:
Convert.ToInt32("1")
Prefiero esto a:
¿Por qué? Estoy aceptando que necesitaré refactorizar el código si necesito la flexibilidad para cambiar el código de conversión. Estoy aceptando que no podré burlarme de esto. Creo que la sencillez y la brevedad valen este oficio.
Mirando el otro extremo del espectro, si
IConverter
fuera unSqlConnection
, me horrorizaría mucho ver eso como una llamada estática. Las razones por las cuales son muy obvias. Señalaría que aSQLConnection
puede ser bastante "transversal" en una aplicación, por lo que no habría usado esas palabras exactas.¿El registro se parece más a un
SQLConnection
oConvert.ToInt32
? Yo diría más como 'SQLConnection`.Deberías burlarte de Logging . Habla con el mundo exterior. Cuando escribo un método usando
Convert.ToIn32
, lo estoy usando como una herramienta para calcular algún otro resultado de la clase que pueda probarse por separado. No necesito verificar siConvert
se llamó correctamente al verificar que "1" + "2" == "3". El registro es diferente, es una salida totalmente independiente de la clase. Supongo que es un resultado que tiene valor para usted, el equipo de soporte y el negocio. Su clase no funciona si el registro no es correcto, por lo que las pruebas unitarias no deberían pasar. Deberías probar lo que registra tu clase. Creo que este es el argumento asesino, realmente podría parar aquí.También creo que es algo que es muy probable que cambie. Un buen registro no solo imprime cadenas, es una vista de lo que está haciendo su aplicación (soy un gran admirador del registro basado en eventos). He visto que el registro básico se convierte en IU de informes bastante elaboradas. Obviamente, es mucho más fácil ir en esta dirección si su registro se parece
_logger.Log(new ApplicationStartingEvent())
y no se pareceLogger.Log("Application has started")
. Uno podría argumentar que esto está creando un inventario para un futuro que tal vez nunca suceda, esta es una decisión y creo que vale la pena.De hecho, en un proyecto personal mío, creé una interfaz de usuario sin inicio de sesión utilizando únicamente el
_logger
para averiguar qué estaba haciendo la aplicación. Esto significaba que no tenía que escribir código para averiguar qué estaba haciendo la aplicación, y terminé con un registro sólido. Siento que si mi actitud hacia el registro fuera simple e inmutable, esa idea no se me habría ocurrido.Así que estaría de acuerdo con Telastyn para el caso de la tala.
fuente
Semantic Logging Application Block
. No lo use, como la mayoría del código creado por el equipo de "patrones y práctica" de MS, irónicamente, es un antipatrón.Primeras preocupaciones transversales no son bloques de construcción importantes y no deben tratarse como dependencias en un sistema. Un sistema debería funcionar si, por ejemplo, Logger no está inicializado o la memoria caché no funciona. ¿Cómo hará que el sistema sea menos acoplado y cohesivo? Ahí es donde SOLID aparece en el diseño del sistema OO.
Mantener el objeto como singleton no tiene nada que ver con SOLID. Ese es el ciclo de vida de su objeto cuánto tiempo desea que el objeto viva en la memoria.
Una clase que necesita una dependencia para inicializarse no debería saber si la instancia de clase proporcionada es singleton o transitoria. Pero tldr; si está escribiendo Logger.Instance.Log () en cada método o clase, entonces es un código problemático (olor a código / acoplamiento duro) realmente desordenado. Este es el momento en que las personas comienzan a abusar de SOLID. Y otros desarrolladores como OP comienzan a hacer preguntas genuinas como esta.
fuente
He resuelto este problema usando una combinación de herencia y rasgos (también llamados mixins en algunos idiomas). Los rasgos son muy útiles para resolver este tipo de preocupación transversal. Sin embargo, generalmente es una característica del lenguaje, así que creo que la respuesta real es que depende de las características del lenguaje.
fuente