Contenedores de Inversión de Control y el patrón de Inyección de Dependencias

Puede encontrar la versión original de este artículo en Inglés en:

http://www.martinfowler.com/

Introducción

Uno de los entretenimientos del mundo Java empresarial es la enorme cantidad de actividad en la construcción de alternativas al canal principal de tecnologías J2EE, la mayoría como Open Source. Casi todo esto es una reacción a la pesada complejidad de la mayor parte del mundo Java, pero otra parte se están explorando alternativas y vienen con ideas creativas. Un problema común es tratar la forma de conectar diferentes elementos: como se puede hacer trabajar juntos a esta arquitectura de controlador web con este interface de base de datos cuando los han construidos diferentes equipos sin ningún conocimiento el uno del otro. Varios marcos de trabajo han intentado resolver este problema, y muchos han optado por proporcionar una capacidad general para ensamblar componentes desde capas diferentes. Normalmente se conoce a estos componentes como componentes de peso ligero, entre ellos se pueden encontrar PicoContainer, y Spring.

Bajo estos contenedores hay varios principios de diseño interesantes, cosas que van mas allá de estos contenedores específicos y dentro de la plataforma Java. Voy a empezar explorando algunos de estos principios. Los ejemplos que utilizo están en Java, pero al igual que la mayoría de mis escritos, los principios son igualmente aplicables a cualquier otro entorno OO (Orientado a Objetos), particularmente a .NET.

Componentes y Servicios

El tema de conectar elementos me lleva casi inmediatamente a los complicados problemas de terminología que rodean los términos servicio y componente. Podrá encontrar con facilidad largos y contradictorios artículos sobre la definición de esta cosas. Para mis propósitos aquí están mis usos de estos términos tan sobrecargados:

Utilizo componente para indicar un elemento software que está pensado para ser utilizado, sin cambios, por una aplicación que está fuera del control de los creadores del componente. Con 'sin cambios' quiero decir que la aplicación que lo utiliza no modifica el código fuente de los componentes, aunque podría alterar su comportamiento extendiéndolo de alguna de las formas permitidas por sus creadores.

Un servicio es similar a un componente en que lo utilizan aplicaciones externas. La diferencia principal es que espero que un componente se utilice de forma local (puenso en un fichero jar, una dll, o una importación de código fuente). Un servicio se utilizará de forma remota a través de algún interface remoto, bien síncrona o asíncronamente (por ejemplo, servicios web, sistemas de mensajería, RPC o sockets).

En este artículo utilizo principalmente servicios, pero para los componentes locales también se puede aplicar la misma lógica. De hecho frecuentemente necesitará algun tipo de marco de trabajo de componentes locales para acceder fácilmente a un servicio remoto. Pero escribir "componente o servicio" es cansado de leer y escribir, y los servicios están mucho más de moda en estos momentos.

Un Ejemplo Ingenuo

Para ayudar a concretar un poco más todo esto utilizaré un ejemplo ejecutable para hablar sobre todo esto. Como todos mis ejemplos es uno de esos ejemplos super-sencillos; suficientemente pequeño para ser ireal, pero espero que sea suficiente para que visualice lo que va ocurriendo sin caer en un pantanoso ejemplo real.

En este ejemplo he escrito un componente que proporciona una lista de películas dirigidas por un director en partícular. Esta función sensacionalmente útil está implementada por un único 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 ingenua, le pide a un objeto finder (que veremos en un momento) que devuelva todos los films que conozca. Luego recorre la lista para devolver aquellos dirigidos por un director en partícular. No voy a corregir esta particular pieza de ingenuidad, ya que es el andamiaje para el objetivo real de este artículo.

El objetivo real de este artículo es el objeto finder, o particularmente como conectamos el objeto lister con un objeto finder particular. La razón para hacer esto es que quiero que mi maravilloso método moviesDirectedBy sea completamente independiente de la forma en que son almacenadas las películas. Por eso, todo lo que el método hace es referirse a un buscador, y todo lo que el buscador hace es saber como responder al método findAll. Puedo hacer esto definiendo un interface para el finder:

