Contenedores de inversión de control y el patrón inyección de dependencias

18. February 2013 08:40 by Oscar.SS in Programación  //  Tags:   //   Comments (4)

Con el permiso explícito de Martin Fowler he traducido este artículo con la intención de acercarlo para todos aquellos que se les resista la lengua de Shakespeare. En gran medida he respetado el texto original pero al mismo tiempo he intentado que la lectura sea lo más fluida posible. El lector encontrará algunos términos sin traducir, normalmente en letra itálica. El motivo ha sido que en la mayoría de los casos son términos por todos conocidos o que la traducción no aportaba realmente nada, muchas veces lo contrario.

Texto Original: http://www.martinfowler.com/articles/injection.html

Fecha de la traducción: 18/02/2013

 

Introducción e índice

En la comunidad Java ha habido una avalancha de contenedores ligeros que ayudan a ensamblar componentes de diferentes proyectos en una aplicación conjunta. Detrás de estos contenedores hay un patrón común que se encarga de las conexiones, un concepto denominado genéricamente "Inversión de Control". En este artículo profundizo sobre como funciona este patrón, bajo el nombre más específico de "Inyección de Dependencias", y lo contrasto con la alternativa "Localizador de Servicios". La elección entre estos dos enfoques es menos importante que el principio de separar la configuración del uso.

Una de las cosas más fascinantes del mundo empresarial Java es el gran número de actividades que buscan producir alternativas a las tecnologías oficiales de J2EE, la mayoría de las cuales tienen lugar en el mundo del código abierto. En gran medida es una reacción a la excesiva complejidad de la corriente principal de J2EE, pero gran parte se debe también a la exploración de alternativas y la creación de ideas creativas. Un problema común que se plantea, es cómo conectar entre sí los diferentes elementos: cómo pueden encajar juntos esta arquitectura de controlador web con el respaldo de aquella interfaz de base de datos cuando se construyeron por diferentes equipos con apenas contacto entre ellos. Una serie de frameworks han considerado este problema, y muchos se están expandiendo para encontrar una metodología genérica para el ensamblado de componentes de diversos orígenes. Estos son frecuentemente denominados como contenedores ligeros, algunos ejemplos son PicoContainer y Spring.

En la base de estos contenedores hay una serie de principios de diseño interesantes, cosas que van más allá de estos dos contenedores específicos, e incluso de la plataforma Java. En este artículo quiero comenzar a explorar algunos de estos principios. Los ejemplos utilizados son en Java, pero como en la mayor parte de mis escritos, los principios son igualmente aplicables a otros entornos orientados a objetos, en particular .NET

 

Componentes y Servicios

Un ejemplo sencillo

Inversión de Control

Formas de Inyección de Dependencias

Constructor Injection con PicoContainer

Setter Injection con Spring

Interface Injection

Usando un Localizador de Servicios

Usando una Interfaz Segregada para el Localizador

Un Localizador de Servicios Dinámico

Utilizando tanto un Localizador como Inyección con Avalon

Decidir que opción usar

Localizador de Servicios contra Inyección de Dependencias

Constructor Injection contra Setter Injection

Código o archivos de configuración

Separar la configuración del uso

Algunos temas adicionales

Reflexiones finales

Revisiones significativas

 

Componentes y Servicios

El tema de la conexión de elementos me lleva casi inmediatamente a los problemas espinosos de la terminología que rodea a los términos servicio y componente. Puedes encontrar fácilmente artículos largos y contradictorios en la definición de estos dos conceptos. Para mis propósitos en este artículo, a continuación hablaré sobre como uso estos términos ya de por si saturados.

Yo uso el termino componente para referirme a un conjunto de software diseñado para ser utilizado sin modificación alguna en una aplicación que está fuera del control de los desarrolladores del componente. Por "sin modificación", me refiero a que la aplicación no cambia el código fuente del componente, aunque se puede cambiar el comportamiento del componente extendiéndolo de alguna forma permitida por los creadores del componente.

Un servicio es similar a un componente en el sentido de es utilizado por aplicaciones externas. La principal diferencia es que espero que un componente se utilice locálmente (piense en un archivo jar, un ensamblado o una dll). Un servicio será utilizado remotamente a través de alguna interfaz remota, síncrona o asíncrona (ej: servicio Web, mensajería, RPC, o socket).

En este artículo me referiré principalmente a los servicios, pero gran parte de la misma lógica puede ser aplicada también a los componentes locales. De hecho, a menudo se necesita algún tipo de componente local para facilitar el acceso a un servicio remoto. Pero escribir "componente o servicio" es agotador tanto para leer como para escribir, y los servicios están más de moda por el momento.

 

Un ejemplo sencillo

Para hacer esto más concreto usaré un ejemplo corriente para hablar sobre este tema. Al igual que todos mis ejemplos, es uno de esos ejemplos super simples, tan pequeño como para parecer irreal, pero espero que sea suficiente para que podamos ver lo que está pasando sin tener en cuenta la complejidad de un ejemplo real.

En este ejemplo, estoy creando un componente que proporciona una lista de películas dirigidas por un director en particular. Esta funcionalidad increíblemente útil se lleva a cabo en un solo método.

        class MovieLister...
        {
            
public Movie[] moviesDirectedBy(String arg)
            {
                List allMovies 
finder.findAll();

                for 
(Iterator it allMovies.iterator()it.hasNext();)
                {
                    Movie movie 
(Movie) it.next();
                    if 
(!movie.getDirector().equals(arg)) it.remove();
                
}

                
return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
            
}
        }

