¿Son las llamadas "preocupaciones transversales" una excusa válida para romper SOLID / DI / IoC?

43

A mis colegas les gusta decir que "registrar / almacenar en caché / etc. es una preocupación transversal" y luego usar el singleton correspondiente en todas partes. Sin embargo, les encanta IoC y DI.

¿Es realmente una excusa válida para romper el principio de SOLI D ?

    
pregunta Den 13.05.2016 - 13:22

11 respuestas

42

No.

SOLID existe como pautas para dar cuenta del cambio inevitable. ¿Realmente no va a cambiar su biblioteca de registro, o destino, o filtrado, o formato, o ...? ¿Realmente no va a cambiar su biblioteca de almacenamiento en caché, su objetivo, su estrategia, su alcance, o ...?

Por supuesto que sí. Como mínimo, , querrás burlarte de estas cosas de una manera sana para aislarlas y probarlas. Y si va a querer aislarlos para las pruebas, es probable que se encuentre con una razón comercial en la que desee aislarlos por razones de la vida real.

Y luego obtendrás el argumento de que el registrador mismo manejará el cambio. "Oh, si el objetivo / filtrado / formato / estrategia cambia, entonces simplemente cambiaremos la configuración". Eso es basura. No solo ahora tiene un Objeto de Dios que maneja todas estas cosas, está escribiendo su código en XML (o similar) donde no obtiene un análisis estático, no obtiene errores de tiempo de compilación y no lo hace. Realmente obtendré pruebas unitarias efectivas.

¿Hay casos que violen las pautas de SOLID? Absolutamente. A veces las cosas no cambian (sin requerir una reescritura completa). 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 inquietudes transversales ubicuas) no son esos casos. Por lo general, son excelentes ejemplos de los problemas de acoplamiento y diseño que surgen al ignorar las pautas.

    
respondido por el Telastyn 13.05.2016 - 15:27
38

Este es el punto central del término "preocupación transversal": significa algo que no encaja perfectamente en el principio SOLID.

Aquí es donde el idealismo se encuentra con la realidad.

Las personas semi-nuevas en 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 no tiene sentido. El corte transversal es el hermano de SOLID, van de la mano.

    
respondido por el Klom Dark 13.05.2016 - 18:20
24

Para el registro creo que es. El registro es generalizado y generalmente no está relacionado con la funcionalidad del servicio. Es común y bien entendido usar los patrones singleton de la infraestructura de registro. Si no lo haces, estás creando e inyectando registradores en todas partes y no quieres eso.

Un problema de lo anterior es que alguien dirá 'pero ¿cómo puedo probar el registro?' . Mi opinión es que normalmente no pruebo el registro, más allá de afirmar que realmente puedo leer los archivos de registro y entenderlos. Cuando he visto el registro probado, generalmente es porque alguien necesita afirmar que una clase realmente ha "hecho" algo "y está usando los mensajes de registro para obtener esa retroalimentación. Preferiría registrar a algún oyente / observador en esa clase y afirmar en mis pruebas que 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.

    
respondido por el Brian Agnew 13.05.2016 - 13:41
12

Mis 2 centavos ...

Sí y no.

Nunca debes realmente violar los principios que adoptas; pero, sus principios siempre deben ser matizados y adoptados en servicio a un objetivo superior. Por lo tanto, con una comprensión adecuadamente condicionada, algunas violaciones aparentes pueden no ser violaciones reales del "espíritu" o "conjunto de principios".

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 de mantenimiento". Por lo tanto, adherirse a cualquier principio particular de SOLID es contraproducente y contradictorio cuando lo hace, entra en conflicto con los objetivos de SOLID. Y aquí, a menudo observo que entregar triunfa sobre mantenibilidad .

Entonces, ¿qué pasa con el D en SOLID ? 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 una sola función, clases, conjuntos de clases y programas.

Y sí, cambiar las implementaciones del registrador probablemente coloca su módulo en "otro contexto distinto".

Entonces, déjame ofrecerte 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 está necesariamente limitado a su experiencia .

Si no actualmente planea usar un módulo en otro contexto, es probable que esté bien para que dependa de él. La advertencia a la advertencia: sus planes probablemente estén equivocados, pero eso es también OK. Cuanto más tiempo escriba un módulo tras otro, más intuitivo y preciso será su idea de si "lo necesitaré de nuevo algún día". Pero, probablemente nunca podrá decir retrospectivamente: "He modularizado y desacoplado todo en la mayor medida posible, pero sin exceso ".