public interface MovieFinder {
    List findAll();
}

Ahora todo esto está muy bien desacoplado, pero en algún momento tendré que venir con una clase concreta que realmente traiga las películas. En este caso he puesto su código en el constructor de mi clase MovieLister:

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

El nombre de la clase de implementación viene del hecho de que estoy obteniendo mi lista desde un fichero de texto delimitado por comas. Le perdonaré los detalles, después de todo, el objetivo es que haya alguna implementación.

Ahora si yo utilizara está clase sólo para mi mismo, todo esto está muy bien y es divertido. Pero ¿qué sucede cuando mis amigos se ven arrastrados por el deseo hacia esta poderosa funcionalidad y quieren un copia de mi programa? Si ellos también almacenan su lista de películas en un fichero de texto delimitado por comas llamado "movies1.txt" todo será perfecto. Si su fichero de películas tiene un nombre diferente, entonces sería fácil poner el nombre del fichero en un fichero de propiedades. Pero ¿qué pasa si ellos tienen una forma totalmente diferente de almacenar su listado de películas: una base de datos SQL, un fichero XML, un servicio web, o simplemente otro formato de fichero de texto? En este caso necesitaremos una clase diferente para capturar los datos. Ahora como he definido un interface MovieFinder, esto no alterará mi método moviesDirectedBy. Pero aún necesitamos tener alguna forma de obtener un ejemplar de la implementación correcta del método finder en algún lugar.

La figura anterior muestra las dependencias de esta situación. La clase MovieLister es dependiente tanto del interface MovieFinder como de la implementación. Hubierámos preferido que sólo fuera dependiente del interface, pero entonces ¿cómo crearíamos un ejemplar con el que trabajar?

En mi libro P of EAA, describo esta situación como un Plugin. La clase de implementación para el método finder no está enlazada al programa durante la compilación, porque no sé lo que van a utilizar mis amigos. En vez de eso yo quiero que mi listador funcione con cualquier implementación, y para que esa implementación sea conectada en algún momento posterior, fuera de mi alcance. El problema es cómo puedo hacer este enlace para que mi clase lister ignore la clase de implementación, pero aún así pueda hablar con un ejemplar para hacer su trabajo.

Expandiendo esto a un sistema real, podríamos tener docenas de estos servicios y componentes. En cada caso podemos abstraer el uso de estos componentes hablando con ellos a través de un interface (utilizando un adaptador si el componente no está diseñado pensando en el interface). Pero si deseamos desplegar este sistema en diferentes formas, necesitamos utilizar plugins para manejar la interacción con esos servicios para que podamos utilizar diferentes implementaciones en diferentes despliegues.

Entonces el problema principal es ¿cómo ensamblamos estos plugins en una aplicación? Este es uno de los principales problemas que encara está nueva línea de contenedores de peso ligero, y universalmente todo lo que ellos hacen es utilizar la Inversión de control.

Inversión de Control

Cuando estos contenedores hablan sobre que son tan útiles porque implementan "Inversión de Control" termino muy confundido. La Inversión de Control es una característica común de los marcos de trabajo, por eso decir que estos contenedores de peso ligero son especiales porque utilizan inversión de control es como decir que mi coche es especial porque tiene ruedas.

La cuestion es ¿qué aspecto del control están invirtiendo? La primera vez que entre en la inversión de control, fue en el control principal de un interface de usuario. Los primeros interfaces de usuario eran controlados por el programa de la aplicación. Tendrían una secuencia de comandos como "Introduzca un nombre", "Introduzca una direccion"; su programa dirigiría las preguntas y recogería una respuesta de cada una. Con los UIs gráficos (o incluso basados en pantallas) el marco de trabajo UI contenía este bucle principal y el programa proporcionaba manejadores de eventos para los distintos campos de la pantalla. El control principal del programa se había invertido, se había sacado del programa al marco de trabajo.

