De esta manera, estoy escribiendo que este código es comprobable, pero ¿hay algún problema con él?

13

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 tiene este aspecto.

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 resultará realmente molesto y tomará 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 en mi mente es la siguiente: puedo burlarme de ella .

Yo puedo. No sería difícil simular un IContext para probar el controlador. Tendría que hacerlo 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.

    
pregunta LiverpoolsNumber9 11.01.2015 - 12:18

4 respuestas

5

Tener uno en lugar de muchos parámetros en el constructor no es la parte problemática de este diseño . Siempre que su clase IContext no sea más que una fachada de servicio , específicamente para proporcionar las dependencias utilizadas en MyControllerBase , y no es un localizador de servicio general usado en todo su código, esa parte de su código es IMHO, está bien.

Tu primer ejemplo podría cambiarse a

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetSomeService();
    AnotherService = ctx.GetAnotherService();
}

eso no sería un cambio de diseño sustancial de MyControllerBase . Si este diseño es bueno o malo, solo depende del hecho si usted quiere

  • asegúrese de que TheContext , SomeService y AnotherService siempre sean todos inicializados con objetos simulados, o todos ellos con objetos reales
  • o, para permitir la inicialización con diferentes combinaciones de los 3 objetos (lo que significa que, para este caso, deberá pasar los parámetros individualmente)

Por 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 exponiendo el método GetService en público. En mi humilde opinión debe evitar esto, en lugar de mantener los "métodos de fábrica" explícitos. Entonces, ¿estaría bien implementar los métodos GetSomeService y GetAnotherService de mi ejemplo usando un localizador de servicios? En mi humilde opinión eso depende. Siempre y cuando la clase IContext siga siendo solo una simple fábrica abstracta con el propósito específico de proporcionar una lista explícita de objetos de servicio, es IMHO aceptable. Las fábricas abstractas suelen ser solo códigos de "pegamento", que no tienen que probarse por unidades. Sin embargo, debe preguntarse si en el contexto de métodos como GetSomeService , si realmente necesita un localizador de servicios, o si una llamada de constructor explícito no sería más simple.

Entonces, tenga cuidado, cuando se adhiere a un diseño en el que la implementación IContext es solo una envoltura alrededor de un método público genérico GetService , que permite resolver cualquier dependencia arbitraria por clases arbitrarias, entonces todo aplica lo que @BenjaminHodgson escribió. Su respuesta.

    
respondido por el Doc Brown 11.01.2015 - 14:05
15

Este diseño se conoce como Localizador de servicios * 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 las dependencias explícitamente) puede reemplazar su contenedor directamente por otro diferente, o regresar a new -expressions. Con tu IContext eso no es realmente posible.

El Localizador de servicios oculta las dependencias . Como cliente, es muy difícil decir lo que necesita para construir una instancia de una clase. Necesita algún tipo de IContext , pero también debe configurar el contexto para que devuelva los objetos correctos para que funcione el MyControllerBase . Esto no es en absoluto obvio a partir de la firma del constructor. Con DI normal, el compilador le dice exactamente lo que necesita. Si tu clase tiene muchas dependencias, deberías sentir ese dolor porque te incitará a refactorizar. Service Locator oculta los problemas con los malos diseños.

El Localizador de servicios causa errores en tiempo de ejecución . Si llama a GetService con un parámetro de tipo incorrecto, obtendrá una excepción. En otras palabras, su función GetService 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 te ayude y te diga cuándo tienes las dependencias incorrectas.

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, el Localizador de servicios se puede ver como si efectivamente tuviera un número infinito de métodos en la interfaz! Este argumento se explica en detalle aquí .

El localizador de servicios es difícil de probar . Ha dado un ejemplo de un IContext falso para las pruebas, 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 al problema de las clases con muchas dependencias, pero a la larga solo vas a hacer que tu vida sea miserable.

* Estoy definiendo Service Locator como un objeto con un método genérico Resolve<T> que es capaz de resolver dependencias arbitrarias y se usa en todo el código base (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 de dependencias conocidas) o Abstract Factory (un objeto que crea instancias de un solo tipo: el tipo de una Abstract Factory puede ser genérico, pero el método no es).

    
respondido por el Benjamin Hodgson 11.01.2015 - 13:28
5