Si te sientes culpable por tus errores de juicio, confiesa y continúa ...

En segundo lugar: Invertir control no es igual a dependencias de inyección .

Esto es especialmente cierto cuando comienza a inyectar dependencias ad nauseam . La inyección de dependencia es una táctica útil para la estrategia global de la IoC. Sin embargo, yo diría que la DI es menos eficiente que otras tácticas, como el uso de interfaces y adaptadores, puntos únicos de exposición al contexto desde el módulo .

Y realmente concentrémonos en esto por un segundo. Porque, incluso si inyecta un Logger ad nauseam , necesita escribir código en la interfaz Logger . No podría comenzar a usar un nuevo Logger de un proveedor diferente que toma 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 único submódulo (Adaptador) para administrar la dependencia.

Y si está codificando contra un Adaptador, si el Logger se inyecta en ese Adaptador o si es descubierto por el Adaptador, en general es bastante insignificante para el objetivo general de mantenimiento. Y lo que es 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é estás usando los principios. Y, más prácticamente, simplemente cree un Adapter para cada módulo. Use su criterio para decidir dónde dibuja los límites del "módulo". Desde dentro de cada módulo, siga adelante y consulte directamente el Adapter . Y seguro, inyecte el registrador real en el Adapter , pero no en cada pequeña cosa que pueda necesitarlo.

    
respondido por el svidgen 13.05.2016 - 20:19
8

La idea de que el registro debe siempre implementarse como un singleton es una de esas mentiras que se ha dicho tan a menudo que ha ganado fuerza.

Durante el tiempo que los sistemas operativos modernos lo han hecho, se ha reconocido que es posible que desee iniciar sesión en varios lugares según la naturaleza de de la salida .

Los diseñadores de sistemas deben cuestionar constantemente la eficacia de las soluciones anteriores antes de incluirlas a ciegas en las nuevas. Si no están realizando esa diligencia, entonces no están haciendo su trabajo.

    
respondido por el Robbie Dee 13.05.2016 - 14:20
4

El registro realmente es un caso especial.

@Telastyn escribe:

  

¿Realmente nunca cambiarás tu biblioteca de registro, tu destino, el filtrado, el formato o ...?

Si anticipa que podría necesitar cambiar su biblioteca de registro, entonces debería usar una fachada; es decir, SLF4J si estás en el mundo Java.

En cuanto al resto, una biblioteca de registro decente se encarga de cambiar a dónde va el registro, qué eventos se filtran, cómo se formatean los eventos de registro utilizando los archivos de configuración del registrador y (si es necesario) clases de complementos personalizados. Hay una serie de alternativas disponibles en el mercado.

En resumen, estos son problemas resueltos ... para el registro ... y, por lo tanto, no es necesario utilizar la inyección de dependencia para resolverlos.

El único caso en el que la DI podría ser beneficiosa (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 una funcionalidad de clases, y no es algo que necesite para probar.

@Telastyn escribe:

  

Y luego obtendrás el argumento de que el registrador manejará el cambio. "Oh, si el objetivo / filtrado / formato / estrategia cambia, entonces simplemente cambiaremos la configuración". Eso es basura. No solo ahora tiene un Objeto de Dios que maneja todas estas cosas, está escribiendo su código en XML (o similar) donde no obtiene un análisis estático, no obtiene errores de tiempo de compilación y no lo hace. Realmente obtendré pruebas unitarias efectivas.

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 realicen pruebas unitarias del registro de un módulo.

Claro, si rellena las configuraciones de registro, entonces puede obtener problemas, pero se manifestarán como un error de la aplicación durante el inicio o demasiado / muy poco registro. 1) Estos problemas se solucionan fácilmente al corregir el error en el archivo de configuración. 2) La alternativa es un ciclo completo de construcción / análisis / prueba / implementación cada vez que realice un cambio en los niveles de registro. Eso no es aceptable.

    
respondido por el Stephen C 14.05.2016 - 03:59
3

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 lo mismo Singleton compartido .

Sin embargo,

No: al mismo tiempo no es razonable parametrizar el registro en cada objeto pequeño (por el método del constructor o de la instancia). Para evitar la hinchazón innecesaria y sin sentido, las entidades más pequeñas deben usar el registrador Singleton de su contexto adjunto.

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 herramientas valiosas de abstracción; deberíamos dar diferentes consideraciones dentro de los límites modulares que cuando se cruzan.

    
respondido por el Erik Eidt 13.05.2016 - 19:10
3