Para esta nueva raza de contenedores la inversión es la forma en la que buscan una implementación del plugin. En mi ejemplo anterior el lister buscaba la implementación del finder ejemplarizándolo directamente. Esto hace que el finder no pueda ser un plugin. La aproximación que esos contenedores utilizan es asegurarse que cualquier usuario de un plugin sigue alguna convención que permita a un módulo ensamblador separado inyectar la implementación dentro del lister.

Como resultado he pensado que necesitamos un nombre más específico para este patrón. Inversión de Control es un término demasiado genérico, y la gente lo encuentra confuso. Como resultado de muchas discusiones con varios abogados de IoC hemos seleccionado el nombre Dependency Injection (Inyección de Dependencia).

Voy a empezar hablando sobre las distintas forma de inyección de dependencia, pero apuntaré que ésta no es la única forma de eliminar la dependencia entre la clase de la aplicación y la implementación del plugin. El otro patrón que se puede utilizar para hacer esto es Service Locator, y lo discutiremos después de que haya terminado mi explicación de Inyección de Dependencia.

Formas de Inyección de Dependencia

La idea básica de Inyección de Dependencia es tener un objeto separado, un ensamblador, que rellene un campo en la clase oyente con una implementación apropiada del interface finder, resultando en un diagrama de dependencia entre las líneas de la siguiente figura:

Hay tres estilos principales de Inyección de Dependencia. Los nombres que estoy utilizando para ellos son Inyección de Constructor, Inyección de Setter, e Inyección de Interface. Si ha leído algo sobre este tema en las discusiones actuales sobre la Inversión de Control, habrá oído referirse a estos como IoC tipo 1 (interface), IoC tipo 2 (setter) y IoC tipo 3 (constructor). Personalmente encuentro los nombres numéricos más díficiles de recordar, y es por eso que he utilizado los nombres que ha visto aquí.

Inyección de Constructor con PicoContainer

Empezaré viendo cómo se hace está inyección utilizando un contendor de peso ligero llamado PicoContainer. Principalmente empezaré aquí porque mis colegas de ThoughtWorks están muy activos en el desarrollo de PicoContainer (si, es una clase de nepotismo coorporativo).

PicoContainer utiliza un constructor para decidir cómo inyectar una implementación de finder en la clase MovieLister. Para que esto funcione, la clase lister necesita declarar un constructor que incluya todo lo que necesita inyectarse.

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

El propio finder también será manejado por el contenedor, y así tendremos el nombre del fichero de texto inyectado por el contenedor:

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

Entonces el contenedor necesita que se le diga qué clase de implementación asociar con cada interface, y qué string inyectar en el 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;
    }

Este código de ejemplo normalmente se configura en una clase diferente. Para nuestro ejemplo cada amigo que utilice mi lister podría escribir el código de configuración apropiado en alguna clase de configuración propia. Por supuesto que es común tener este tipo de información de configuración en ficheros de configuración separados. Puede escribir una clase para leer un fichero de configuración y configurar el contenedor de la forma apropiada. Aunque PicoContainer no contiene esta funcionalidad por sí mismo, hay un proyecto muy relacionado llamado NanoContainer que proporciona las envolturas apropiadas para permitir tener ficheros de configuración XML. Dicho nano-contenedor analizará el XML y luego configurará el pico-contenedor subyacente. La filosofía del proyecto es separar el formado del fichero de configuración del mecanismo subyacente.

Para utilizar el contenedor debe escribir algún código parecido a 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 inyección de constructor, PicoContainer también soporta inyección de setter, aunque sus desarrolladores prefieren utilizar el primero.

Inyección de Setter con Spring

El marco de trabajo Spring es un marco de trabajo de amplio espectro para desarrollo empresarial en Java. Incluye capas de abstacción para transaciones, marcos de trabajo de persistencia de transaciones, desarrollo de aplicaciones web y JDBC. Al igual que PicoContainer soporta inyección tanto de constructor como de setter, pero sus desarrolladores tienden a preferir setter - lo que lo hace una elección más apropiada para este ejemplo.

Para hacer que mi MovieLister acepte la inyección he definido un método set para este servicio:

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

De forma similar, he definido un método set para el string del finder:

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