La implementación de esta función es extremadamente trivial, pidiendo al objeto finder (sobre el que volveremos en breve) que devuelva todas las películas que conoce. Luego recorre esta lista para devolver sólo las películas de un director en particular. No voy a reparar esta particular porción de ingenuidad, ya que es sólo el andamiaje para el verdadero objetivo de este artículo.

El verdadero punto de este artículo es el objeto finder, o particularmente como podemos conectar la clase MovieLister con un objecto finder en particular. Esto es interesante porque quiero que mi maravilloso método moviesDirectedBy sea completamente independiente de como están siendo almacenadas todas la películas. Por lo tanto, todo lo que el método hace es referenciar al objeto finder, y lo único que hace el objeto finder es conocer como responder al método findAll. Puedo hacer esto definiendo de una interfaz para el objeto finder.

    public interface MovieFinder
    {
        List findAll()
;
    
}

Ahora todo esto está bien desacoplado, pero en algún momento tengo que conseguir una clase concreta para realmente obtener las películas. En este caso, pongo el código para hacer esto en el constructor de mi clase.

    class MovieLister...
    {
        
private MovieFinder finder;

        public 
MovieLister()
        {
            finder 
= new ColonDelimitedMovieFinder("movies1.txt");
        
}
    }

El nombre de la clase que implementa la interfaz MovieFinder viene del hecho de que estoy obteniendo mi lista desde un archivo delimitado por el caracter ':'. Les ahorraré los detalles, después de todo, el tema es que hay alguna implementación.

Ahora bien, si estoy usando esta clase símplemente para mí, todo es correcto y elegante. ¿Pero qué sucede cuando mis amigos están abrumados por el deseo de tener esta maravillosa funcionalidad y les gustaría una copia de mi programa?. Si ellos también almacenan sus listas de películas en un archivo de texto delimitado por caracteres ':' llamado "movies1.txt", entonces todo es maravilloso. Si ellos tienen un nombre diferente para su archivo de películas, podríamos poner luego el nombre del archivo en un archivo de propiedades. ¿Pero que pasa si ellos tiene un forma completamente diferente de almacenar sus listas de películas: una base de datos SQL, un archivo XML, un servicio Web, o simplemente otro formato en el archivo de texto?. En este caso necesitamos una clase diferente para recuperar los datos. Ahora, como he definido la interfaz MovieFinder, esto no cambiará mi método moviesDirectedBy. Pero todavía tengo que tener una manera de obtener una instancia de la correcta implementación del objeto finder.

Figura 1: Las dependencias mediante una simple creación en la clase MovieLister

La Figura 1 muestra las dependencias de esta situación. La clase MovieLister depende tanto de la interfaz MovieFinder como de su implementación concreta. Hubiéramos preferido que sólo dependiera de la interfaz, pero ¿cómo podemos obtener una instancia para trabajar?

En mi libro P of EAA, se describe esta situación como un plugin. La implementación de la clase para el objeto finder no está vinculada al programa en tiempo de compilación, ya que no sé lo que mis amigos van a utilizar. En su lugar, quiero que finder funcione en cualquier aplicación, y que dicha implementación se utilice luego fuera de mi control. El problema es cómo puedo hacer la conexión para que mi clase MovieLister sea ignorante de la implementación concreta del objeto finder, pero aún pueda tratar con una instancia para hacer su trabajo.

Expandiendo esto a un sistema real podemos tener docenas de servicios y componentes similares. En cada caso, se puede abstraer el uso de estos componentes que interactuan con ellos a través de una interfaz (y el uso de un adaptador, si el componente no es diseñado con una interfaz en la mente). Pero si queremos desplegar este sistema de diferentes maneras, tenemos que usar plugins para manejar la interacción con estos servicios, con lo que podemos utilizar diferentes implementaciones en diferentes despliegues.

Así que el problema central es, ¿cómo podemos ensamblar estos plugins en una aplicación?. Este es uno de los principales problemas que esta nueva generación de contenedores ligeros debe tratar de resolver, y universalmente todos ellos hacen esto usando Inversión de Control.

 

Inversión de Control

Cuando la gente habla acerca de cómo estos contenedores son tan útiles para la aplicación de "Inversión de Control", me siento desconcertado. Inversión de Control es una característica común en los frameworks, así que decir que estos contenedores ligeros son especiales debido a que utilizan la Inversión de Control es como decir que mi coche es especial porque tiene ruedas.

La pregunta es, ¿qué aspecto del control están invirtiendo? La primera vez que me tope con Inversión de Control, era el control principal de una interfaz de usuario. En sus inicios las interfaces de usuario eran controladas por el programa de la aplicación. Teníamos una secuencia de comandos parecida a "introducir el nombre" o "introducir la dirección", y tu programa procesaba la entrada de datos para responder a cada una. Con las interfaces de usuario gráficas el framework de interfaz de usuario contendría este bucle principal, y en su lugar tu programa proporcionaba manejadores de eventos para los distintos campos de la pantalla. El control principal del programa fue invertido, se apartó de ti para moverse hacia el framework.

Para este nuevo lote de contenedores, la inversión es sobre cómo buscan la implementación de un plugin. En mi sencillo ejemplo, la clase MovieLister obtenía la implementación del objeto finder directamente instanciándolo. Esto evita que el finder sea un plugin. El enfoque que utilizan estos contenedores es asegurar que cualquier usuario de un plugin sigue alguna convención que permite a un módulo independiente de ensamblado inyectar la lista de objetos.

Como resultado creo que necesitamos un nombre más específico para este patrón. Inversión de Control es un termino demasiado genérico, y por lo consiguiente la gente lo encuentra confuso. Como resultado de muchas discusiones con varios defensores de IoC acordamos denominarlo Inyección de Dependencias.

