Tengo una interfaz llamada IContext
. A los efectos de esto, realmente no importa lo que hace, excepto lo siguiente:
T GetService<T>();
Lo que hace este método es mirar el contenedor DI actual de la aplicación e intenta resolver la dependencia. Bastante estándar, creo.
En mi aplicación ASP.NET MVC, mi constructor se ve así.
protected MyControllerBase(IContext ctx)
{
TheContext = ctx;
SomeService = ctx.GetService<ISomeService>();
AnotherService = ctx.GetService<IAnotherService>();
}
Entonces, en lugar de agregar múltiples parámetros en el constructor para cada servicio (porque esto será realmente molesto y requerirá mucho tiempo para los desarrolladores que extienden la aplicación) Estoy usando este método para obtener servicios.
Ahora, se siente mal . Sin embargo, la forma en que lo estoy justificando actualmente es esto: puedo burlarme de él .
Yo puedo. No sería difícil burlarse IContext
para probar el controlador. Tendría que de todos modos:
public class MyMockContext : IContext
{
public T GetService<T>()
{
if (typeof(T) == typeof(ISomeService))
{
// return another mock, or concrete etc etc
}
// etc etc
}
}
Pero como dije, se siente mal. Cualquier pensamiento / abuso bienvenido.
fuente
public SomeClass(Context c)
. Este código es bastante claro, ¿no? Dice,that SomeClass
depende de aContext
. Err, pero espera, no lo hace! Solo se basa en la dependenciaX
que obtiene de Context. Eso significa que, cada vez que se realice un cambio enContext
él podría romperSomeObject
, a pesar de que sólo cambióContext
sY
. Pero sí, sabes que soloY
no cambiasteX
, así queSomeClass
está bien. Pero escribir un buen código no se trata de lo que usted sabe, sino de lo que sabe el nuevo empleado cuando mira su código la primera vez.Respuestas:
Tener uno en lugar de muchos parámetros en el constructor no es la parte problemática de este diseño . Mientras su
IContext
clase no sea más que una fachada de servicio , específicamente para proporcionar las dependencias utilizadasMyControllerBase
, y no un localizador de servicio general utilizado en todo su código, esa parte de su código está en mi humilde opinión.Su primer ejemplo podría cambiarse a
eso no sería un cambio sustancial de diseño de
MyControllerBase
. Si este diseño es bueno o malo depende solo del hecho si quieresTheContext
,SomeService
yAnotherService
siempre están todos inicializados con objetos simulados, o todos ellos con objetos realesPor lo tanto, usar solo un parámetro en lugar de 3 en el constructor puede ser completamente razonable.
Lo que es problemático es
IContext
exponer elGetService
método en público. En mi humilde opinión, debe evitar esto, en su lugar, mantenga los "métodos de fábrica" explícitos. Entonces, ¿estará bien implementar los métodosGetSomeService
yGetAnotherService
de mi ejemplo utilizando un localizador de servicios? En mi humilde opinión eso depende. Mientras laIContext
clase siga siendo una simple fábrica abstracta para el propósito específico de proporcionar una lista explícita de objetos de servicio, eso es aceptable en mi humilde opinión. Las fábricas abstractas suelen ser solo un código de "pegamento", que no tiene que ser probado por sí mismo. Sin embargo, debe preguntarse si en el contexto de métodos comoGetSomeService
, si realmente necesita un localizador de servicios, o si una llamada explícita de constructor no sería más simple.Así que tenga cuidado, cuando se adhiere a un diseño donde la
IContext
implementación es solo una envoltura alrededor de unGetService
método público genérico , que permite resolver cualquier dependencia arbitraria por clases arbitrarias, entonces todo se aplica a lo que @BenjaminHodgson escribió en su respuesta.fuente
GetService
método genérico . Refactorizar a métodos explícitamente nombrados y escritos es mejor. Mejor aún sería explicarIContext
explícitamente las dependencias de la implementación en el constructor.IValidatableObject
?Este diseño se conoce como Service Locator * y no me gusta. Hay muchos argumentos en contra:
El Localizador de servicios lo acopla a su contenedor . Usando la inyección de dependencia regular (donde el constructor explica explícitamente las dependencias) puede reemplazar directamente su contenedor por uno diferente, o volver a
new
-expresiones. Con tuIContext
eso no es realmente posible.Service Locator oculta dependencias . Como cliente, es muy difícil saber qué necesita para construir una instancia de una clase. Necesita algún tipo de
IContext
, pero también necesita configurar el contexto para devolver los objetos correctos para hacer elMyControllerBase
trabajo. Esto no es del todo obvio por la firma del constructor. Con DI regular, el compilador le dice exactamente lo que necesita. Si su clase tiene muchas dependencias, debe sentir ese dolor porque lo estimulará a refactorizar. Service Locator oculta los problemas con los malos diseños.Service Locator provoca errores en tiempo de ejecución . Si llama
GetService
con un parámetro de tipo incorrecto, obtendrá una excepción. En otras palabras, suGetService
función no es una función total. (Las funciones totales son una idea del mundo FP, pero básicamente significa que las funciones siempre deben devolver un valor). Es mejor dejar que el compilador ayude y le diga cuándo tiene las dependencias equivocadas.El Localizador de servicios viola el Principio de sustitución de Liskov . ¡Debido a que su comportamiento varía según el argumento de tipo, Service Locator puede verse como si efectivamente tuviera un número infinito de métodos en la interfaz! Este argumento se explica en detalle aquí .
Service Locator es difícil de probar . Has dado un ejemplo de una
IContext
prueba falsa , lo cual está bien, pero seguramente es mejor no tener que escribir ese código en primer lugar. Simplemente inyecte sus dependencias falsas directamente, sin pasar por su localizador de servicios.En resumen, simplemente no lo hagas . Parece una solución seductora para el problema de las clases con muchas dependencias, pero a la larga vas a hacerte la vida imposible.
* Estoy definiendo Service Locator como un objeto con un
Resolve<T>
método genérico que es capaz de resolver dependencias arbitrarias y se usa en toda la base de código (no solo en la raíz de composición). Esto no es lo mismo que Service Facade (un objeto que agrupa un pequeño conjunto conocido de dependencias) o Abstract Factory (un objeto que crea instancias de un solo tipo; el tipo de Abstract Factory puede ser genérico pero el método no lo es) .fuente
MyControllerBase
no está acoplado a un contenedor DI específico, ni es "realmente" un ejemplo para el antipatrón del Localizador de servicios.GetService<T>
método genérico . Resolver dependencias arbitrarias es el verdadero olor, que está presente y es correcto en el ejemplo del OP.GetService<T>()
método permite que se soliciten clases arbitrarias: "Lo que hace este método es mirar el contenedor DI actual de la aplicación e intenta resolver la dependencia. Creo que es bastante estándar". . Estaba respondiendo a tu comentario en la parte superior de esta respuesta. Este es 100% un localizador de serviciosMark Seemann afirma claramente los mejores argumentos contra el antipatrón del Localizador de servicios, por lo que no profundizaré demasiado en por qué es una mala idea: es un viaje de aprendizaje que debe tomarse el tiempo para comprender por sí mismo (I También recomiendo el libro de Mark ).
OK, así que para responder la pregunta, volvamos a exponer su problema real :
Hay una pregunta que aborda este problema en StackOverflow . Como se menciona en uno de los comentarios allí:
Está buscando la solución a su problema en el lugar equivocado. Es importante saber cuándo una clase está haciendo demasiado. En su caso , sospecho que no hay necesidad de un "Controlador base". De hecho, en OOP casi nunca hay necesidad de herencia en absoluto . Las variaciones en el comportamiento y la funcionalidad compartida se pueden lograr por completo mediante el uso apropiado de interfaces, lo que generalmente da como resultado un código mejor factorizado y encapsulado, y sin necesidad de pasar dependencias a los constructores de superclase.
En todos los proyectos en los que he trabajado donde hay un controlador base, se realizó con el único fin de compartir propiedades y métodos convenientes, como
IsUserLoggedIn()
yGetCurrentUserId()
. PARADA . Este es un mal uso horrible de la herencia. En su lugar, cree un componente que exponga estos métodos y dependa de él donde lo necesite. De esta manera, sus componentes seguirán siendo comprobables y sus dependencias serán obvias.Aparte de cualquier otra cosa, al usar el patrón MVC siempre recomendaría controladores flacos . Puede leer más sobre esto aquí, pero la esencia del patrón es simple, que los controladores en MVC deberían hacer una sola cosa: manejar los argumentos pasados por el marco MVC, delegando otras preocupaciones a algún otro componente. De nuevo, este es el Principio de responsabilidad única en el trabajo.
Realmente sería útil conocer su caso de uso para hacer un juicio más preciso, pero sinceramente, no puedo pensar en ningún escenario en el que una clase base sea preferible a las dependencias bien factorizadas.
fuente
protected
las delegaciones de una línea a los nuevos componentes. Entonces puede perder BaseController y reemplazar esosprotected
métodos con llamadas directas a dependencias: esto simplificará su modelo y hará explícitas todas sus dependencias (¡lo cual es algo muy bueno en un proyecto con cientos de controladores!)Estoy agregando una respuesta a esto basada en las contribuciones de todos los demás. Muchas gracias a todos. Primero, aquí está mi respuesta: "No, no hay nada de malo".
Respuesta de "Fachada de servicio" de Doc Brown. Acepté esta respuesta porque lo que estaba buscando (si la respuesta era "no") eran algunos ejemplos o alguna expansión sobre lo que estaba haciendo. Proporcionó esto al sugerir que A) tiene un nombre y B) probablemente hay mejores formas de hacerlo.
Respuesta del "Localizador de servicios" de Benjamin Hodgson Aunque aprecio el conocimiento que he adquirido aquí, lo que tengo no es un "Localizador de servicios". Es una "Fachada de servicio". Todo en esta respuesta es correcto pero no para mis circunstancias.
Respuestas de la USR
Abordaré esto con más detalle:
No pierdo ninguna herramienta y no pierdo ninguna escritura "estática". La fachada de servicio devolverá lo que configuré en mi contenedor DI, o
default(T)
. Y lo que devuelve es "escrito". El reflejo está encapsulado.Ciertamente no es "raro". Como estoy usando un controlador base , cada vez que necesito cambiar su constructor, es posible que tenga que cambiar 10, 100, 1000 otros controladores.
Estoy usando inyección de dependencia. Ese es el punto.
Y finalmente, el comentario de Jules sobre la respuesta de Benjamin de que no estoy perdiendo flexibilidad. Es mi fachada de servicio. Puedo agregar tantos parámetros
GetService<T>
como desee para distinguir entre diferentes implementaciones, tal como lo haría uno al configurar un contenedor DI. Entonces, por ejemplo, podría cambiarGetService<T>()
paraGetService<T>(string extraInfo = null)
evitar este "problema potencial".De todos modos, gracias de nuevo a todos. Ha sido realmente útil. Salud.
fuente
GetService<T>()
método puede (intentar) resolver dependencias arbitrarias . Esto lo convierte en un Localizador de servicios, no en una Fachada de servicios, como expliqué en la nota de pie de página de mi respuesta. Si tuviera que reemplazarlo con un pequeño conjunto deGetServiceX()
/GetServiceY()
métodos, como sugiere @DocBrown, entonces sería una Fachada.IContext
ay inyectar eso, y de manera crucial: si agrega una nueva dependencia, el código aún se compilará , a pesar de que las pruebas fallarán en tiempo de ejecución