Primero comienza con un cache de singleton fuerte, las siguientes cosas que verás son singletons fuertes para la capa de base de datos introduciendo el estado global, las API no descriptivas de class es y el código no comprobable.

Si decides no tener un singleton para una base de datos, probablemente no sea una buena idea tener un singleton para un caché, después de todo, representan un concepto muy similar, el almacenamiento de datos, solo que usa diferentes mecanismos.

El uso de un singleton en una clase convierte a 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 por 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 preocupación tan grande. Caché, por otro lado, definitivamente siempre lo haría como una dependencia.

    
respondido por el Andy 13.05.2016 - 21:37
3

Sí y No, pero en su mayoría No

Supongo que la mayor parte de la conversación se basa en instancias estáticas frente a inyectadas. ¿Nadie está proponiendo que el registro rompa el SRP que asumo? 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? Claramente, a veces está bien. Las respuestas afirmativas señalan los beneficios de la abstracción y las respuestas negativas indican que son algo por lo que pagas. 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.

Toma: Convert.ToInt32("1")

Prefiero esto a:

private readonly IConverter _converter;

public MyClass(IConverter converter)
{
   Guard.NotNull(converter)
   _converter = conveter
}

.... 
var foo = _converter.ToInt32("1");

¿Por qué? Estoy aceptando que necesitaré refactorizar el código si necesito la flexibilidad para intercambiar el código de conversión. Estoy aceptando que no podré burlarme de esto. Creo que la simplicidad y la tersitud merecen este intercambio.

Mirando al otro extremo del espectro, si IConverter fuera un SqlConnection , estaría bastante horrorizado de ver eso como una llamada estática. Las razones por las que son obvias. Me gustaría señalar que un SQLConnection puede ser bastante "transversal" en una aplicación, por lo que no habría utilizado esas palabras exactas.

¿El registro es más como SQLConnection o Convert.ToInt32 ? Diría más como 'SQLConnection'.

Debería estar burlándose del Registro . Habla con el mundo exterior. Cuando escribo un método usando Convert.ToIn32 , lo estoy usando como una herramienta para calcular alguna otra salida de la clase que se pueda probar por separado. No necesito verificar que Convert fue llamado correctamente al verificar que "1" + "2" == "3". El registro es diferente, es un resultado totalmente independiente de la clase. Supongo que es una salida 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 de unidad no deberían pasar. Debería estar probando cuáles son los registros de su clase. Creo que este es el argumento más importante, realmente podría detenerme aquí.

También creo que es algo que es muy probable que cambie. El buen registro no solo imprime cadenas, es una vista de lo que hace su aplicación (soy un gran fanático del registro basado en eventos). He visto cómo el inicio de sesión básico se convierte en interfaces de usuario muy elaboradas. Obviamente, es mucho más fácil dirigirse en esta dirección si su registro se ve como _logger.Log(new ApplicationStartingEvent()) y menos como Logger.Log("Application has started") . Se podría argumentar que esto es crear un inventario para un futuro que tal vez nunca ocurra, es una decisión de juicio y creo que vale la pena.

De hecho, en un proyecto personal mío, creé una interfaz de usuario sin registro simplemente usando _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 y que no cambia, esa idea no me habría ocurrido.

Así que estaría de acuerdo con Telastyn para el caso de registro.

    
respondido por el Nathan Cooper 14.05.2016 - 19:41
3

Las preocupaciones de First Cross Cut no son elementos fundamentales y no deben tratarse como dependencias en un sistema. Un sistema debería funcionar si, por ejemplo, El registrador no está inicializado o el caché no funciona. ¿Cómo harás que el sistema sea menos acoplado y cohesivo? Ahí es donde SOLID entra en escena 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 tu objeto por cuánto tiempo quieres que el objeto viva en la memoria.

Una clase que necesita una dependencia para inicializarse no debería saber si la instancia de clase provista 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 de código / acoplamiento duro) uno 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.

    
respondido por el vendettamit 15.05.2016 - 13:31
2

Resolví este problema usando una combinación de herencia y rasgos (también llamados mixins en algunos idiomas). Los rasgos son súper prácticos para resolver este tipo de preocupación transversal. Aunque generalmente es una característica del idioma, así que creo que la respuesta real es que depende de las características del idioma.

    
respondido por el RibaldEddie 13.05.2016 - 16:24

Lea otras preguntas en las etiquetas