Voy a empezar hablando de varias formas de Inyección de Dependencias, pero ser conscientes de que este no es el único camino para eliminar las dependencias de la clase de aplicación hacia la implementación de un plugin. El otro patrón que puedes usar es el Localizador de Servicios, y discutiré esto más tarde, una vez terminada la explicación sobre Inyección de Dependencias.

 

Formas de Inyección de Dependencias

La idea básica de la Inyección de Dependencia es tener un objeto separado, un ensamblador, que puebla un campo en la clase MovieLister con una implementación apropiada para la interfaz MovieFinder, resultando un diagrama de dependencia cómo en la Figura 2.

Figura 2: Las dependencias para una Inyección de Dependencias

Principalmente existen 3 formas de Inyección de Dependencias. Los nombres que uso para ellos son Constructor Injection, Setter Injection, y Interface Injection. Si lees sobre estos temas en los debates actuales sobre Inversión de Control, oirás nombrarlos como Tipo 1 IoC (interface injection), Tipo 2 IoC (setter injection) y Tipo 3 IoC (constructor injection). Encuentro particularmente difícil recordar los nombres numéricos, así que he utilizado los nombres aquí definidos.

 

Constructor Injection con PicoContainer

Voy a comenzar mostrando cómo se realiza esta inyección utilizando un contenedor ligero llamado PicoContainer. Empiezo por aquí principalmente porque muchos de mis colegas en ThoughtWorks son muy activos en el desarrollo de PicoContainer (sí, esto es un tipo de nepotismo corporativo).

PicoContainer usa un constructor para decidir como inyectar la implementación del objeto finder dentro de la clase MovieLister. Para que esto funcione, la clase MovieLister necesita declarar un constructor que incluye todo todo lo que necesita ser inyectado.

    class MovieLister...
    {
        
public MovieLister(MovieFinder finder) 
        {
            
this.finder finder;       
        
}
    }

El propio objeto finder será también gestionado por el contenedor PicoContainer, tal que tendremos el nombre del archivo de texto inyectado por el contenedor.

    class ColonMovieFinder...
    {
        
public ColonMovieFinder(String filename) 
        {
            
this.filename filename;
        
}
    }

PicoContainer entonces necesita ser informado de que implementación de clase asociar a cada interfaz y que string inyectar en el objeto finder.

    private MutablePicoContainer configureContainer() 
    {
        MutablePicoContainer pico 
= new DefaultPicoContainer();

        
Parameter[] finderParams =  
            
{
                
new ConstantParameter("movies1.txt")
            }
;

        
pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
        
        
pico.registerComponentImplementation(MovieLister.class);
        
        return 
pico;
    
}

Esta configuración de código se realiza normalmente en una clase diferente. Para nuestro ejemplo, cada amigo que use mi clase MovieLister podría escribir el código de configuración apropiado en alguna clase de inicialización de su propiedad . Por supuesto, es común mantener este tipo de información de configuración en un archivo config separado. Puedes escribir un clase para leer el archivo config y configurar el contenedor apropiadamente. Aunque PicoContainer no contienen esta característica, existe un proyecto relacionado llamado NanoContainer que proporciona los wrappers adecuados para permitirte tener un archivo de configuración XML. Dicho NanoContainer interpretará el XML para luego configurar un contendor PicoContainer. La filosofía del proyecto es separar el formato del archivo de configuración del mecanismo interno.

Para utilizar el contenedor se necesita un código como este.

    public void testWithPico() 
    {
        MutablePicoContainer pico 
configureContainer();
        
        
MovieLister lister (MovieLister) pico.getComponentInstance(MovieLister.class);
        
        
Movie[] movies lister.moviesDirectedBy("Sergio Leone");
        
        
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
    
}

Aunque en este ejemplo he utilizado Constructor Injection, PicoContainer también soporta Setter Injection, sin embargo sus desarrolladores prefieren el primero.

 

Setter Injection con Spring

Spring es un extenso framework para el desarrollo empresarial Java. Incluye capas de abstracción para transacciones, frameworks de persistencia, desarrollo de aplicaciones Web y JDBC. Al igual que PicoContainer, soporta tanto Constructor Injection como Setter Injection, pero sus desarrolladores tienden a preferir Setter Injection, lo que lo convierte en una opción apropiada para este ejemplo.

Para conseguir que mi clase MovieLister acepte la inyección defino un método Set para este servicio.

    class MovieLister...
    {
        
private MovieFinder finder;
        
        public void 
setFinder(MovieFinder finder) 
        {
            
this.finder finder;
        
}
    }

Similarmente defino un método Set para el nombre de archivo.

    class ColonMovieFinder...
    {    
        
public void setFilename(String filename) 
        {
            
this.filename filename;
        
}
    }

El tercer paso es ajustar la configuración para los archivos. how do you feel after taking the abortion pill Spring soporta la configuración a través de archivos XML y también a través de código, pero XML es la manera esperada para hacerlo.

    <beans>
        
<bean id="MovieLister" class="spring.MovieLister">
            
<property name="finder">
                
<ref local="MovieFinder"/>
            </
property>
        
</bean>
        
<bean id="MovieFinder" class="spring.ColonMovieFinder">
            
<property name="filename">
                
<value>movies1.txt</value>
            
</property>
        
</bean>
    
</beans>

Luego el test es similar a esto.

        public void testWithSpring() throws Exception 
        {
            ApplicationContext ctx 
= new FileSystemXmlApplicationContext("spring.xml");

            
MovieLister lister (MovieLister) ctx.getBean("MovieLister");

            
Movie[] movies lister.moviesDirectedBy("Sergio Leone");

            
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
        
}

 

Interface Injection