El tercer paso es generar la configuración para los ficheros. Spring soporta configuración a través de ficheros XML y a través de código, pero XML es la forma esperada de 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>

El test se parecerá 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());
    }

Inyección de Interface

La tercera técnica de inyección es definir y utilizar interfaces. Avalon es un ejemplo de marco de trabajo que utiliza esta técnica. Hablaré un poco más sobre ello más adelante, pero en este caso voy a utilizarlo con un sencillo código de ejemplo.

Con esta técnica empiezo definiendo un interface que utilizaré para realizar la inyección. Aquí está el interface para inyectar un Moviefinder en un objeto:

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

Este interface tendría que ser definido por quien proporcione el interface MovieFinder. Necesita ser implementado por cualquier clase que quiera utilizar un finder, como mi MovieLister:

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

Utilizo una aproximación similar para inyectar el nombre del fichero en la implementación de finder:

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

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

Luego, como es usual, necesito algún código de configuración para conectar la implementación. Por propósitos de simplicidad haré esto dentro del código:

class Tester...
    private Container container;

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

Esta configuración tiene dos estados, el registro de componentes a través de la búsqueda de claves 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 interface de inyección necesita algún código para inyectar el código dependiente. Aquí hago esto registrando los objetos inyectores con el contenedor. Cada objeto inyector implementa el interface injector:

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

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

}

Cuando la dependencia es una clase escrita para este contenedor, tiene sentido que el componente implemente el propio interface injector, como hago aquí con la clase MovieFinder. Para clases genéricas, como string utilizo 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 utilizan 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 los interfaces de inyección para imaginarse las dependencias y los inyectores para inyectar las dependencias correctas. (La implementación específica del contenedor que he hecho aquí no es importante para la técnica, y no he querido mostrarla aquí para no embarullarlo todo).

Utilizar el patrón Service Locator

El beneficio principal de la Inyección de Dependencia es que elimina la dependencia que tiene la clase MovieLister de una implementación concreta de MovieFinder. Esto me permite darle a mis amigos la clase y que ellos puedan conectar una implementación adecuada para sus propios entornos. La inyección no es la única forma de romper esta dependencia, otra forma es utilizar un service locator.

La idea básica que hay detrás de un service locator es tener un objeto que conozca cómo obtener todos los servicios que una aplicación podría necesitar. Es decir un service locator para está aplicación tendría un método que devuelva un MovieFinder cuando se necesite. Por supuesto esto sólo desplaza el problema, aún tenemos que poner el localizador en el lister, resultando en las dependencias de la siguiente figura:

En este caso utilizaré el ServiceLocator como un único Registry. El lister entonces puede usarlo para obtener el finder cuando se ejemplariza:

class MovieLister...
    MovieFinder finder = ServiceLocator.movieFinder();

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

Al igual que en la aproximación de inyección, tenemos que configurar el sevice locator. Aquí lo he hecho dentro del código, pero no es dificil utilizar un mecanismo que pudiera leer los datos apropiados desde un fichero 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 prueba:

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());
    }

Frecuentemente he oído la queja de que este tipo de localizadores de servicios son una mala idea porque no se puede probar ya que no se puede sustituir implementaciones para ellos. Ciertamente puede diseñarlos malamente para caer en este tipo de problemas, pero no tiene porque hacerlo. En este caso el ejemplar del service locator es sólo un contendor de datos. Puedo crear fácilmente el localizador con implementación de prueba de mis servicios.

Para un localizador más sofisticado puedo extender service locator y pasar esta subclase a la variable de la clase registry. Puedo cambiar los métodos estáticos para llamar a un método de ejemplar en vez de acceder directamente a las variables de ejemplar. Puedo proporcionar localizadores específicos de threads utilizando un almacenamiento específico de threads. Todo esto se puede hacer sin modificar los clientes del service locator.

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

Utilizar un Interface Separado para el Locator

Uno de los problemas del sencillo ejemplo anterior, es que MovieLister depende de toda la clase del service locator, incluso si sólo utiliza un servicio. Podemos reducir esto utilizando un interface separado. De esta forma, en lugar de utilizar todo el interface del service locator, el lister puede declarar sólo la parte del interface que necesita.

