¿Es ServiceLocator un antipatrón?

137

Recientemente leí el artículo de Mark Seemann sobre el antipatrón del Localizador de servicios.

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

  1. Problema de uso de la API (con el que estoy perfectamente de acuerdo)
    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)
    Considere el siguiente ejemplo

Tenemos una clase 'MyType' que emplea un enfoque de localización 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 importante o no. Debe comprender toda la aplicación en la que se utiliza el Localizador de servicios, y el compilador no lo ayudará.

Pero espere un segundo, si estuviéramos usando el enfoque DI, introduciríamos una dependencia con otro parámetro en el constructor (en caso de inyección del constructor). Y el problema seguirá ahí. Si nos olvidamos de configurar ServiceLocator, entonces 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 las pruebas unitarias. Pero, ¿no tendremos problemas con el enfoque DI? ¿No necesitaremos actualizar todas las pruebas que instanciaron esa clase? Los actualizaremos para pasar una nueva dependencia simulada 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. ¿Alguien podría disipar mis dudas?

ACTUALIZACIÓN (RESUMEN):

La respuesta a mi pregunta "¿Es el localizador de servicios un antipatrón" realmente depende de las circunstancias. Y definitivamente no sugeriría tacharlo de su lista de herramientas. Puede ser muy útil cuando comienzas a lidiar con código heredado. Si tiene la suerte de estar al comienzo de su proyecto, entonces el enfoque DI podría ser una mejor opción ya que tiene algunas ventajas sobre el Localizador de servicios.

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

  • Lo más obvio e importante: Service Locator oculta las dependencias de clase
  • Si está utilizando algún contenedor de IoC, es probable que escanee todo el constructor al inicio para validar todas las dependencias y le dará comentarios inmediatos sobre las asignaciones faltantes (o la configuración incorrecta); Esto no es posible si está utilizando su contenedor IoC como Localizador de servicios

Para más detalles lea las excelentes respuestas que se dan a continuación.

davidoff
fuente
"¿No necesitaremos actualizar todas las pruebas que instanciaron esa clase?" No necesariamente es cierto si está utilizando un generador en sus pruebas. En este caso, solo tendría que actualizar el generador.
Peter Karlsson
Tienes razón, depende. En grandes aplicaciones de Android, por ejemplo, hasta ahora las personas han sido muy reacias a usar DI debido a problemas de rendimiento en dispositivos móviles de baja especificación. En tales casos, debe encontrar una alternativa para seguir escribiendo código comprobable, y yo diría que Service Locator es un sustituto suficientemente bueno en ese caso. (Nota: las cosas pueden cambiar para Android cuando el nuevo marco Dagger 2.0 DI es lo suficientemente maduro.)
G. Lombard
1
Tenga en cuenta que, dado que esta pregunta se ha publicado, hay una actualización en el Localizador de servicios de Mark Seemann . Es una publicación antipatrón que describe cómo el localizador de servicios viola la OOP al romper la encapsulación, que es su mejor argumento hasta el momento (y la razón subyacente de todos los síntomas). que utilizó en todos los argumentos anteriores). Actualización 26/10/2015: El problema fundamental con Service Locator es que viola la encapsulación .
NightOwl888

Respuestas:

125

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

En cambio, tenemos que ver si hay usos válidos de los patrones, y para Service Locator hay varios casos de uso. Pero comencemos mirando 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 crea y usa esa clase:

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

No comprende que tiene dependencias si están ocultas mediante la ubicación del servicio. Ahora, si en su lugar usamos 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 esa misma razón. Debe ser el patrón a usar cuando no hay otras opciones.

¿Es el patrón un antipatrón?

No.

Por ejemplo, la inversión de los contenedores de control no funcionaría sin la ubicación del servicio. Es cómo 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? Así es, ubicación del servicio.

Tus preguntas