La tercera técnica de inyección es definir y utilizar interfaces para la inyección. Avalon es un ejemplo de framework que utiliza esta técnica. Hablaré un poco más sobre esto después, pero en este caso lo voy a usar con unos simples ejemplos de código.

Con esta técnica, comienzo por definir la interfaz que voy a utilizar para realizar la inyección. Aquí está la interfaz para inyectar un MovieFinder a un objeto.

    public interface InjectFinder
    {
        
void injectFinder(MovieFinder finder);
    
}

Esta interfaz sería definida por cualquiera que proporcione la interfaz MovieFinder. Necesita ser implementada por cualquier clase que quiera usar el objeto finder, como la clase MovieLister.

    class MovieLister implements InjectFinder...
    {
        
public void injectFinder(MovieFinder finder) 
        {
            
this.finder finder;
        
}
    }

Uso un enfoque similar para inyectar el nombre del archivo en la implementación del objeto finder.

    public interface InjectFinderFilename 
    {
        
void injectFilename (String filename);
    
}

    
class ColonMovieFinder implements MovieFinder, InjectFinderFilename......
    {
        
public void injectFilename(String filename) 
        {
            
this.filename filename;
        
}
    }

Entonces, como de costumbre, necesito algún código de configuración para conectar las implementaciones. Por simplicidad lo haré en el código.

    class Tester...
    {
        
private Container container;

         private void 
configureContainer() 
         {
           container 
= new Container();
           
registerComponents();
           
registerInjectors();
           
container.start();
        
}
    }

Esta configuración tiene dos etapas. El registro de la identificación de los componentes clave es muy similar a los otros ejemplos.

    class Tester...
    {
          
private void registerComponents() 
          {
            container.registerComponent(
"MovieLister", MovieLister.class);
            
container.registerComponent("MovieFinder", ColonMovieFinder.class);
          
}
    }

Un nuevo paso es registrar los inyectores que inyectarán los componentes dependientes. Cada interfaz de inyección necesita código para inyectar el objeto dependiente. Aquí hago esto registrando objetos inyectores con el contenedor. Cada uno de los objetos inyectores implementa la interfaz del inyector.

    class Tester...
    {
          
private void registerInjectors() 
          {
            container.registerInjector(InjectFinder.
class, container.lookup("MovieFinder"));
            
container.registerInjector(InjectFinderFilename.classnew FinderFilenameInjector());
          
}
    }

    
public interface Injector 
    {
        
public void inject(Object target);
    
}

Cuando la dependencia es una clase escrita para este contenedor, tiene sentido para el contenedor implementar la interface Injector, tal y como hago aquí con la clase MovieFinder. Para clases genéricas, tal como string, uso una clase interna dentro del código de configuración.

    class ColonMovieFinder implements Injector......
    {
        
public void inject(Object target) 
        {
            ((InjectFinder) target).injectFinder(
this);        
        
}
    }

    
class Tester...
    {
        
public static class FinderFilenameInjector implements Injector 
        {
            
public void inject(Object target) 
            {
                ((InjectFinderFilename)target).injectFilename(
"movies1.txt");      
            
}
        }
    }

Luego los tests usan el contenedor.

    class IfaceTester...
    {
        
public void testIface() 
        {
          configureContainer()
;

          
MovieLister lister (MovieLister)container.lookup("MovieLister");
          
Movie[] movies lister.moviesDirectedBy("Sergio Leone");
          
          
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
        
}
    }

El contenedor utiliza las interfaces declaradas de inyección para descubrir las dependencias e inyectores para inyectar las dependientes correctas. (La implementación específica del contenedor que hice aquí no es importante para la técnica, y no lo voy a mostrar porque te reirías)

 

Usando un Localizador de Servicios

El principal beneficio de la Inyección de Dependencia es que elimina la dependencia que la clase MovieLister tiene con la implementación concreta de MovieFinder. Esto me permite compartir con amigos clases MovieLister, y ellos pueden conectar las implementaciones adecuadas para su entorno. La Inyección de Dependencia no es la única manera de romper estas dependencias, otra es utilizar un Localizador de Servicios.

La idea básica detrás de un Localizador de Servicios es tener un objeto que sabe cómo obtener todos los servicios que una aplicación puede necesitar. Así que, el Localizador de Servicios para esta aplicación tendría un método que devuelve un objeto finder cuando esto es necesario. Por supuesto, esto sólo traslada esta carga un poco, todavía tenemos que obtener el localizador en el objeto lister, dando lugar a las dependencias de la Figura 3.

Figura 3: Depencencias para un Localizador de Servicios

En este caso, voy a utilizar el Localizador de Servicios como un Registry único. La clase MovieLister entonces se puede utilizar para obtener el objeto finder cuando es instanciado.

    class MovieLister...
    {
        
private MovieFinder finder ServiceLocator.movieFinder();
    
}

    
class ServiceLocator...
    {
        
public static MovieFinder movieFinder()
        {
            
return soleInstance.movieFinder;
        
}

        
private static ServiceLocator soleInstance;
        private 
MovieFinder movieFinder;
    
}

Al igual que en el enfoque de Inyección de Dependencia, tenemos que configurar el Localizador de Servicios. Aquí lo voy a hacer en el código, pero no es difícil usar un mecanismo que lea los datos adecuados desde un archivo de configuración.

    class Tester...
    {
        
private void configure()
        {
            ServiceLocator.load(
new ServiceLocator(new ColonMovieFinder("movies1.txt")));
        
}
    }

    
class ServiceLocator...
    {
        
public static void load(ServiceLocator arg)
        {
            soleInstance 
arg;
        
}

        
public ServiceLocator(MovieFinder movieFinder)
        {
            
this.movieFinder movieFinder;
        
}
    }

