patterns lista diseño antipatrones antipatron anti design-patterns dependency-injection anti-patterns service-locator

design patterns - lista - ¿ServiceLocator es un antipatrón?



lava flow antipatron (6)

Recientemente, leí el artículo de Mark Seemann sobre Service Locator anti-pattern.

El autor señala dos razones principales por las cuales ServiceLocator es un antipatrón:

  1. Problema de uso de API (que estoy perfectamente bien con)
    Cuando la clase emplea un localizador de servicios, es muy difícil ver sus dependencias, ya que, en la mayoría de los casos, la clase solo tiene un constructor PARAMETERLESS. A diferencia de ServiceLocator, el enfoque DI expone explícitamente las dependencias a través de los parámetros del constructor para que las dependencias se vean fácilmente en IntelliSense.

  2. Problema de mantenimiento (que me desconcierta)
    Considera el siguiente ejemplo

Tenemos una clase ''MyType'' que emplea un enfoque de localizador de servicios:

public class MyType { public void MyMethod() { var dep1 = Locator.Resolve<IDep1>(); dep1.DoSomething(); } }

Ahora queremos agregar otra dependencia a la clase ''MyType''

public class MyType { public void MyMethod() { var dep1 = Locator.Resolve<IDep1>(); dep1.DoSomething(); // new dependency var dep2 = Locator.Resolve<IDep2>(); dep2.DoSomething(); } }

Y aquí es donde comienza mi malentendido. El autor dice:

Se vuelve mucho más difícil saber si está introduciendo un cambio de rotura o no. Debe comprender la aplicación completa en la que se está utilizando el Localizador de servicios, y el compilador no lo ayudará.

Pero espere un segundo, si usáramos el enfoque DI, introduciríamos una dependencia con otro parámetro en el constructor (en el caso de la inyección del constructor). Y el problema seguirá ahí. Si olvidamos configurar ServiceLocator, podemos olvidar agregar una nueva asignación en nuestro contenedor IoC y el enfoque DI tendría el mismo problema de tiempo de ejecución.

Además, el autor mencionó las dificultades de la prueba unitaria. Pero, ¿no tendremos problemas con el enfoque DI? ¿No tendremos que actualizar todas las pruebas que crearon una instancia de esa clase? Los actualizaremos para pasar una nueva dependencia burlada solo para que nuestra prueba sea compilable. Y no veo ningún beneficio de esa actualización y gasto de tiempo.

No estoy tratando de defender el enfoque del localizador de servicios. Pero este malentendido me hace pensar que estoy perdiendo algo muy importante. ¿Podría alguien disipar mis dudas?

ACTUALIZACIÓN (RESUMEN):

La respuesta a mi pregunta "Is Service Locator an anti-pattern" depende de las circunstancias. Y definitivamente no sugeriría tacharlo de su lista de herramientas. Puede ser muy útil cuando empiezas a lidiar con el código heredado. Si tiene la suerte de estar al principio de su proyecto, entonces el enfoque DI puede ser una mejor opción, ya que tiene algunas ventajas sobre el Localizador de servicios.

Y aquí están las diferencias principales que me convencieron de no usar Service Locator para mis nuevos proyectos:

  • Más obvio e importante: el Localizador de servicios oculta las dependencias de clase
  • Si está utilizando algún contenedor IoC, es probable que escanee todo el constructor al inicio para validar todas las dependencias y darle retroalimentación inmediata sobre las asignaciones faltantes (o configuración incorrecta); esto no es posible si está utilizando su contenedor IoC como Localizador de servicios

Para detalles, lea las respuestas excelentes que se dan a continuación.


Problema de mantenimiento (que me desconcierta)

Hay dos razones diferentes por las cuales el uso del localizador de servicios es malo en este sentido.