Pero espere un segundo, si estuviéramos usando el enfoque DI, introduciríamos una dependencia con otro parámetro en el constructor (en caso de 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 del servicio.
  2. ¿Cómo saber qué duración deben tener las dependencias y cómo / cuándo deben limpiarse?

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

Si nos olvidamos de configurar ServiceLocator, entonces 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 mejores contenedores también validan todas las dependencias al inicio (escaneando todos los constructores). Entonces, con esos contenedores obtienes el error de tiempo de ejecución directamente, y no en algún momento temporal posterior.

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

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

jgauffin
fuente
2
Jgauffin, gracias por tu respuesta. Usted señaló una cosa importante sobre la verificación automática al inicio. No pensé en eso y ahora veo otro beneficio de DI. También dio un ejemplo: "var myType = new MyType ()". Pero no puedo contarlo como válido ya que nunca instanciamos dependencias en una aplicación real (IoC Container lo hace por nosotros todo el tiempo). Es decir: en la aplicación MVC tenemos un controlador, que depende de IMyService y MyServiceImpl depende de IMyRepository. Nunca crearemos una instancia de MyRepository y MyService. Obtenemos instancias de los parámetros de Ctor (como de ServiceLocator) y los usamos. Nosotros no?
davidoff 01 de
33
Su único argumento para que el Localizador de servicios no sea un antipatrón es: "la inversión de los contenedores de control no funcionaría sin la ubicación del servicio". Sin embargo, este argumento no es válido, ya que Service Locator trata sobre intenciones y no sobre mecánica, como Mark Seemann explica claramente aquí : "Un contenedor DI encapsulado en una Raíz de Composición no es un Localizador de Servicio, es un componente de infraestructura".
Steven
44
@jgauffin Web API no utiliza la ubicación del servicio para DI en los controladores. No hace DI en absoluto. Lo que hace es: le da la opción de crear su propio ControllerActivator para pasar a los Servicios de configuración. A partir de ahí, puede crear una raíz de composición, ya sea Pure DI o Contenedores. Además, está combinando el uso de la Ubicación del servicio, como un componente de su patrón, con la definición del "Patrón del localizador de servicios". Con esa definición, Composition Root DI podría considerarse un "patrón de localización de servicios". Entonces, el punto completo de esa definición es discutible.
Suamere
1
Quiero señalar que esta es una respuesta generalmente buena, acabo de comentar un punto engañoso que hizo.
Suamere
2
@jgauffin DI y SL son versiones de IoC. SL es simplemente la forma incorrecta de hacerlo. Un contenedor de IoC podría ser SL, o podría usar DI. Depende de cómo está conectado. Pero SL es malo, malo malo. Es una forma de ocultar el hecho de que estás uniendo estrechamente todo.
MirroredFate
37

También me gustaría señalar que SI está refactorizando el código heredado, el patrón del Localizador de servicios no solo no es un antipatrón, sino que también es una necesidad práctica. Nadie jamás va a agitar 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 a una base de código existente, a menudo es el caso de que cambie las cosas para convertirse en servicios DI lentamente, y el código que hace referencia a estos servicios a menudo NO será servicios DI. Por lo tanto, ESOS servicios necesitarán usar el Localizador de servicios para obtener instancias de aquellos servicios que SE HAN convertido para usar DI.

Entonces, al refactorizar grandes aplicaciones heredadas para comenzar a usar conceptos DI, diría que no solo Service Locator NO es un antipatrón, sino que es la única forma de aplicar gradualmente los conceptos DI a la base del código.

jwells131313
fuente
12
Cuando se trata de código heredado, todo está justificado para sacarte de ese lío, incluso si eso significa tomar pasos intermedios (e imperfectos). El localizador de servicios es un paso intermedio de este tipo. Le permite salir de ese infierno un paso a la vez, siempre y cuando recuerde que existe una buena solución alternativa que está documentada, es repetible y ha demostrado ser efectiva . Esta solución alternativa es la inyección de dependencia y esta es precisamente la razón por la cual el Localizador de servicios sigue siendo un antipatrón; el software correctamente diseñado no lo usa.
Steven
RE: "Cuando se trata de código heredado, todo está justificado para sacarte de ese lío" A veces me pregunto si solo ha existido un poco de código heredado, pero debido a que podemos justificar cualquier cosa para solucionarlo, de alguna manera nunca logró hacerlo.
Drew Delano
8

Desde el punto de vista de la prueba, el Localizador de servicios es malo. Vea la explicación de Google Tech Talk de Misko Hevery con ejemplos de código http://youtu.be/RlfLCWKxHJ0 a partir del minuto 8:45. Me gustó su analogía: si necesita $ 25, solicite dinero directamente en lugar de darle su billetera de donde sacará 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.

usuario3511397
fuente
10
Esta es una opinión reciclada y hubiera sido mejor como comentario. Además, creo que su analogía (¿su?) Sirve para demostrar que algunos patrones son más apropiados para algunos problemas que otros.
8bitjunkie
6

Problema de mantenimiento (que me desconcierta)

Hay 2 razones diferentes por las que usar el localizador de servicios es malo a este respecto.

  1. En su ejemplo, está codificando una referencia estática al localizador de servicios en su clase. Esto acopla su clase directamente al localizador de servicios, lo que a su vez significa que no funcionará sin el localizador de servicios . Además, sus pruebas unitarias (y cualquier otra persona que use la clase) también dependen implícitamente del localizador de servicios. Una cosa que parece pasar desapercibida aquí es que cuando se usa la inyección del constructor no se necesita un contenedor DI cuando se realizan pruebas unitarias , lo que simplifica considerablemente las pruebas unitarias (y la capacidad de los desarrolladores para comprenderlas). Ese es el beneficio de las pruebas unitarias realizadas que obtiene al usar la inyección del 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 DI) . Con el tiempo, paga dividendos si puede mirar la definición del constructor para comprender las dependencias de una clase, en lugar de mirar la documentación (con suerte actualizada) o, en su defecto, volver al código fuente original (que podría no serlo). ser útil) para determinar cuáles son las dependencias de una clase. La clase con el localizador de servicios es generalmente más fácil de escribir , pero es más que pagar 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 acepte sus dependencias a través de su constructor.