Aquí está el código de test.

    class Tester...
    {
        
public void testSimple()
        {
            configure()
;
            
MovieLister lister = new MovieLister();
            
Movie[] movies lister.moviesDirectedBy("Sergio Leone");
            
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
        
}
    }

Muchas veces he oído quejas sobre que este tipo de Localizador de Servicios no son buenos porque no permiten tests ya que no es posible reemplazar sus implementaciones. Ciertamente, puedes diseñarlos mal y entrar en este tipo de dificultades, pero tu no tienes porque hacerlo. En este caso, la instancia del Localizador de Servicios es sólo un simple contenedor para los datos. Puedo crear fácilmente el Localizador de Servicios con las implementaciones de prueba de mis servicios.

Para un localizador más sofisticado puedo heredar del Localizador de Servicios y pasar esta subclase en la variable de la clase de registro. Puedo cambiar los métodos estáticos para llamar a un método en la instancia en lugar de acceder directamente a las variables de instancia. Puedo proporcionar localizadores para un hilo específico usando una ubicación de almacenamiento especifica para el hilo. Todo esto puede ser realizado sin cambios en los clientes del Localizador de Servicios.

Una forma de pensar en esto es que el Localizador de Servicios es un registro, no un singleton. Un singleton proporciona una forma sencilla de implementar un resgistro, pero esta decisión de implementación se puede cambiar fácilmente.

 

Usando una Interfaz Segregada para el Localizador

Uno de los problemas con el sencillo enfoque anterior, es que MovieLister depende de la clase del Localizador de Servicios, incluso si se utiliza un solo servicio. Podemos reducir esto utilizando una interfaz segregada. De esta forma, en lugar de usar la interfaz completa del Localizador de Servicios, el lister puede declarar solo parte de la interfaz que necesita.

En esta situación el proveedor del lister también proveería una interfaz localizador la cual necesita para obtener el finder.

    public interface MovieFinderLocator
    {
        
public MovieFinder movieFinder();
    
}

El localizador entonces debe implementar esta interfaz para proporcionar acceso al finder.

         MovieFinderLocator locator ServiceLocator.locator();
         
MovieFinder finder locator.movieFinder();
           
         public static 
ServiceLocator locator() 
         {
            
return soleInstance;
         
}

         
public MovieFinder movieFinder() 
         {
            
return movieFinder;
         
}

         
private static ServiceLocator soleInstance;
         private 
MovieFinder movieFinder;

Notarás que dado que queremos utilizar una interfaz, no podemos acceder simplemente a los servicios a través de métodos estáticos. Tenemos que utilizar la clase para obtener una instancia del localizador y luego usarla para conseguir lo que necesitemos.

 

Un Localizador de Servicios dinámico

El ejemplo anterior era estático, en donde la clase del Localizador de Servicios dispone de métodos para cada servicio que necesitas. Esta no es la única manera de hacer esto, también puedes hacer un abortion pill online Localizador de Servicios dinámico que te permita agrupar todos los servicios que necesitas y realizar sus selecciones en tiempo de ejecución.

En este caso, el Localizador de Servicios utiliza un mapa en lugar de campos para cada servicio, y proporciona métodos genéricos para la obtención y carga de los servicios.

    class ServiceLocator...
    {
        
private static ServiceLocator soleInstance;

        public static void 
load(ServiceLocator arg)
        {
            soleInstance 
arg;
        
}

        
private Map services = new HashMap();

        public static 
Object getService(String key)
        {
            
return soleInstance.services.get(key);
        
}

        
public void loadService(String key, Object service)
        {
            services.put(key, service)
;
        
}
    }

La configuración consiste en cargar el servicio con una key apropiada. 

    class Tester...
    {
        
private void configure()
        {
            ServiceLocator locator 
= new ServiceLocator();
            
locator.loadService("MovieFinder"new ColonMovieFinder("movies1.txt"));
            
ServiceLocator.load(locator);
        
}
    }

Puedo utilizar el servicio usando el mismo key string.

    class MovieLister...
    {
        
private MovieFinder finder (MovieFinder) ServiceLocator.getService("MovieFinder");
    
}

En general no me gusta este enfoque. A pesar de que es muy flexible, no es muy explícito. La única forma en la que puedo enterarme como alcanzar un servicio es a través de keys textuales. Prefiero métodos explícitos ya que es más fácil encontrar dónde están estos mirando las definiciones de la interfaz.

 

Utilizando tanto un localizador como una inyección con Avalon

La Inyección de Dependencias y el Localizador de Servicios no son necesariamente conceptos mutuamente excluyentes. Un buen ejemplo usando ambos al mismo tiempo es el framework Avalon. Avalon usa un Localizador de Servicios, pero usa inyección para decirle a los componentes donde encontrar el localizador.

Berin Loritsch me envió esta sencilla versión de mi ejemplo usando Avalon.

    public class MyMovieLister implements MovieLister, Serviceable
    {
        
private MovieFinder finder;

        public void 
service(ServiceManager manager) throws ServiceException
        {
            finder  
( MovieFinder ) manager.lookup("finder");
        
}
    }

El método service es un ejemplo de inyección de interfaz, permitiendo al contenedor inyectar un gestor de servicios en MyMovieLister. El gestor de servicios es un ejemplo de un Localizador de Servicios. En este ejemplo el lister no almacena el gestor en un campo, en su lugar, lo usa inmediatamente para encontrar el finder.

 

Decidir que opción usar

Hasta ahora me he concentrado en explicar como veo estos patrones y sus variantes. Ahora puedo hablar sobre sus pros y contras para ayudar a encontrar las respuesta a cual usar y cuando.

 