En esta situación el proveedor del lister también podría proporcionar un interface locator que necesita para contener el finder:

public interface MovieFinderLocator {
    public MovieFinder movieFinder();

Entonces el locator necesita implementar este interface 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;

Observará que como queremos utilizar un interface, ya no podremos acceder nunca más a los servicios utilizando métodos estáticos. Tenemos que utilizar la clase para obtener un ejemplar del locator y luego utilizar éste para obtener lo que necesitamos.

Un Service Locator Dinámico

El ejemplo de arriba era estático, la clase del service locator tiene métodos para cada servicio que se necesite. Esta no es la única forma de hacerlo, también se puede hacer un service locator dinámico que permita adjuntar cualquier servicio que se necesite y hacer nuestras elecciones en tiempo de ejecución.

En este caso, el service locator utiliza un map en lugar de campos para cada servicio, y proporciona métodos genéricos para obtener y cargar 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 implica la carga de servicios con una clave apropiada:

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

Utilizo el servicio utilizando el mismo string clave:

class MovieLister...
    MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");

En su totalidad no me gusta esta aproximación. Aunque es ciertamente flexible, no es muy explícita. La única forma que he podido encontrar de como localizar un servicio es a través de claves de texto. Prefiero los métodos explícitos porque es más fácil encontrar donde están buscando en las definiciones de interfaces.

Utilizar juntos un locator y la inyección con Avalon

La inyección de dependencias y un service locator no son neceariamente conceptos mutuamente excluyentes. Un buen ejemplo es el uso de los dos en el marco de trabajo Avalon. Avalon utiliza un service locator, pero también utiliza inyección para decirle a los componentes donde encontrar un locator.

Berin Loritsch me envió esta simple versión de mi ejemplo utilizando 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 ejemplar de inyección de interface, que permite al contenedor inyectar un manejador de sercicios en MyMovieLister. El manejador de sercicios es un ejemplo de un service locator. En este ejemplo el lister no almacena el manejador en un campo, en vez de eso lo utiliza inmediatamente para buscar el finder, que si almacena.

Decidir que opción utilizar

Hasta aquí me he concentrado en explicar como veo estos patrónes y sus variaciones. Ahora puedo empazar a hablar de sus pros y contras para ayudar a imaginar cuál utilizar y cuándo.

Service Locator vs Inyección de Dependencia

La elección fundamental está entre Service Locator e Inyección de Dependencia. El primer punto es que ambas implementaciones proporcionar el desacoplamiento fundamental que no estaba en el ejemplo ingénuo del principio - en ambos casos el código de la aplicación es independiente de la implementación concreta del interface del servicio. La diferencia más importante entre los dos patrones es la forma de proporcionar la implementación a la clase de aplicación. Con service locator la clase de aplicación envía un mensaje explícito al locator. Con inyección no hay petición explícita, el servicio aparece en la clase de aplicación - debido a la inversión de control.

La inversión de control es una característica común de los marcos de trabajo, pero es algo que tiene un precio. Tiende a ser díficil de entender y presenta problemas cuando se está intentando depurar. Por eso prefiero evitarla a menos que la necesite de verdad. Con esto no quiero decir que sea una cosa mala, sólo que pienso que necesita justificarse ante una alternativa más correcta.

La diferencia principal es que con un service locator cada usuario de un servicio tiene una dependencia del locator. El locator puede ocultar dependencias de otras implementaciones, pero usted no necesita ver el locator. Por eso la elección entre locator e inyector depende de si la dependencia es un problema.

Utilizar la inyección de dependencia puede ayudar a hacer más fácil de ver donde están las dependencias de componentes. Con el inyector de depencia sólo tiene que mirar al mecanismo de inyección, que es el contructor, y ver las dependencias. Con el service locator tiene que buscar en el código fuente las llamadas al locator. Los IDEs modernos con una característica de búsqueda de referencias hace esto más fácil, pero aún así no es tán fácil como buscar el contructor o los métodos set.

Mucho de esto depende de la naturaleza del usuario del servicio. Si está construyendo una aplicación con varias clases que utilicen un servicio, entonces una dependencia de las clases de aplicación hacia el locator no es una gran idea. En mi ejemplo de dar un MovieLister a mis amigos, utilizar un service locator funciona bastante bien. Todo lo que necesitan hacer es configurar el locator para dirigirse a las implementaciones del servicio correcto, bien a través de algún código de configuración o un fichero de configuración. En este tipo de escenario no ve en la inversión del inyector algo competente.

La diferencia aparece si el lister es un componente que estoy proporcionando a una aplicación que está escribiendo otra gente. En este caso no se mucho sobre los APIs de los service locator que van a utilizar mis clientes. Cada cliente podría tener sus propios service locatos incompatibles. Puedo atajar esto utilizando el interface separado. Cada cliente puede escribir un adaptador que cumpla mi interface a su locator, pero en cualquier caso todavía necesito ver el primer locator para buscar mi interface específico. Y una vez que aparezca el adaptador la simplicidad de la conexión directa a un locator todo es empezar a resvalar.

Como con un inyector no se tiene una dependencia de un componente al inyector, el componente no puede obtener posteriores servicios desde el inyector una vez que está configurado.

Una razón común por la que la gente prefiere la inyección de dependencia es que hace más fácil el testeo. El punto aquí es que para hacer tests se necesita reemplazar facilmente el servicio real con esqueletos o maquetas. Sin embargo realmente no hay diferencia entre la inyección de dependencia y el service locator: ambos son muy manejables para crear esqueletos. Sospecho que esta observación viene de proyectos donde la gente no realiza suficiente esfuerzo para asegurar que su service locator pueda ser sustituido fácilmente. Aquí es donde ayuda el testeo contínuo, si no puede crear fácilmente servicios para testearlos, entonces esto implica un serio problema de diseño.

Por supuesto que el problema del testeo se ve aumentado por componentes del entorno que son muy intrusivos, como el marco de trabajo EJB de Java. Mi punto de vista es que este tipo de marcos de trabajo deberían minimizar su impacto sobre el código de la aplicación, y particularmente no deberían hacer cosas que relenticen el ciclo editar-ejecutar. Utilizar plugins para sustituir componentes de peso pesado ayuda mucho en este proceso, que es vital para las prácticas como el Desarrollo Dirigido al Test.

Por eso el problema principal es para la gente que está escribiendo código que espera que se utilice en aplicaciones fuera del control del desarrollador. En estos caso incluso la más mínima asumpción sobre un servive locator es un problema.

Inyección de Constructor vs Inyección de Setter

Para combinaciones de servicios, siempre se tiene que tener alguna convención para poder conectar las cosas. La ventaja de la inyección principalmente es que requiere convenciones muy simples - al menos para las inyecciones de constructor y de setter. No tiene que hacer nada raro en su componente y es bastante sencillo configurar completamente un inyector.

La inyección de interface es más invasiva ya que tenemos que escribir muchos interfaces para obtener todo tipo de cosas. Para un contenedor que requiera un pequeño conjunto de intefaces, como es el caso de Avalon, esto no está demasiado mal. Pero es mucho trabajo para ensamblar componentes y dependencias, que es por lo que la mayoría de los contenedores de peso ligero vienen con la inyección de setter y de constructor.

La elección entre inyección de setter o de constructor es interesante ya que los espejos son un problema general en programación orientada a objetos - debería rellenar campos en un construtor o con métodos set.

Mi larga experiencia con objetos me dice que mientras sea posible cree objetos válidos durante la construcción. Este consejo viene de Smalltalk Best Practice Patterns de Kent Beck: Método Constructor y Método Constructor Parametrizado. Los constructores con parámetros le dan una idea clara de lo que significa crear un objeto válido en un lugar óbvio. Hay más de una forma de hacer esto, crear varios constructores que muestren las diferentes combinaciones.

Otra ventaja de la inicialización con constructor es que permite ocultar claramente cualquier campo que sea inmutable simplemente no proporcionando un método set. Creo que esto es importante - si algo no debería cambiar la ausencia de un método set lo comunica muy bien. Si utiliza métodos set para inicialización, esto se puede convertir en un dolor. (De hecho, en estas situaciones yo prefiero evitar la convención usual de los métodos set, y prefiero un método como initFoo, para señalar claramente que esto es algo que se debería hacer durante el nacimiento).

Pero como en cualquier situación hay excepciones. Si tiene un constructor con muchos parámetros, las cosas se pueden embarullar, sobre todo en lenguajes sin parámetros de palabras claves. Es cierto que un gran constructor es signo de un objeto sobre-ocupado que debería dividirse, pero hay casos en que es lo que se necesita.

Si tiene varias formas de construir un objeto válido, puede ser duro mostrar esto mediante constructores, ya que los constructores sólo pueden variar en el número y tipo de sus parámetros. Aquí es donde entran en juego los métodos Factory. éstos pueden usar una combinación de constructores privados y métodos set para realizar su trabajo. El problema con los clásicos métodos Factory para ensamblaje de componentes es que normalmente se ven como métodos estáticos, y no puede tenerlos en interfaces. Usted puede crear una clase factory, pero simplemente se convierte en un ejemplar de servicio. Un servicio de factoría frecuentemente es una buen táctica pero áun así tendrá que ejemplarizar la factoría utilizando una de las técnicas descritas en este artículo.

Los constructores también sufren si tiene parámetros simples como strings. Con la inyección de setter puede darle a cada método set un nombre para indicar que se supone que hace el string. Con los constructores sólo trata con la posición, que es más dificil de seguir.

Si tiene varios constructores y herencia, entonces las cosas se vuelven particularmente dificiles. Para poder inicializarlo todo tiene que proporcionar constructores para reenviar a cada constructor de superclase, mientras tambien añade sus propios argumentos. Esto puede llevar a un enorme explosión de constructores.

A pesar de las desventajas yo prefiero empezar con la inyección de constructor, pero estoy listo para cambiar a la inyección de setter tan pronto como los problemas que he indicado arriba se conviertan en un verdadero problema.

Este tema ha llevado a muchos debates entre los distintos equipos que proporcionan inyectores de dependencia como parte de sus marcos de trabajo. Sin embargo, parece que la mayoría de la gente que construye estos marcos de trabajo se ha dado cuenta que es importante soportar ámbos mecanismos, incluso si hay alguna preferencia por alguno de ellos.

Configuración con Código o con Ficheros

Un problema separado pero frecuente es si utilizar ficheros de configuración o código sobre un API para conectar servicios. Para la mayoría de las aplicaciones que se van a desplegar en muchos lugares, un fichero de configuración separado tiene más sentido. Casi siempre éste será un fichero XML, y esto tiene sentido. Sin embargo hay casos donde es más fácil usar código de programa para hacer el ensamblaje. Una caso es donde usted tiene una sencilla aplicación que no necesitan una gran variación de despliegue. En este caso un poco de código puede ser más clarificador que un fichero XML separado.

Un caso contario es donde el ensamblaje es bastante complejo, inplicando pasos adicionales. Una vez que empieza a acercarse demasiado al lenguaje de programación el XML empieza a convertirse en una mejor aproximación a un lenguaje real que tiene toda la sintaxis para escribir un programa limpio.

Algunas veces pienso que la gente está sobre-entusiasmada definiendo ficheros de configuración. Muchas veces un lenguaje de programación crea un mecanismo de configuración sencillo y poderoso. Los lenguajes modernos pueden compilar fácilmente pequeños ensambladores que se pueden ensamblar para plugins de grandes sistemas. Si la compilación es un dolor, entonces están los lenguajes de script que también pueden funcionar bien.

Es frecuente decir que los ficheros de configuración no deberían utilizar un lenguaje de programación porque necesitan ser editados por no-progamadores. Pero ¿cuántas veces se da este caso? ¿La gente espera realmente que los no-programadores alteren los niveles de aislamiento de transaciones de una compleja aplicación del lado del servidor? Los ficheros de configuración que no utilizan un lenguaje funcionan bien sólo para extender que son simples. Si se vuelven complejos es hora de pensar en utilizar un lenguaje de programación adecuado.

Un cosa que estamos viendo en el mundo Java en este momento es una cacofonía de ficheros de configuración, donde cada componente tiene sus propios ficheros de configuración que son diferentes de los de cualquier otro. Si usted utiliza una docena de estos componentes, puede terminar fácilmente con una docena de ficheros de configuración que sincronizar.

Mi consejo aquí es siempre proporcionar una forma de facilitar la configuración con un interface programático, y luego tratar un fichero de configuración separado como un futuro opcional. Siempre se pude construir fácilmente un fichero de configuración que maneje el uso del interface programático. Si está escribiendo un componente deje a su usuario la opción de utilizar el interface programático, su formato de fichero de configuración, o que escriba él su propio formato de fichero de configuración y lo una al interface programático.

Separar la Configuración de la Utilización

El problema importante en todo esto es asegurarse de que la configuración de los sevicios está separada de su utilización. De hecho este es un principio de diseño fundamental que se une a la separación de los interfaces y su implementación. Es algo que hemos visto dentro de un programa orientado a objetos cuando la lógica condicional decide qué clase ejemplarizar, y luego las futuras evaluaciones de ese condicional se hacen utilizando polimorfismo en lugar de código condicional duplicado.

Si esta separación es útil dentro de un sencillo código base, es especialmente vital cuando está utilizando elementos externos como componentes y servicios. La primera cuestión es si usted desea retrasar la elección de la clase de implementación a los despliegues particulares. Si usted necesita utilizar alguna implementación del plugin. Una vez que esté utilizando plugins es esencial que su ensamblaje se haga de forma separada al resto de la aplicación para que pueda sustituir fácilmente las diferentes configuraciones para diferentes despliegues. La forma de conseguir esto es secundaria. Este mecanismo de configuración puede configurar un service locator, o usar inyección para configurar objetos directamente.

Algunos Problemas Más

En este artículo me he concentrado en los problemas básicos de la configuración de servicios utilizando Inyección de Dependencia y Service Locator. Hay algunos otros temas que entran aquí a los que les he prestado alguna atención, pero en los que no he tenido tiempo de entrar. En particular está el problema del comportamiento del ciclo de vida. Algunos componentes tienen distintos eventos de ciclo de vida: parada y arranque por ejemplo. Otro problema es el interés creciente en utilizar ideas orientadas la aspecto con estos contenedores. Aunque no he considerado este material en este artículo en este momento, espero poder escribir más sobre ello extendiendo este artículo o escribiendo otro.

Puede encontrar más información sobre estas ideas buscando en las sites dedicadas a los contenedores de peso ligero. Navegando por las webs de picocontainer y spring podrá encontrar toda esta información.

Pensamientos Finales

Toda la marañá actual de contenedores de peso ligero tiene en común el patrón subyacente de cómo ensamblan los servicios -- el patrón del inyector de dependencias. La Inyección de Dependencia es una alternativa útil a Service Locator. Cuando se construyen clases de aplicación los dos son equivalentes, pero creo que el Service Locator está más afinado debido a su sencillo comportamiento. Sin embargo, si está construyendo clases para utilizarlas en múltiples alicaciones la Inyección de Dependencia es la mejor opción.

Si utiliza Inyección de Dependencia hay varios estilos entre los que elegir. Yo le sugeriría que siga la inyección de constructor a menos que llegue a alguno de los problemas específicos que hemos visto, en cuyo caso debe cambiar a la inyección de setter. Si ha elegido construir u obtener un contenedor, busque uno que soporte tanto la inyección de constructor como la de setter.

La elección entre Service Locator e Inyección de Dependencia es menos importante que el principio de separar la configuración del servicio de su utilización dentro de una aplicación.

COMPARTE ESTE ARTÍCULO

ENVIAR A UN AMIGO
COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN GOOGLE +
¡SÉ EL PRIMERO EN COMENTAR!
Conéctate o Regístrate para dejar tu comentario.