Considere el caso en el que necesita usar un servicio del LibraryAque su autor decidió usarlo ServiceLocatorAy un servicio de LibraryBcuyo autor decidió usarlo ServiceLocatorB. No tenemos otra opción que usar 2 localizadores de servicios diferentes en nuestro proyecto. El número de dependencias que se deben configurar es un juego de adivinanzas si no tenemos buena documentación, código fuente o el autor en marcación rápida. Al fallar estas opciones, es posible que necesitemos usar un descompilador solopara averiguar cuáles son las dependencias. Es posible que necesitemos 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. Es posible que no sea posible compartir una instancia de una dependencia entre las dos bibliotecas. La complejidad del proyecto podría incluso agravarse aún más si los localizadores de servicios no residen realmente en las mismas bibliotecas que los servicios que necesitamos: estamos arrastrando implícitamente referencias de bibliotecas adicionales a nuestro proyecto.

Ahora considere los mismos dos servicios realizados con inyección de constructor. Agregar una referencia a LibraryA. Agregar una referencia a LibraryB. Proporcione las dependencias en su configuración DI (analizando lo que se necesita a través de Intellisense). Hecho.

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

NightOwl888
fuente
1

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 significa necesariamente que no pueda ser un antipatrón. Especialmente, cuando se trata de bibliotecas de terceros, no tiene control total sobre todos los aspectos y puede terminar usando la mejor solución.

Aquí hay un párrafo del Código adaptativo a través de C # :

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

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

Siavash Mortazavi
fuente
0

El autor razona que "el compilador no lo ayudará", y es cierto. Cuando diseñe una clase, tendrá que elegir cuidadosamente su interfaz, entre otros objetivos para que sea tan independiente como ... como tenga sentido.

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

  • implícitamente se comprueba, por lo que el compilador "ayuda".
  • También está eliminando la necesidad de que el cliente sepa algo sobre el "Localizador" o mecanismos similares, por lo que el cliente es en realidad más independiente.

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

Zrin
fuente
Zrin, gracias por tus pensamientos. Según tengo entendido, con un enfoque DI "adecuado" no debería crear instancias de mis dependencias en ninguna parte, excepto las pruebas unitarias. Entonces, el compilador me ayudará solo con mis pruebas. Pero como describí en mi pregunta original, esta "ayuda" con pruebas rotas no me da nada. ¿Lo hace?
davidoff
El argumento 'información estática' / 'verificación en tiempo de compilación' es un hombre de paja. Como ha señalado @davidoff, DI es igualmente susceptible a errores de tiempo de ejecución. También agregaría que los IDE modernos proporcionan vistas de información sobre comentarios / resumen de información para los miembros, e incluso en aquellos que no lo hacen, alguien seguirá buscando documentación hasta que simplemente "conozca" la API. La documentación es documentación, ya sea un parámetro de constructor requerido o una observación sobre cómo configurar una dependencia.
martes
Pensando en la implementación / codificación y el aseguramiento de la calidad: la legibilidad del código es crucial, especialmente para la definición de la interfaz. Si puede prescindir de las comprobaciones automáticas del tiempo de compilación y si comenta / documenta adecuadamente su interfaz, entonces supongo que puede remediar al menos parcialmente las desventajas de la dependencia oculta de un tipo de variable global con contenido no fácilmente visible / predecible. Diría que necesita buenas razones para usar este patrón que supere estas desventajas.
Zrin
0

Sí, el localizador de servicios es un antipatrón que viola la encapsulación y es sólido .

Alireza Rahmani Khalili
fuente
1
Entiendo lo que quieres decir, pero sería útil añadir más detalle, por ejemplo, por lo que viola SÓLIDO
reggaeguitar