Localizador de Servicios contra Inyección de Dependencias

La principal elección es entre el Localizador de Servicios y la Inyección de Dependencias. La idea principal es que estas dos implementaciones proporcionan el desacople fundamental que no contenía el sencillo ejemplo del principio, en ambos casos, el código de la aplicación es indepenciente de la concreta implementación de la interfaz de servicio. La diferencias más importante entre estos dos patrones es como esa implementación es proporcionada a la clase de la aplicación. Con el Localizador de Servicios la clase de la aplicación lo pregunta explicitamente con un mensaje en el localizador. Con la Inyección de Dependencias no hay una petición explicita, el servicio aparece en la clase de aplicación, de ahí la Inversión de Control.

La Inversión de Control es una característica común en los frameworks, pero esto es algo que tiene un precio. Tiende a ser difícil de entender y proporciona algunos problemas cuando tratas de depurarlo. Así que en general prefiero evitarlo a menos que lo necesite. Esto no es lo mismo que decir que es algo malo, simplemente pienso que necesita justificarse sobre una alternativa más directa.

Usar Inyección de Dependiencias puede ayudar a que sea más fácil ver las dependencias de los componentes. Con el Inyector de Dependencias puedes simplemente mirar los mecanismos de inyección, tales como el constructor, y mirar las dependencias. Con el Localizador de Servicios tienes que buscar el código fuente de las llamadas al localizador. Los modernos IDEs (Entornos de Desarrollo Intregrado) con funciones de búsqueda de referencias hacen esto más fácil, pero todavía no es tan fácil como mirar el constructor o los métodos de configuración.

Gran parte de esto depende de la naturaleza del usuario del servicio. Si estás construyendo una aplicación con varias clases que utilizan un servicio, entonces una dependencia desde las clases de la aplicación para el localizador no es un gran problema. En mi ejemplo de ofrecer un listador de películas para mis amigos, usar un Localizador de Servicios funciona bastante bien. Todo lo que ellos necesitan hacer es configurar el localizador para enganchar las correctas implementaciones de los servicios, ya sea a través de algún código de configuración o a través de un archivo de configuración. En este tipo de escenario no veo que la inversión de inyectores proporcione algo convincente.

La diferencia viene si el listador de películas es un componente que estoy proporcionando a una aplicación que otras personas están escribiendo. En este caso, no sé mucho sobre las APIs de los Localizadores de Servicios que mis clientes van ha utilizar. Cada cliente podría tener sus propios Localizadores de Servicios incompatibles. Puedo esforzarme en hacer algo de esto usando la segregación de una interfaz. Cada cliente puede escribir un adaptador para el localizador que coincida con mi interfaz, pero en cualquier caso todavía necesito ver el primer localizador para buscar mi específica interfaz. Una vez que el adaptador aparece, entonces la simplicidad de la conexión directa para el localizador empieza a tambalearse.

Debido a que tienes una Inyección de Dependencia del componente inyector, el componente no puede obtener servicios adicionales desde el inyector una vez ha sido configurado.

Una razón común que la gente da para preferir la Inyección de Dependencias es que esto facilita los tests. El asunto aquí es que para realizar tests, necesitas reemplazar fácilmente las implementaciones reales de los servicios con stubs o mocks. Sin embargo, realmente no hay diferencia aquí entre la Inyección de Dependencias y el Localizador de Servicios: ambos están muy dispuestos para stubs. Sospecho que esta observación proviene de proyectos en los que la gente no hace el esfuerzo para asegurar que su Localizador de Servicios pueda ser fácilmente sustituible. Aquí es donde la prueba continua ayuda, si no puedes preparar fácilmente los servicios para ser testeados, entonces esto implica un serio problema con tu diseño.

Por supuesto el problema de los tests son exacerbados por entornos de componentes que  son muy intrusivos, tales como el framework Java EJB. Mi opinión es que este tipo de frameworks debe reducir su impacto en el código de la aplicación, y particularmente no deben hacer cosas que ralentizan el ciclo de edición-ejecución. Utilizar plugins para sustituir componentes pesados hace mucho para ayudar en este proceso, el cual es vital para prácticas como TDD.

Así que el primer poblema es para aquellos desarrolladores que están escribiendo código que espera ser usado en aplicaciones fuera del control de sus desarrolladores. En estos casos, incluso una mínima suposición sobre un Localizador de Servicios es un problema.

 

Constructor Injection contra Setter Injection

Para la combinación de servicios siempre tienes que tener alguna convención con el fin de conectar las cosas. La principal ventaja de la inyección es que requiere una convención muy simple, al menos para el Constructor Injection y el Setter Injection. No tienes que hacer nada raro en tu componente y es bastante sencillo para un inyector conseguir que todo esté configurado. 

Inyection Interface es más intrusiva ya que tienes que escribir muchas interfaces para solucionar todo. Para un conjunto pequeño de interfaces requeridas por el contenedor, como en el enfoque de Avalon, no es tan malo. Pero es mucho trabajo para el ensamblado de componentes y dependencias, por lo que la actual cosecha de contenedores ligeros trabajan con Setter Injection y Constructor Injection.

La elección entre Setter Injection y Constructor Injection es interesante ya que refleja un problema más general de la programación orientada a objetos - debes de llenar los campos en un constructor o con setters.

Por defecto mi larga experiencia con los objetos es crear, tanto sea posible, objetos validos en el momento de la construcción. Esta sugerencia nos devuelve a las mejores prácticas de patrones con Smalltalk de Kent Beck: Constructores y constructores con parámetros. Lo constructores con parámetros proporcionan una idea clara de lo que significa crear un objeto válido en un lugar visible. Si hay más de una forma para hacerlo, basta con crear más constructores que muestran las diferentes combinaciones.