  1. En su ejemplo, está codificando una referencia estática al localizador de servicios en su clase. Esto estrecha estrechamente su clase directamente con el localizador de servicios, lo que a su vez significa que no funcionará sin el localizador de servicios . Además, las pruebas de su unidad (y cualquier otra persona que use la clase) también dependen implícitamente del localizador de servicio. Una cosa que parece haber pasado desapercibida aquí es que al usar la inyección de constructor no necesita un contenedor DI cuando se prueba la unidad , lo que simplifica considerablemente las pruebas unitarias (y la capacidad de los desarrolladores para comprenderlas). Ese es el beneficio de prueba unitaria que obtiene al usar la inyección de constructor.
  2. En cuanto a por qué el constructor Intellisense es importante, la gente aquí parece haber perdido el punto por completo. Una clase se escribe una vez, pero se puede usar en varias aplicaciones (es decir, varias configuraciones de DI) . Con el tiempo, rinde dividendos si puede ver la definición del constructor para comprender las dependencias de una clase, en lugar de mirar la documentación (ojalá actualizada) o, en su defecto, volver al código fuente original (que podría no serlo). ser útil) para determinar las dependencias de una clase. La clase con el localizador de servicios generalmente es más fácil de escribir , pero usted más que paga el costo de esta conveniencia en el mantenimiento continuo del proyecto.

Claro y simple: una clase con un localizador de servicios es más difícil de reutilizar que una que acepta sus dependencias a través de su constructor.

Considere el caso donde necesita usar un servicio de la LibraryA que su autor decidió usar ServiceLocatorA y un servicio de la LibraryB cuyo autor decidió utilizar ServiceLocatorB . No tenemos más opción que usar 2 localizadores de servicio diferentes en nuestro proyecto. Cuántas dependencias deben configurarse es un juego de adivinanzas si no contamos con una buena documentación, código fuente o el autor en el marcado rápido. Al fallar estas opciones, podríamos necesitar usar un descompilador solo para descubrir cuáles son las dependencias. Es posible que tengamos que configurar 2 API de localizador de servicios completamente diferentes, y dependiendo del diseño, puede que no sea posible simplemente envolver su contenedor DI existente. Puede que no sea posible compartir una instancia de una dependencia entre las dos bibliotecas. La complejidad del proyecto podría agravarse aún más si los localizadores de servicios no residen realmente en las mismas bibliotecas que los servicios que necesitamos; estamos implícitamente arrastrando referencias de biblioteca adicionales a nuestro proyecto.

Ahora considere los mismos dos servicios hechos con inyección de constructor. Agregue una referencia a LibraryA . Agregue una referencia a LibraryB . Proporcione las dependencias en su configuración DI (mediante el análisis de lo que se necesita a través de Intellisense). Hecho.

Mark Seemann tiene una respuesta de que ilustra claramente este beneficio en forma gráfica , que no solo se aplica cuando se utiliza un localizador de servicios de otra biblioteca, sino también cuando se utilizan valores predeterminados extranjeros en los servicios.


Desde el punto de vista de la prueba, el Localizador de servicios es malo. Vea la explicación agradable de Google Tech Talk de Misko Hevery con ejemplos de código http://youtu.be/RlfLCWKxHJ0 comenzando en el minuto 8:45. Me gustó su analogía: si necesita $ 25, pida directamente dinero en lugar de darle su billetera desde donde se tomará el dinero. También compara Service Locator con un pajar que tiene la aguja que necesita y sabe cómo recuperarla. Las clases que usan Service Locator son difíciles de reutilizar debido a eso.


El autor razona que "el compilador no lo ayudará", y es cierto. Cuando defina una clase, querrá elegir cuidadosamente su interfaz, entre otros objetivos, para que sea tan independiente como ... como sea lógico.

Al hacer que el cliente acepte la referencia a un servicio (a una dependencia) a través de una interfaz explícita, usted

