Estoy usando Unity en C # para la inyección de dependencia, pero la pregunta debería ser aplicable a cualquier lenguaje y marco que esté usando la inyección de dependencia.
Estoy tratando de seguir el principio SÓLIDO y, por lo tanto, obtuve muchas abstracciones. Pero ahora me pregunto si hay una mejor práctica para cuántas inyecciones debe inyectar una clase.
Por ejemplo, tengo un repositorio con 9 inyecciones. ¿Será difícil de leer para otro desarrollador?
Las inyecciones tienen las siguientes responsabilidades:
- IDbContextFactory: crea el contexto para la base de datos.
- IMapper: asignación de entidades a modelos de dominio.
- IClock - Abstracts DateTime. Ahora para ayudar con las pruebas unitarias.
- IPerformanceFactory: mide el tiempo de ejecución de métodos específicos.
- ILog: Log4net para iniciar sesión.
- ICollectionWrapperFactory: crea colecciones (que amplía IEnumerable).
- IQueryFilterFactory: genera consultas basadas en la entrada que consultará db.
- IIdentityHelper: recupera el usuario conectado.
- IFaultFactory: crea diferentes FaultExceptions (uso WCF).
No estoy realmente decepcionado con la forma en que delegué las responsabilidades, pero estoy empezando a preocuparme por la legibilidad.
Entonces, mis preguntas:
¿Hay un límite para cuántas inyecciones debe tener una clase? Y si es así, ¿cómo evitarlo?
¿Muchas inyecciones limitan la legibilidad o realmente la mejoran?
fuente
Respuestas:
Demasiadas dependencias pueden indicar que la clase misma está haciendo demasiado. Para determinar si no está haciendo demasiado:
Mira la clase en sí. ¿Tendría sentido dividirlo en dos, tres, cuatro? ¿Tiene sentido en su conjunto?
Mira los tipos de dependencias. ¿Cuáles son específicos del dominio y cuáles son "globales"? Por ejemplo, no consideraría
ILog
al mismo nivel queIQueryFilterFactory
: el primero estaría disponible en la mayoría de las clases de negocios de todos modos si están utilizando el registro. Por otro lado, si encuentra muchas dependencias específicas de dominio, esto puede indicar que la clase está haciendo demasiado.Mire las dependencias que pueden ser reemplazadas por valores.
Esto podría reemplazarse fácilmente si
DateTime.Now
se pasa directamente a los métodos que requieren conocer la hora actual.Al observar las dependencias reales, no veo nada que sea indicativo de que sucedan cosas malas:
IDbContextFactory: crea el contexto para la base de datos.
OK, probablemente estamos dentro de una capa empresarial donde las clases interactúan con la capa de acceso a datos. Se ve bien.
IMapper: asignación de entidades a modelos de dominio.
Es difícil decir algo sin la imagen general. Es posible que la arquitectura sea incorrecta y que la capa de acceso a los datos se haga directamente, o que la arquitectura esté perfectamente bien. En todos los casos, tiene sentido tener esta dependencia aquí.
Otra opción sería dividir la clase en dos: una que se ocupa del mapeo, la otra que se ocupa de la lógica comercial real. Esto crearía una capa de facto que separará aún más el BL del DAL. Si las asignaciones son complejas, podría ser una buena idea. Sin embargo, en la mayoría de los casos, solo agregaría una complejidad inútil.
IClock - Abstracts DateTime. Ahora para ayudar con las pruebas unitarias.
Probablemente no sea muy útil tener una interfaz (y clase) separada solo para obtener la hora actual. Simplemente pasaría
DateTime.Now
a los métodos que requieren la hora actual.Una clase separada puede tener sentido si hay alguna otra información, como las zonas horarias o los rangos de fechas, etc.
IPerformanceFactory: mide el tiempo de ejecución de métodos específicos.
Ver el siguiente punto.
ILog: Log4net para iniciar sesión.
Dicha funcionalidad trascendente debería pertenecer al marco, y las bibliotecas reales deberían ser intercambiables y configurables en tiempo de ejecución (por ejemplo, a través de app.config en .NET).
Desafortunadamente, este no es (todavía) el caso, lo que le permite elegir una biblioteca y seguir con ella, o crear una capa de abstracción para poder intercambiar las bibliotecas más adelante si es necesario. Si su intención es específicamente ser independiente de la elección de la biblioteca, hágalo. Si está seguro de que continuará usando la biblioteca durante años, no agregue una abstracción.
Si la biblioteca es demasiado compleja de usar, tiene sentido un patrón de fachada.
ICollectionWrapperFactory: crea colecciones (que amplía IEnumerable).
Supongo que esto crea estructuras de datos muy específicas que son utilizadas por la lógica de dominio. Parece una clase de utilidad. En su lugar, use una clase por estructura de datos con constructores relevantes. Si la lógica de inicialización es un poco complicada para encajar en un constructor, utilice métodos de fábrica estáticos. Si la lógica es aún más compleja, use el patrón de fábrica o de construcción.
IQueryFilterFactory: genera consultas basadas en la entrada que consultará db.
¿Por qué no está eso en la capa de acceso a datos? ¿Por qué hay un
Filter
en el nombre?IIdentityHelper: recupera el usuario conectado.
No estoy seguro de por qué hay un
Helper
sufijo. En todos los casos, otros sufijos tampoco serán particularmente explícitos (IIdentityManager
?)De todos modos, tiene mucho sentido tener esta dependencia aquí.
IFaultFactory: crea diferentes FaultExceptions (uso WCF).
¿Es la lógica tan compleja que requiere usar un patrón de fábrica? ¿Por qué se usa la inyección de dependencia para eso? ¿Cambiaría la creación de excepciones entre el código de producción y las pruebas? ¿Por qué?
Intentaría refactorizar eso en simple
throw new FaultException(...)
. Si se debe agregar alguna información global a todas las excepciones antes de propagarlas al cliente, WCF probablemente tenga un mecanismo donde detecte una excepción no controlada y pueda cambiarla y volver a lanzarla al cliente.Medir la calidad por números suele ser tan malo como ser pagado por las líneas de código que escribe por mes. Es posible que tenga una gran cantidad de dependencias en una clase bien diseñada, ya que puede tener una clase mala con pocas dependencias.
Muchas dependencias hacen que la lógica sea más difícil de seguir. Si la lógica es difícil de seguir, la clase probablemente esté haciendo demasiado y debería dividirse.
fuente
Este es un ejemplo clásico de DI que le dice que su clase probablemente se está volviendo demasiado grande para ser una sola clase. Esto a menudo se interpreta como "wow, DI hace que mis constructores sean más grandes que Júpiter, esta técnica es horrible", pero lo que REALMENTE te dice es que "tu clase tiene una gran cantidad de dependencias". Sabiendo esto, podemos
Existen innumerables formas de administrar las dependencias, y es imposible decir qué funciona mejor en su caso sin conocer su código y su aplicación.
Para responder a sus preguntas finales:
Sí, el límite superior es "demasiados". Cuántos son demasiadas"? "Demasiados" es cuando la cohesión de la clase se vuelve "demasiado baja". Todo depende. Normalmente, si su reacción a una clase es "wow, esto tiene muchas dependencias", eso es demasiado.
Creo que esta pregunta es engañosa. La respuesta puede ser sí o no. Pero no es la parte más interesante. El punto de inyectar dependencias es hacerlas VISIBLES. Se trata de hacer una API que no mienta. Se trata de prevenir el estado global. Se trata de hacer que el código sea comprobable. Se trata de reducir el acoplamiento.
Las clases bien diseñadas con métodos razonablemente diseñados y nombrados son más legibles que las mal diseñadas. DI realmente no mejora ni perjudica la legibilidad per se, solo hace que sus elecciones de diseño se destaquen y, si son malas, le picarán los ojos. Esto no significa que DI haya hecho que su código sea menos legible, solo le mostró que su código ya era un desastre, lo acababa de ocultar.
fuente
Según Steve McConnel, autor del omnipresente "Código completo", la regla general es que más de 7 es un olor a código que perjudica la mantenibilidad. Personalmente, creo que el número es más bajo en la mayoría de los casos, pero hacer DI correctamente resultará en tener muchas dependencias para inyectar cuando estás muy cerca de la raíz de composición. Esto es normal y esperado. Es una de las razones por las que los contenedores de IoC son algo útil y valen la complejidad que agregan a un proyecto.
Entonces, si está muy cerca del punto de entrada de su programa, esto es normal y aceptable. Si profundiza en la lógica del programa, es probable que se deba abordar un olor.
fuente