Otra ventaja de la inicialización de constructores es que te permite ocultar claramente ocultar cualquier campo que sea inmutable simplemente no proporcionando un setter. Creo que esto es importante - si algo no debe cambiar la falta de un setter transmite muy bien esto. Si utilizas setters para la inicialización, entonces esto puede llegar a ser doloroso. (De hecho, en estas situaciones prefiero evitar la convención usual de ajuste, preferiría un método como initFoo, para enfatizar que se trata de algo que debes hacer solo al comienzo).

Pero con cualquier situación hay excepciones. Si tienes muchos parámetros del constructor las cosas pueden parecer desordenadas, especialmente en lenguajes sin palabras clave para parámetros. Es cierto que un extenso constructor es a menudo una señal de un objecto sobre cargado que debe ser dividido, pero hay casos donde eso es lo que necesitas.

Si tienes muchas formas de construir un objeto válido, puede ser difícil demostrar esto a través de constructores, ya que los constructores sólo pueden variar en el número y el tipo de parámetros. Aquí es donde los Factory Methods entran en juego, estos pueden utilizar una combinación de constructores privados y setters para realizar su trabajo. El problema con los clásicos Factory Methods para el ensamblado de componentes es que por lo general son vistos como métodos estáticos, y no puedes tener las interfaces. Puedes hacer una clase factory, pero entonces eso simplemente se convierte en otra instancia de servicio. Un servicio de factory es a menudo una buena táctica, pero todavía tienes que instanciar la factory utilizando una de las técnicas presentadas aquí.

Los constructores también sufren si tienes parámetros simples tales como strings. Con Setter Injection puedes dar a cada setter un nombre para indicar lo que el string debería hacer. Con los constructores estás dependiendo de la posición, lo que es más difícil de seguir.

Si tienes múltiples constructores y herencia, entonces las cosas pueden ser particularmente incómodas. Para inicializar todo tienes que proporcionar constructores para invocar a cada constructor de la superclase, y al mismo tiempo añadir tus argumentos. Esto puede conducir a una explosión de constructores incluso mayor.

A pesar de estas desventajas mi preferencia es comenzar con Constructor Inyection, pero estar preparado para cambiar a Setter Inyection tan pronto como los problemas que he descrito anteriormente comiencen a convertirse en un problema.

Este argumento ha dado lugar a un gran debate entre los diferentes equipos que proporcionan inyección de dependencias como parte de su framework. Sin embargo parece que la mayoría de la gente que crea estos frameworks se ha dado cuenta que es importante soportar ambos mecanismos, incluso si tienen una preferencia por uno de ellos.

 

Código o archivos de configuración

Una cuestión distinta pero a menudo mezclada es si utilizar archivos de configuración o el código de una API para conectar los servicios. Para la mayoría de las aplicaciones que pueden desplegarse en muchos lugares, un archivo de configuración independiente por lo general tiene más sentido. La mayor parte del tiempo será un archivo XML, y esto tiene sentido. Sin embargo, hay casos en donde es más fácil usar código para el ensamblado de los componentes. Uno de estos casos es cuando tienes una sencilla aplicación que no tiene demasiada variación en el despliegue. En este caso un poco de código puede ser más claro que un archivo XML independiente.

Un caso opuesto es cuando el ensamblado es bastante complejo, incluyendo pasos condicionales. Una vez que empiezas a acercarte a un lenguaje de programación luego XML empieza a dejar de trabajar y es mejor usar un lenguaje real que tiene toda la sintaxis para escribir un programa claro. Escribes por lo tanto una clase constructora que realiza el ensamblado. Si tienes distintos escenarios de construcción puedes proporcionar varias clases constructoras y usar un simple archivo de configuración para seleccionar entre ellas.

A menudo pienso que la gente es demasiado entusiasta definiendo archivos de configuración. Con frecuencia un lenguaje de programación proporciona un mecanismo de configuración directo y poderoso. Los lenguajes modernos pueden fácilmente compilar pequeños ensamblados que pueden ser usados para ensamblar plugins para sistemas grandes. Si la compilación es un dolor, entonces hay lenguajes de script que funciona igual de bien.

A menudo se dice que los archivos de configuración no deben usar un lenguaje de programación porque necesitan ser editados por no programadores. Pero, ¿como de frecuente es este caso?. ¿Espera la gente realmente que los no programadores modifiquen los niveles de aislamiento de una compleja aplicación del lado del servidor?. Los archivos de configuración "sin lenguaje" funcionan bien solo si tienden a ser simples. Si se vuelven complejos, entonces es el momento de pensar en usar un apropiado lenguaje de programación.

Una cosa que estamos viendo en el mundo Java en este momento es una cacofonía de los archivos de configuración, donde todo componente tiene sus propios archivos de configuración que son diferentes a todos los demás. Si usas una docena de estos componentes, puedes fácilmente terminar con una docena de archivos de configuración para mantener sincronizados.

Mi consejo aquí es proporcionar siempre un forma de realizar toda la configuración fácilmente con una interfaz programática, y entonces tratar un archivo de configuración independiente como una característica opcional. Puedes fácilmente construir un archivo de configuración manejando el uso de la interfaz programática. Si estás escribiendo un componente entonces deja a tu usuario decidir si usar la interfaz programática, el formato de tu archivo de configuración, o escribir su propios formato de archivo de configuración y atar esto con la interfaz programática.

 

Separar la configuración del uso