  • verifica implícitamente, por lo que el compilador "ayuda".
  • También elimina la necesidad de que el cliente sepa algo sobre el "Localizador" o mecanismos similares, por lo que el cliente es realmente más independiente.

Tiene razón en que DI tiene sus problemas / desventajas, pero las ventajas mencionadas las superan con creces ... IMO. Tiene razón, con DI hay una dependencia introducida en la interfaz (constructor), pero esperamos que sea la dependencia que necesita y que desea que sea visible y comprobable.


Mi conocimiento no es lo suficientemente bueno para juzgar esto, pero en general, creo que si algo tiene un uso en una situación particular, no necesariamente significa que no puede ser un antipatrón. Especialmente, cuando se trata de bibliotecas de terceros, no tiene control total sobre todos los aspectos y puede terminar utilizando la mejor solución.

Aquí hay un párrafo de Adaptive Code Via C # :

"Desafortunadamente, el localizador de servicios a veces es un antipatrón inevitable. En algunos tipos de aplicaciones, particularmente Windows Workflow Foundation, la infraestructura no se presta para la inyección de constructores. En estos casos, la única alternativa es usar un localizador de servicios. mejor que no inyectar dependencias en absoluto. Para todos mi virulencia contra el patrón (anti-), es infinitamente mejor que construir dependencias manualmente. Después de todo, todavía habilita esos importantísimos puntos de extensión proporcionados por interfaces que permiten decoradores, adaptadores, etc. y beneficios similares ".

- Hall, Gary McLean. Código adaptable a través de C #: codificación ágil con patrones de diseño y principios SOLIDOS (Referencia del desarrollador) (página 309). Educación Pearson.


Si define patrones como antipatrones solo porque hay algunas situaciones en las que no encaja, entonces SÍ es un patrón anti. Pero con ese razonamiento todos los patrones también serían anti patrones.

En cambio, tenemos que ver si hay usos válidos de los patrones, y para el Localizador de servicios hay varios casos de uso. Pero comencemos por mirar los ejemplos que ha dado.

public class MyType { public void MyMethod() { var dep1 = Locator.Resolve<IDep1>(); dep1.DoSomething(); // new dependency var dep2 = Locator.Resolve<IDep2>(); dep2.DoSomething(); } }

La pesadilla de mantenimiento con esa clase es que las dependencias están ocultas. Si creas y usas esa clase:

var myType = new MyType(); myType.MyMethod();

No comprende que tiene dependencias si están ocultas usando la ubicación del servicio. Ahora, si en cambio usamos la inyección de dependencia:

public class MyType { public MyType(IDep1 dep1, IDep2 dep2) { } public void MyMethod() { dep1.DoSomething(); // new dependency dep2.DoSomething(); } }

Puede detectar directamente las dependencias y no puede usar las clases antes de satisfacerlas.

En una aplicación de línea de negocio típica, debe evitar el uso de la ubicación del servicio por ese mismo motivo. Debería ser el patrón para usar cuando no hay otras opciones.

¿Es el patrón un antipatrón?

No.

Por ejemplo, la inversión de contenedores de control no funcionaría sin la ubicación del servicio. Es la forma en que resuelven los servicios internamente.

Pero un ejemplo mucho mejor es ASP.NET MVC y WebApi. ¿Qué crees que hace posible la inyección de dependencia en los controladores? Eso es correcto - ubicación del servicio.

Tus preguntas

Pero espere un segundo, si usáramos el enfoque DI, introduciríamos una dependencia con otro parámetro en el constructor (en el caso de la inyección del constructor). Y el problema seguirá ahí.

Hay dos problemas más serios:

  1. Con la ubicación del servicio también está agregando otra dependencia: el localizador de servicios.
  2. ¿Cómo se puede saber qué duración deberían tener las dependencias y cómo / cuándo deberían limpiarse?

Con la inyección de constructor usando un contenedor, obtienes eso gratis.

Si olvidamos configurar ServiceLocator, podemos olvidar agregar una nueva asignación en nuestro contenedor IoC y el enfoque DI tendría el mismo problema de tiempo de ejecución.

Es verdad. Pero con la inyección de constructor no tiene que escanear toda la clase para descubrir qué dependencias faltan.

Y algunos contenedores mejores también validan todas las dependencias al inicio (al escanear todos los constructores). Entonces con esos contenedores se obtiene el error de tiempo de ejecución directamente, y no en un punto temporal posterior.

Además, el autor mencionó las dificultades de la prueba unitaria. Pero, ¿no tendremos problemas con el enfoque DI?

No. Como no tiene una dependencia a un localizador de servicio estático. ¿Has intentado obtener pruebas paralelas trabajando con dependencias estáticas? No es divertido.


También me gustaría señalar que SI está refabricando el código heredado, el patrón de Localizador de servicios no solo no es un antipatrón sino que también es una necesidad práctica. Nadie va a mover una varita mágica sobre millones de líneas de código y de repente todo ese código estará listo para DI. Entonces, si desea comenzar a introducir DI en una base de código existente, a menudo cambiará las cosas para convertirlas en servicios DI lentamente, y el código que hace referencia a estos servicios a menudo NO será un servicio DI. Por lo tanto, ESOS servicios necesitarán utilizar el Localizador de Servicios para obtener instancias de aquellos servicios que HAN SIDO convertidos para usar DI.

Por lo tanto, al refaccionar aplicaciones legadas de gran tamaño para comenzar a utilizar los conceptos de DI, diría que el Localizador de servicios no es un antipatrón sino que es la única forma de aplicar gradualmente los conceptos de DI a la base de códigos.