Contexto: estoy usando C #
Diseñé una clase y, para aislarla y facilitar las pruebas unitarias, paso todas sus dependencias; no hace objeto de instanciación internamente. Sin embargo, en lugar de hacer referencia a interfaces para obtener los datos que necesita, hago referencia a Funcs de propósito general que devuelven los datos / comportamiento que requiere. Cuando inyecto sus dependencias, puedo hacerlo con expresiones lambda.
Para mí, esto parece un mejor enfoque porque no tengo que hacer ninguna burla tediosa durante la prueba de la unidad. Además, si la implementación circundante tiene cambios fundamentales, solo necesitaré cambiar la clase de fábrica; No se necesitarán cambios en la clase que contiene la lógica.
Sin embargo, nunca antes había visto a IoC hacerlo de esta manera, lo que me hace pensar que hay algunos escollos potenciales que me pueden faltar. Lo único que se me ocurre es una incompatibilidad menor con la versión anterior de C # que no define Func, y este no es un problema en mi caso.
¿Hay algún problema con el uso de delegados genéricos / funciones de orden superior como Func para IoC, en lugar de interfaces más específicas?
fuente
Func
ya que puede nombrar los parámetros, donde puede indicar su intención.Respuestas:
Si una interfaz contiene solo una función, no más, y no hay una razón convincente para introducir dos nombres (el nombre de la interfaz y el nombre de la función dentro de la interfaz), usar un
Func
lugar puede evitar un código innecesario y en la mayoría de los casos es preferible: al igual que cuando comienza a diseñar un DTO y reconoce que solo necesita un atributo de miembro.Supongo que muchas personas están más acostumbradas a usar
interfaces
, porque en el momento en que la inyección de dependencia y la IoC se estaban volviendo populares, no había un equivalente real de laFunc
clase en Java o C ++ (ni siquiera estoy seguro de siFunc
estaba disponible en ese momento en C # ) Por lo tanto, muchos tutoriales, ejemplos o libros de texto aún prefieren lainterface
forma, incluso si usarlosFunc
fuera más elegante.Puede consultar mi respuesta anterior sobre el principio de segregación de interfaz y el enfoque de diseño de flujo de Ralf Westphal. Este paradigma implementa DI con
Func
parámetros exclusivamente , exactamente por la misma razón que usted ya mencionó (y algunos más). Como puede ver, su idea no es realmente nueva, sino todo lo contrario.Y sí, utilicé este enfoque solo, para el código de producción, para los programas que necesitaban procesar datos en forma de una tubería con varios pasos intermedios, incluidas las pruebas unitarias para cada uno de los pasos. Entonces, a partir de eso, puedo brindarle experiencia de primera mano que puede funcionar muy bien.
fuente
Una de las principales ventajas que encuentro con IoC es que me permitirá nombrar todas mis dependencias nombrando su interfaz, y el contenedor sabrá cuál proporcionar al constructor haciendo coincidir los nombres de los tipos. Esto es conveniente y permite nombres de dependencias mucho más descriptivos que
Func<string, string>
.También a menudo encuentro que, incluso con una dependencia simple y simple, a veces necesita tener más de una función: una interfaz le permite agrupar estas funciones de una manera que es autodocumentada, en lugar de tener múltiples parámetros que todos se leen
Func<string, string>
,Func<string, int>
.Definitivamente, hay momentos en los que es útil simplemente tener un delegado que se pasa como una dependencia. Es una decisión decisiva cuando se utiliza un delegado frente a tener una interfaz con muy pocos miembros. A menos que esté realmente claro cuál es el propósito del argumento, generalmente me equivocaré al producir código autodocumentado; es decir. escribiendo una interfaz.
fuente
Realmente no. Un Func es su propio tipo de interfaz (en el significado en inglés, no en el significado de C #). "Este parámetro es algo que proporciona X cuando se le pregunta". El Func incluso tiene el beneficio de proporcionar de manera perezosa la información solo según sea necesario. Hago esto un poco y lo recomiendo con moderación.
En cuanto a las desventajas:
T
y otras sonFunc<T>
.fuente
Tomemos un ejemplo simple: tal vez esté inyectando un medio de registro.
Inyectando una clase
Creo que está bastante claro lo que está sucediendo. Además, si está utilizando un contenedor de IoC, ni siquiera necesita inyectar nada explícitamente, solo agregue a la raíz de su composición:
Al depurar
Worker
, un desarrollador solo necesita consultar la raíz de la composición para determinar qué clase concreta se está utilizando.Si un desarrollador necesita una lógica más complicada, tiene toda la interfaz para trabajar:
Inyectando un método
Primero, observe que el parámetro constructor tiene un nombre más largo ahora
methodThatLogs
,. Esto es necesario porque no se puede saber quéAction<string>
se supone que debe hacer. Con la interfaz, estaba completamente claro, pero aquí tenemos que recurrir a confiar en la denominación de parámetros. Esto parece inherentemente menos confiable y más difícil de aplicar durante una compilación.Ahora, ¿cómo inyectamos este método? Bueno, el contenedor de IoC no lo hará por usted. Por lo tanto, queda inyectado explícitamente cuando crea una instancia
Worker
. Esto plantea un par de problemas:Worker
Worker
encontrarán que es más difícil descubrir qué instancia concreta se llama. No pueden simplemente consultar la raíz de la composición; Tendrán que rastrear el código.¿Qué tal si necesitamos una lógica más complicada? Su técnica solo expone un método. Ahora supongo que podrías hornear las cosas complicadas en la lambda:
pero cuando estás escribiendo tus pruebas unitarias, ¿cómo pruebas esa expresión lambda? Es anónimo, por lo que su marco de prueba de unidad no puede instanciarlo directamente. Tal vez puedas encontrar una forma inteligente de hacerlo, pero probablemente será una PITA más grande que usar una interfaz.
Resumen de las diferencias:
Si está de acuerdo con todo lo anterior, entonces está bien inyectar solo el método. De lo contrario, te sugiero que sigas con la tradición e inyectes una interfaz.
fuente
Worker
dependencia viable a la vez, permitiendo que cada lambda se inyecte de forma independiente hasta que se satisfagan todas las dependencias. Esto es similar a la aplicación parcial en FP. Además, el uso de lambdas ayuda a eliminar el estado implícito y el acoplamiento oculto entre dependencias.Considere el siguiente código que escribí hace mucho tiempo:
Toma una ubicación relativa a un archivo de plantilla, lo carga en la memoria, representa el cuerpo del mensaje y ensambla el objeto de correo electrónico.
Puede mirar
IPhysicalPathMapper
y pensar: "Solo hay una función. Esa podría ser unaFunc
". Pero en realidad, el problema aquí es queIPhysicalPathMapper
ni siquiera debería existir. Una solución mucho mejor es simplemente parametrizar la ruta :Esto plantea una gran cantidad de otras preguntas para mejorar este código. Por ejemplo, tal vez solo debería aceptar un
EmailTemplate
, y luego tal vez debería aceptar una plantilla previamente renderizada, y luego tal vez debería estar alineado.Es por eso que no me gusta la inversión de control como un patrón generalizado. En general, se presenta como esta solución divina para componer todo su código. Pero en realidad, si lo usa penetrante (en comparación con moderación), que hace que su código mucho peor por que le anima a introducir una gran cantidad de interfaces innecesarias que se utilizan totalmente al revés. (Hacia atrás en el sentido de que la persona que llama realmente debería ser responsable de evaluar esas dependencias y pasar el resultado en lugar de que la clase misma invoque la llamada).
Las interfaces deben usarse con moderación, y la inversión de control y la inyección de dependencia también deben usarse con moderación. Si tiene grandes cantidades de ellos, su código se vuelve mucho más difícil de descifrar.
fuente