La cuestión importante en todo esto es asegurar que la configuración de los servicios está separada de su uso. De hecho, esto es un principio de diseño fundamental que cae bien con la separación de interfaces de implementación. Esto es algo que vemos dentro de un programa orientado a objetos cuando la lógica condicional  decide qué clase instanciar, y luego las evaluaciones futuras de ese condicional son hechas a través de polimorfismo en lugar de a través de código condicional duplicado.

Si esta separación es útil dentro de un único código base, esto es especialmente vital cuando estás usando elementos externos tales como componentes y servicios. La primera cuestión es si deseas aplazar la elección de la clase de implementación para un despliegue particular. Si es así, necesitas usar alguna implementación de un plugin. Una vez que estás usando puglins entonces es esencial que el ensamblado de los plugins esté hecho por separado del resto de la aplicación de forma que puedes sustituir fácilmente diferentes configuraciones por diferentes despliegues. Como logras esto es secundario. Este mecanismo de configuración puede incluso configurar un Localizador de Servicios, o usar inyección para configurar objetos directamente.

 

Algunos temas adicionales

En este artículo, me he concentrado en los temas básicos de la configuración de un servicio usando Inyección de Dependencias y Localizador de Servicios. Hay algunos temas más que tienen un papel que también merecen atención, pero no he tenido tiempo todavía de profundizar sobre ello. En particular está la cuestión del comportamiento del ciclo de vida. Algunos componentes tienen distintos eventos del ciclo de vida: detenerse y iniciarse por ejemplo. Otro problema es el creciente interés en el uso de las ideas orientadas a aspectos con estos contenedores. A pesar de que no he considerado este material en este artículo por el momento, espero escribir más sobre esto ya sea extendiendo este artículo o bien escribiendo otro.

Puedes descubrir un montón más sobre estas ideas buscando en los sitios web dedicados a los contenedores ligeros. Navegar por los sitios web de Picocontainer y Spring te conducirá dentro de muchas más discusiones sobre estos temas y al comienzo de algunos temas adicionales.

 

Reflexiones finales

La actual avalancha de todos los contenedores ligeros tienen en común un patrón subyacente sobre como realizan el ensamblado de servicios - el patrón de Inyección de Dependencias. Inyección de Dependencias es una alternativa útil a el Localizador de Servicios. Cuando la construcción de clases de aplicación de ambos son más o menos equivalentes, creo que el Localizador de Servicios tiene una ligera ventaja debido a su comportamiento más sencillo y directo. Sin embargo, si estás construyendo clases para ser usadas en múltiples aplicaciones entonces Inyección de Dependencias es una mejor elección.

Si usas Inyección de Dependencias hay distintas opciones para elegir. Te sugeriría utilizar Constructor Inyection a menos que te encuentres con uno de los problemas específicos de este enfoque, en este caso cambia a Setter Inyection. Si estás eligiendo para construir o obtener un contenedor, busca uno que soporte ambos enfoques.

La elección entre el Localizador de Servicios y la Inyección de Dependencias es menos importante que el principio de separación de configuración del servicio utilizando los servicios dentro de una aplicación.

 

Agradecimientos

Mi más sincero agradecimiento a las muchas personas que me ayudaron con este artículo. Rod Johnson, Paul Hammant, Joe Walnes, Aslak Hellesøy, Jon Tirsén y Bill Caputo me ayudaron a comprender estos conceptos y comentar sobre los primeros borradores de este artículo. Berin Loritsch y Hamilton Verissimo de Oliveira me proporcionaron algunos consejos muy útiles sobre cómo llevarse bien con Avalon. Dave W Smith insistió en hacer preguntas sobre mi interfaz de configuración inicial de inyección de código, anteponiendo que era estúpido.

 

Revisiones significativas

23 de enero 2004: Se volvió hacer el código de configuración del ejemplo de inyección de interfaz.

16 de enero 2004: Se añadió un pequeño ejemplo tanto de un localizador como de inyección con Avalon.

14 de enero 2004: Primera Publicación.

Comments (4) -

jose farrel
jose farrel
8/8/2013 3:33:30 PM #

Hola,muy bueno el post.
Habria alguna posibilidad de descargarlo en formato PDF ??
Gracias

Oscar.SS
Oscar.SS
8/16/2013 10:31:07 AM #

Hola Jose,

La verdad, no tengo pensado editarlo en PDF. Pero siéntete libre de copiarlo y guardartelo como PDF Wink

Un saludo

René Enríquez
René Enríquez
10/3/2013 6:51:41 PM #

Gran traducción hay solo errores de forma como por ejemplo
"se apartó de de ti para moverse hacia el framework." repites "de de"
"Dicho NanoContainer interpretará el XML y para luego configurar un contendor PicoContainer" creo que el y deberias quitarlo

"condicional  decide qué clase instaciar" te falto la n en la palabra instaNciar

Una critica posible al articulo original podria ser respecto de este parrafo

"En general no me gusta este enfoque. A pesar de que es muy flexible, no es muy explícito. La única forma en la que puedo enterarme como alcanzar un servicio es a través de keys textuales. Prefiero métodos explícitos ya que es más fácil encontrar dónde están estos mirando las definiciones de la interfaz"

Una forma fácil de solventarlo pienso yo seria dejando como key el classname y buscar los servicios de la forma
ServiceLocator.buscarServicio(ClaseBuscada.class.getName())


Por lo demas es genial una clara explicación de Inyeccion de dependencias y los diferentes tipos, definitivamente es un gran aporte muchas gracias por traducirlo y publicarlo

Oscar.SS
Oscar.SS
10/4/2013 10:59:15 AM #

Hola René,

Muchas gracias por comentar los errores gramaticales. Ya están corregidos.

Un saludo

Recent Comments

Comment RSS

Month List