Mark Seemann indica los mejores argumentos en contra del anti-patrón de Service Locator. así que no voy a profundizar mucho en por qué es una mala idea, es un viaje de aprendizaje que debes tomarte el tiempo de entender por ti mismo (también recomiendo El libro de Mark ).

Está bien, así que para responder la pregunta, volvamos a indicar su problema real :

  

Entonces, en lugar de agregar múltiples parámetros en el constructor para cada servicio (porque esto resultará realmente molesto y lento para los desarrolladores que extienden la aplicación) Estoy usando este método para obtener servicios.

Hay una pregunta que trata este tema en StackOverflow . Como se menciona en uno de los comentarios allí:

  

El mejor comentario: "Uno de los maravillosos beneficios de la Inyección de Constructor es que hace que las violaciones del Principio de Responsabilidad Única sean claramente evidentes".

Está buscando en el lugar equivocado la solución a su problema. Es importante saber cuándo una clase está haciendo demasiado. En su caso, sospecho fuertemente que no hay necesidad de un "Controlador Base". De hecho, en la POO casi nunca hay necesidad de herencia en absoluto . Las variaciones en el comportamiento y la funcionalidad compartida se pueden lograr completamente mediante el uso adecuado de las interfaces, lo que generalmente resulta en un código mejor factorizado y encapsulado, y no es necesario pasar las dependencias a los constructores de superclase.

En todos los proyectos en los que he trabajado donde hay un controlador básico, se realizó con el único propósito de compartir propiedades y métodos convenientes, como IsUserLoggedIn() y GetCurrentUserId() . DETENER . Este es un mal uso horrible de la herencia. En su lugar, cree un componente que exponga estos métodos y tome una dependencia de él donde lo necesite. De esta manera, sus componentes seguirán siendo verificables 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 este aquí pero la esencia del patrón es simple, eso los controladores en MVC deben hacer una sola cosa: manejar los argumentos que pasa el marco de trabajo de MVC, delegando otras preocupaciones a algún otro componente. De nuevo, este es el principio de responsabilidad única en el trabajo.

Realmente ayudaría conocer su caso de uso para emitir un juicio más preciso, pero honestamente no puedo pensar en ningún escenario en el que una clase base sea preferible a las dependencias bien calculadas.

    
respondido por el AlexFoxGill 14.01.2015 - 15:32
-2

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 en ello".

La "Fachada del servicio" de Doc Brown Acepté esta respuesta porque lo que estaba buscando (si la respuesta era "no") eran algunos ejemplos o alguna expansión de lo que estaba haciendo. Dio esto al sugerir que A) tiene un nombre, y B) probablemente hay mejores maneras de hacerlo.

Respuesta del "Localizador de servicios" de Benjamin Hodgson Por mucho que aprecie 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 USR

Voy a abordar esto con más detalle:

  

De esta manera, estás cediendo mucha información estática. Tu eres   aplazar las decisiones al tiempo de ejecución como lo hacen muchos lenguajes dinámicos. Ese   Cómo perder la verificación estática (seguridad), documentación y herramientas   Soporte (autocompletar, refactorizar, encontrar usos, flujo de datos).

No pierdo ninguna herramienta y no pierdo ninguna escritura "estática". La fachada de servicio devolverá lo que he configurado en mi contenedor DI, o default(T) . Y lo que devuelve es "tipado". La reflexión está encapsulada.

  

No veo por qué agregar servicios adicionales como argumentos de constructor es   una gran carga

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 controladores más.

  

Si utiliza un marco de inyección de dependencias, ni siquiera tendrá que   pasar manualmente en los valores de los parámetros. Entonces otra vez pierdes algo   Ventajas estáticas pero no tantas.

Estoy usando la inyección de dependencia. Ese es el punto.

Y, finalmente, el comentario de Jules sobre la respuesta de Benjamin No estoy perdiendo ninguna flexibilidad. Es mi fachada de servicio. Puedo agregar tantos parámetros a GetService<T> como quiera para distinguir entre diferentes implementaciones, tal como lo haría una al configurar un contenedor DI. Así, por ejemplo, podría cambiar GetService<T>() a GetService<T>(string extraInfo = null) para solucionar este "problema potencial".

De todos modos, gracias de nuevo a todos. Ha sido realmente útil. Saludos.

    
respondido por el LiverpoolsNumber9 14.01.2015 - 14:54

Lea otras preguntas en las etiquetas