Inversión de Control

De ChuWiki

Traducción del original de Martin Fowler Inversion of Control Containers and the Dependency Injection pattern. Terminado de traducir el 12 Dic 2007.


Contenedores de Inversion de Control y patrón de inyección de dependencia


En la comunidad Java ha habido un montón de contenedores ligeros que ayudan a ensamblar componentes de difrerentes proyectos en una aplicación coherente. Bajo estos contenedores hay un patrón común sobre cómo deben efectuar dicho "cableado", un concepto al que se refieren con el nombre genérico de "inversión de control". Es este artículo voy a profundizar en cómo trabaja este patrón, bajo el nombre más específico de "inyección de dependencia", y compararlo con la alternativa "Service Locator" (localizador de servicio). La elección entre ellos es menos importante que el principio de separar configuración de uso.


Chino | Portugués | Francés | Italiano


Una de las cosas más entretenidas en el mundo de "enterprise java" es la gran cantidad de actividad que hay en la construcción de alternativas a las tecnologías J2EE, muchas de las cuales son open source. Muchas de ellas son una respuesta a lo pesado y complejo que es el mundo de J2EE, pero otras muchas además exploran alternativas y traen ideas creativas. Un tema común con el que tratan es cómo unir diferentes elementos: cómo juntas este arquitectura controlador web con la interface de base de datos cuando ambas han sido construidas por diferentes equipos con poco conocimiento el uno del otro. Un número de frameworks han abordado este problema, y varios están empezando a trabajar para conseguir la capacidad general de ensamblar componentes de diferentes capas. A menudo se conoce a estos frameworks como contenedores ligeros, por ejemplo PicoContainer y Spring.

Bajo estos contenedores hay gran cantidad de principios de diseño interesantes, cosas que van más allá de estos contenedores específicos e incluso de la plataforma java. Quiero empezar a explorar aquí algunos de estos principios. Los ejemplos que uso están en java, pero como la mayoría de mis escritos, los principios se pueden aplicar igualmente a otros entornos OO, particularmente a .NET.


Componentes y Servicios[editar]

La idea de unir elementos juntos traen inmediatamente los complejos problemas de terminología alrededor de las palabras "servicio" y "componente". Puedes encontrar artículos largos y contradictorios con las definiciones de estas dos palabras con facilidad. Para esté artículo, aquí está la definción que emplearemos.

Uso componente para indicar un trozo de software pensado para ser usado, sin cambios, por una aplicación que no tiene nada que ver con los que han codificado el componente. Con "sin cambios" quiero decir que la aplicación no cambiará el código fuente del componente, aunque si puede alterar el comportamiento de dicho componente en las formas previstas por los que lo han codificado.

Un servicio es similar a un componente en el sentido de que lo usará una aplicación ajena. La diferencia principal es que el componente se supone que se usará de forma local (por ejemplo, un fichero jar, assembly, dll, import). Un servicio se usará de forma remota a través de una interface remota, bien de forma síncrona o asíncrona (por ejemplo, un web service, sistema de mensajes, RPC o socket).

Usaré principalmente servicio en este artículo, pero gran parte de la lógica explicada se puede aplicar igualmente a los componentes. Incluso a menudo necesitarás hacer algún tipo de componente local para acceder fácilmente al servicio remoto. Escribir "componente o servicio" es mucho más cansado de leer y escribir, y los servicios están más de moda hoy día.

Un ejemplo tonto[editar]

Para hacer todo esto más concreto, usaré un ejemplo concreto. Como todos mis ejemplos, es uno de esos ejemplos super-simples. Suficientemente pequeño como para no ser real, pero con la esperanza de que sirva para visualizar lo que se quiere explicar sin perderse en la complejidad de un ejemplo real.

Es este ejemplo voy a escribir un componetne que proporciona una lista de peliculas dirigidas por determinado director. Esta sensacional y útil función se implementa con un simple 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 tonta, pide a un objeto buscador (finder, sobre el que volveremos en un momento) que devuelva todas las peliculas que conozca. Entonces simplemente busca en esta lista para devolver aquellas dirigidas por el director concreto que queremos. No voy a arreglar esta tontería de código, porque es precisamente el esqueleto del punto de interés de este artículo.

El punto importante de este artículo es el objeto buscador (finder), o en concreto sobre cómo conectar el objetos que hace el listado (lister) con un objetos buscador (finder) en concreto. El motivo por el que es interesante es que quiero que mi maravilloso método moviesDirectoryBy sea completamente independiente de cómo se guardan las películas. De esta forma, todo lo que el método tiene que hacer es referirse a un buscador (finder) y todo lo que el buscador tiene que hacer es saber qué hacer cuando se llame al método findAll. Se puede hacer esto definiendo una interface para el buscador (finder).

public interface MovieFinder {
    List findAll();
}

Ahora todo está desacoplado, pero en algún momento hay que pasar una clase concreta que tenga las películas. En este caso, ponemos el código necesario para esto en el constructor de la clase lister.

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

El nombre de la clase concreta de lister viene del hecho de que obtengo la lista de un fichero de texto separado por comas. Te evitaré los detalles, después de todo, sólo importa que detrás hay algún tipo de implementación.

Si uso esta clase sólo para mí, todo va estupendo. Pero, ¿qué pasa si a mis amigos les abruma el deseo de usar mi programa y quieren una copia de él?. Si ellos guardan sus películas en un fichero de texto movies1.txt delimitado por comas como el mio, todo sigue siendo estupendo. Si el nombre del fichero es distinto, se puede arreglar fácilmente poniendo este nombre en un fichero de propiedades. Pero, ¿qué pasa si ellos almacenan las películas de una forma totalmente distinta: una base de datso SQL, un fichero XML, un web service o símplemente un fichero de texto con otro formato?. En ese caso necesitan una clase distinta para recoger estos dato. Como he definido una interface MovieFinder, no se modificará el método moviesDirectoryBy. Pero necesitamos una forma de obtener una instancia del fider concreto y ponerla en su sitio.


Figura 1: Las dependencias haciendo un new simple en la clase lister

La figura 1 muestra las dependencias en esta situación. La clase MovieLiser depende tanto de la interface MovieFinder como de su implementación. Nos gusaría que sólo dependiera de su interface, pero entonces, ¿cómo creamos la instancia concreta para trabajar con ella?

En mi libro P de EAA, describí esta situación como un Plugin. La implementación de finder no se enlaza dentro del programa en tiempo de compilación, ya que no sé qué es lo que querrán usar mis amigos. En su lugar, queremos que mi lister trabaje con cualquier implementación, y que dicha implementación se añada más adelante, sin tener yo control sobre ello. El problema es cómo hacer dicho enlace sin tener ni idea de cual es la implementación, pero siendo capaces de hablar con ella.

Ampliando esto en un sistema real, podríamos tener docenas de servicios y componentes similares. En cada caso deberíamos abstraernos de dichos componentes a través de interfaces (y usando un adaptador si el componente no fué diseñado con una interface en mente). Si queremos desplegar este sistema de diferentes maneras, necesitamos usar plugins para manejar la interacción con estos servicios, de forma que podamos usar diferentes implementaciones en diferentes despliegues.

Así que el problema es principal es ¿cómo ensamblamos estos plugins en la aplicación?. Este es uno de los problemas principales que se presenta en los contenedores ligeros, y universalmente todos ellos lo resuelven con Inversión de Control.

Inversion de Control[editar]

Cuando estos contenedores dicen lo útiles que son porque usan "Inversión de Control", termino muy contrariado. Inversión de Control es una característica común de estos frameworks, por lo que decir que un contenedor ligero es especial porque lo usa, es como decir que mi coche es especial porque tiene ruedas.

La cuestión es, ¿qué aspecto del control están invirtiendo?. Cuando por primera vez use inversión de control, fué en el control principal de una interface de usuario. Las interfaces de usuario anteriores se controlaban con la aplicación. Normalmente tenías una secuencia de comandos como "introduce tu nombre", "introduce tu dirección"; tu programa controlaba las peticiones y recogía las respuestas a cada una de ellas. Con interfaces de usuario gráficas (e incluso basado en terminales), el framewor de UI contiene un bucle principal y tu progrma simplemente proporciona controladores de eventos para los distintos campos de la pantalla. El control del programa se ha invertido, moviéndose de tú código al del framework.

Para este nuevo grupo de contenedores de inversión es para los que estamos buscando una implementación de plugins. En el ejemplo tonto, el lister consigue la implementación del finder instanciándolo directamente. Esto impide que el finder sea un plugin. La idea que manejan estos contenedores es asegurarse de que cualquier plugin sigue unas reglas, de forma que un modulo separado sea capaz de inyectar la implementación de finder dentro de lister.

Por tanto, creo que necesito un nombre más específico para este patrón. Inversión de control es un término demasiado genérico, y por ello la gente se confunde. Como resultado de un montón de discusiones decidimos llamarlo "Inyección de Dependencia".

Voy a empezar hablando de varias formas de inyección de dependencia, pero quiero puntualizar que esas no son las únicas formas de eliminar la dependencia de la clase con la implementación del plugin. El otro patrón que puedes usar para lo mismo es el localizador de servicio, y hablaré de él más adelante, cuando termine de explicar la inyección de dependencia.

Formas de Inyección de Dependencia[editar]

La idea básica de la inyección de dependencia es tener un objeto separado, un ensamblador, que rellena un campo en la clase lister con una implementación apropiada de la interface finder, de forma que resulta el diagrama de dependencias de la figura 2.

Figura 2: Dependencias de un inyector de dependencias.

Hay tres tipos principales de inyección de dependencias. Los nombre que uso son Inyección de constructor, Inyección de Setter e Inyección de Interface. Si lees sobre este tema en las actuales conversaciones sobre inversión de control, oirás estos nombres como tipo 1 IoC (Inyección de interface), tipo 2 IoC (Inyección de Setter) y tipo 3 IoC (Inyección de constructor). Creo que los números son más difíciles de recordar, por lo que usaré los nombres que acabo de poner.

Inyección de constructor, con PicoContainer[editar]

Comenzaré mostrando cómo se usa este tipo de inyección en un contenedor ligero de nombre PicoContaniner. Comienzo por aquí principalmente porque muchos de mis compañeros de ThoughtWorks están muy ocupados en el desarrollo de PicoContainer (sí, es un tipo de nepotismo corporativo).

PicoContainer usa un constructor para decidir cómo inyectar una implementación de finder en la clase lister. Para ello, la clase lister debe tener un constructor que incluya todo lo que deba ser inyectado.

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

El finder también será controlado por PicoContainer, así que contenedor inyectará el nombre del fichero de texto con las películas.

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

PicoContainer necesita que le indiquemos qué implementaciones se asocia con cada interface, y qué cadena debe 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 configuración habitualmente se escribe en una clase separada. En nuestro ejemplo, cada amigo que quiera usar mi lister debería esciribir su propio código de configuración en alguna clase suya. Por supuesto, suele ser habitual tener este tipo de información en ficheros de configuración separados. Puedes escribir una clase que lea el fichero de configuración y configure el contenedor adecuadamente. A pesar de que PicoContainer no proporiciona esta funcionalidad, hay un proyecto hermano llamado NanoContainer que proporciona los wrappers adecuados para usar ficheros de configuración XML. NanoContainer leer el fichero XML y configura un PicoContainer interno. La filosofía del proyecto es mantener separado el formato del fichero de configuración del mecanismo interno.

Para usar el contenedor, debes escribir algo como esto

    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 hemos usado inyector de constructor, PicoContainer admite inyección de setter. De todas formas, sus desarrolladores prefieren el mecanismo del constructor.

Inyección de Setter con Spring[editar]

Spring Framework es un framework muy completo para el desarrollo de Java Enterprise. Incluye capas de abstracción para transacciones, persistencia, frameworks, desarrollo de aplicaciones web y JDBC. Como PicoContainer, suporta tanto inyección de constructores como de Setters, pero sus desarrolladores prefieren usar los setter - por lo que se convierte una elección apropiada para este ejemplo.

Para que la clase lister acepte la inyección, definimos un método setter para ese servicio.

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

De igual forma, definimos un setter para el nombre de fichero.

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

El tercer paso es escribir la configuración para estos ficheros. Spring soporta tanto configuración en ficheros XML como a través de código, pero se supone que XML es la forma 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>

Y el test se puede 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[editar]

La tercera técnica de inyección es definir y usar interfaces. Avalon es un ejemplo de framework que usa esta técnica. Hablaré de ello más adelante, pero es este caso voy a usarlo con un código simple.

Con esta técnica, empiezo definiendo una interface con la que se hará la inyección. La siguiente es una interface para inyectar un finder en un objeto.

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

Esta interface debería definirla el que proporciona la interface MovieFinder. Debe ser implementada por cualquier clase que quiera usar un finder, como por ejemplo nuestro lister.

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

Haré algo parecido 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;
    }

Ahora, como es usual, necesito algún tipo de código de configuración para juntar todas estas 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, registrar componentes usando claves es similar a lo que se hace en los otros ejemplos.

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

El paso nuevo es registrar los inyectores que inyectarán los componentes dependientes. Cada interface de inyección necesita algo de código para inyectar el objeto dependiente. Haré esto registrando los inyectores en el contenedor. Cada inyector implementa la interface de inyector correspondiente.

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 clase dependiente es una clase escrita para este contendedor, tiene sentido que implemente la interface inyector ella misma, como hice con el finder de películas. Para clases generales, como String, he usado una clase interna el el 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");      
    }
    }

Los tests que usan este 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 usa las interfaces de inyección que se han declarado para ver las dependencias y los inyectores para inyectar las dependencias correctas. (La implementación concreta del contenedor no es tan importante, y no quiero enseñarla para que no te rías).

Uso de un localizador de servicios[editar]

El beneficio principal del inyector de dependencias es que elimina la dependencia que tiene la clase MovieLister de una implementación concreta de MovieFinder. Esto me permite pasar Lister a mis amigos y que ellos puedan incluir una implementación adecuada a su entorno. La inyección no es la única forma de romper esta dependencia, otra manera es usar un localizador de servicios.

La idea básica de un localizador de servicios es tener un objeto que sabe cómo conseguir cualquier servicio que la aplicación necesite. Un localizador de servicios para esta aplicación podría tener un método que devuelva un finder de películas cuando se necesite. Por supuesto, esto no hace más que cambiar de sitio el problema. Todavía necesitaremos obtener un localizador de servicios en la clase lister, dando lugar al esquema de dependencias de la figura 3.

Figura 3: Dependencias con un localizador de servicios.

Es este caso usaré el ServiceLocator como un Registro Singleton. Lister lo usará para obtener el finder cuando se instancie.

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

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

Como en el ejemplo de inyección, tenemos que configurar el localizador de servicios. Aquí lo voy a hacer en código, pero no sería demasiado costoso hacer un mecanismo que recoja los datos necesarios de 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;
    }

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

A menudo he oido que la queja de que este tipo de localizadores de servicio no son buenos puesto que no son testeables, ya que no se puede sustituir su implementación. Ciertamente puedes diseñarlos lo suficientemente mal como para tener este tipo de problemas, pero no tienes por qué. En este caso el localizador de servicios no es más que un almacén de datos. Se puede crear fácilmente un localizador con implementaciones de test de mis servicios.

Se puede hacer un localizador más sofisticado heredando del localizador de servicios y pasando dicha clase hija al registro. Se puede cambiar los métodos estáticos para que llamen a un método de la instancia en vez de acceder directamente a las variables. Se pueden proporcionar diferentes grupos de localizadores sin necesidad de cambiar el código de los clientes del localizador.

Una forma de ver esto es pensar que el localizador de servicios es un registro y no un singleton. Un singleton es una forma simple de implementar un registro, pero se puede cambiar fácilmente.


Uso de una interface separada para el localizador[editar]

Uno de los problemas del ejemplo anterior es que MovieLister depende totalmente de la clase localizador de servicios, aunque sólo use uno de sus servicios. Podemos reducir este efecto usando una interface separada. De esta forma, en vez de usar una interface gigante para el localizador de servicios, el lister pude declarar sólo lo poco que necesita.

En esta situación, el que proporiciona la clase lister debería proporcionar también la interface del localizador que necesita para obtener el finder.

public interface MovieFinderLocator {
    public MovieFinder movieFinder();

El localizador tiene que implementar esta 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;

Podrás observar que como queremos usar una interface, ya no podemos acceder a los servicios a través de métodos estáticos. Tenemos que usar la clase para obtener una instancia del localizador y luego usarlo de la forma que necesitemos.

Un localizador de servicios dinámicos[editar]

El ejemplo anterior era estático, en el sentido de que el localizador de servicios tiene un método para cada servicio que necesitemos. No es la única forma de hacerlo. Se puede hacer un localizador de servicios dinámico que te permita guardar cualquier servicio que necesites y decidir qué usamos en tiempo de ejecución.

En este caso, el localizador de servicios usará un Map en lugar de campos para cada servicio, y proporcionará un método general para obtener y cargar dichos 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 guardar los servicios con una clave apropiada.

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

Uso el servicio usando el mismo String como clave.

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

En conjunto no me gusta este enfoque. A pesar de que es realmente flexible, no es demasiado claro. La única forma en que se puede saber cómo conseguir un servicio es por medio de claves de texto. Prefiero usar métodos explícitos puesto que es más fácil saber qué están buscando simplemente mirando la definición de la interface.


Uso conjunto de localizador e inyección con Avalon[editar]

La inyección de dependencia y el localizador de servicios no son necesariamente conceptos incompatibles. Un buen ejemplo de cómo usarlos juntos es el framework Avalon. Avalon usa un localizador de servicios, pero usa la inyección para indicar a los componentes dónde está el localizador.

Berin Loritsch me envió esta versión simple 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 servide es un ejemplo de inyección de interface, que permite al contenedor inyectar un ServiceManager en MyMovieLister. El service manager es un ejemplo de localizador de servicios. En este ejemplo, lister no guarda el manager en un campo, en su lugar de forma inmediata lo usa par obtener el finder, que es el que realmente se guarda.

Decidir qué opción usar[editar]

Hasta ahora me he preocupado de explicar como veo estos patrones y sus variaciones. Ahora voya a empezar a hablar de sus pros y contras para intentar ayudar a decidir cual usar y cuando usarlo.


Localizador de servicios vs Inyección de dependencias[editar]

La elección fundamental está entre localizador de servicios e inyección de dependencias. Lo principal es que ambas implementaciones proporcionan el desacoplamiento que habíamos perdido en el ejemplo tonto - en ambos casos, el código de la aplicación es independiente de la implementación concreta de una interface. La diferencia principal entre los dos patrones es cómo se pasa la dicha implementación a la clase de nuestra aplicación. Con un localizador de servicios la clase lo pide explicitamente. Con el mecanimos de inyeccion no hay una petición explicita, el servicio se le pasa a la clase de la aplicación - de aquí lo de la inversión de control.

La inversión de control es una característica común de todos los frameworks, pero tiene un precio. Suele ser complicado entender y corregir los problemas cuando intentas depurar. Por lo que en general, prefiero evitarlos salvo que los necesite. No estoy diciendo que sean malos, sino que es necesario justificar su uso frente a otras alternativas más directas.

La diferencia clave es que con un localizador de servicios cada cliente del servicio tiene una dependencia con el localizador. El localizador puede evitar dependencias con las otras implementaciones, pero es necesario ver al localizador. La decisión entre usar un localizador o usar inyección de dependencia depende de si esta dependencia con el localizador es un problema.

Con la inyección de dependencias puede ser más fácil entender que dependencias tiene un componente. Con inyección de depedencias nos bastaría mirar el mecanismo de inyección, como el contructor, para ver qué dependencias tiene la clase. Con un localizador de servicios debemos mirar el código fuente de la clase para ver qué peticiones se hacen al localizador. Los IDEs modernos hacen esta búsqueda fácil, pero sigue siendo más fácil mirar los constructores o métodos set.

En gran parte depende de la naturaleza del usuario del servidio. Si estás construyendo una aplicación con varias clases que usan el servicio, el que estas clases dependan del lozalizador no es una gran idea. En mi ejemplo de dar el lister a mis amigos con un localizador de servicio funciona bien. Todo lo que necesitan es configurar el localizador para que devuelva la implementación correcta, bien a través de algo de código o bien con un fichero de configuración. En este escenario no veo que el inyector proporcione nada interesante.

La diferencia viene si el lister es un componente que proporciono a una aplicación que está escribiendo otra gente. En este caso, no sé nada de la API de los localizadores de servicios que van a usar. Cada uno puede querer usar sus propios localizadores de servicios, incompatibles unos con otros. Se puede evitar en parte el problema usando interfaces separadas. Cada cliente pude escribir su propio adaptador que encaje mi interface en su localizador, pero de cualquier forma, necesito ver el primer localizador para definir mi interface. Y una vez que aparece un adaptador, la simiplicidad de una conexión directa al localizador empieza a fallar.

Como con un inyector no hay depdendencia del componente con el inyector, el componente no puede obtener más servcios del inyector salvo para los que ha sido configurado.

La razón por la que la gente suele preferir dependencia de inyección es que puede hacer mejor los test. La idea aquí es que para hacer test, se pueda reemplazar fácilmente la implementación real con "stubs" o "mocks". Sin embargo, en realidad no hay diferencia entre la inyección de dependencia o el localizador: ambos son fácilmente reemplazables. Empiezo a pensar que esta afirmación viene de proyectos donde la gente no hace un esfuerzo para asegurar que sus localizadores de servicios se pueden reemplazar fácilmente. Aquí es donde los tests continuados pueden ayudar, si no puedes reemplazar fácilmente servicios para los tests, es que tienes un problema de diseño serio.

Por supuesto, el problema de los test se compilica en entornos muy intrusivos, como el framework de EJBs de Java. Mi punto de vista es que ese tipo de frameworks deberían minimizar su impacto en el código de una aplicación, y particularmente, no deberían hacer cosas que entorpezcan el ciclo editar-ejecutar. El uso de plugins para sustituir los componentes más pesados ayuda mucho a este proceso, y es vital en prácticas como el desarrollo orientado a test -test driven development-.

Esta es la cuestión principal para la gente que desarrolla código que se usará en aplicaciones sobre las que no se tiene control. En estos casos, la más mínima suposición sobre un localizador de servicios es un problema.


Inyeccion en constructor vs setter[editar]

Para la combinación de servicios, siempre debemos tener alguna norma para unir las cosas. La ventaja principal de la inyección es que requiere normas muy simples -al menos para las de constructor o setter-. No hay que hacer nada especial en el componente y es muy fácil para un inyector configurarlo todo.

La inyección de interface es algo más intrusiva, ya que hay que escribir bastantes interfaces para tenerlo todo dispuesto. Hacer un pequeño conjunto de interfaces que requiera al contenedor, como en el caso de Avalon, no es del todo malo. Lo malo es el trabajo que hay que hacer para ensamblar los componentes con sus dependencias. Este es el motivo por el que los contenedores ligeros actuales suelen usar inyección de setters o de constructor.

La elección entre setter y constructor es interesante y no es más que otra versión del problema general en la programación orientada a objetos - deberíamos rellenar los campos en el constructor o con setters.

Por defecto y hasta donde es posible, tiendo a crear objetos válidos en el momento de construirlos. Este consejo viene de las mejores prácticas en Smalltalk de Kent Beck's. Los constructores con parámetros dan una idea clara de qué se necesita para crear un objeto válido. Si hay más de una forma de hacerlo, debemos crear varios constructores con las posibles combinaciones.

Otra ventaja del constructor es que permite ocultar los campos que no pueden cambiar, simplemnte no proporcionando un método setter. Creo que esto es importante - si algo no debe cambiar, la ausencia de un setter lo deja bien claro. Si se usan setter para la inicialización, se puede convertir en un problema. En esta situación, incluso prefiero evitar la norma de los setters, prefiero llamar al método algo así como initFoo, para reforzar la idea de que es algo que sólo se debería hacer inmediatamente después de crear el objeto.

Pero en todas las ocasiones hay excepciones. Si tienes un montón de parámetros en el constructor, las cosas pueden parecer feas, particularmente en lenguajes sin parématros clave. Es cierto que normalmente un constructor con muchos parámetros es un objetos sobrecargado que debería partirse en otros más simples, pero a veces eso es lo que necesitamos.

Si hay varias formas válidas de construir un objeto, puede ser pesado reflejarlo a base de constructores, ya que los constructores sólo pueden variar en el número y tipo de parámetros. Aquí es cuando entran en juego los métodos Factory, que pueden usar una combinación de constructores privados y métodos setter para hacer su trabajo. El problema con estos métodos Factory para el ensamblado de componentes es que normalmente son estáticos y no se les puede ver a través de interfaces. Se puede hacer una clase factory, pero no es más que trasladar el problema. La factoría es una buena idea, pero hay que instanciarla usando alguna de las técnicas aquí descritas.

Los constructores además se complican si tienes parámetros simples como por ejemplo strings. Con los setter el nombre del método indica qué se supone que es el parámetro. Con los constructores, esto debe hacerse por la posición del parámetro, con lo que se hace más difícil.

Si tienes muchos constructores y herencia, las cosas se pueden poner especialmente duras. Para inicializar todo hay que proporcionar construcotores que redirijan a los contructores de la clase padre, además de añadir los parámetros propios de la clase hija. Esto puede llevar a una gran explosión de constructores.

A pesar de las pegas, prefiero comenzar con la inyección de constructores, pero siempre preparado para cambiar a la inyección de setter si empiezan a plantearse los problemas mencionados antes.

Este tema ha llevado a muchos debates entre los distintos equipos que proporcionan inyectores de dependencias como parte de sus frameworks. Sin embargo, parece que los que construyen estos frameworks se han dado cuenta de que es importante soportar ambos mecanismos, incluso se se prefiere uno u otro.

Código o ficheros de configuración[editar]

Otro tema separado es si usar ficheros de configuración o código para unir los servicios. Para aplicaciones que se van a desplegar en muchos sitios, un fichero de configuración separado suele tener más sentido. En la mayoría de las veces será un fichero XML. Sin embargo, hay casos en los que es más sencillo usar código para el ensamblado. Es el caso de aplicaciones que no se van a desplegar en muchos sitios variados. En este caso, un poco de código puede ser más claro que un fichero XML separado.

El caso opuesto es cuando el ensamblado es muy complejo, con pasos condicionales. En el momento en que empieces a acercarte más a un lenguaje de programación que a XML, es mejor usar un verdadero lenguaje de programación que tiene toda la sintaxis necearia para hacer un programa claro. En este caso se escribe una clase constructor que hace todo el ensamblado. Si tienes varios escenarios, puedes hacer varias clases constructoras y usar un fichero de configuración simple para elegir una u otra.

A menudo pienso que la gente son demasiado entusiastas de definir ficheros de configuración. Muchas veces un lenguaje de programación hace el trabajo de forma más directa y proporciona un mecanismo de configuración más potente. Los lenguajes modernos puede compilar fácilmente pequeños ensambladores que se pueden usar para ensamblar plugins en grandes sistemas. Si la compilación es un problema, hay lenguajes de script que también pueden hacerlo.

Se suele decir que los ficheros de configuración no deberían usar lenguajes de programación poque tienen que se editados por no programadores. Pero, ¿cuántas veces es así?. ¿Realmente esperamos que no programadores toquen los ficheros de configuración de una aplicación compleja?. Los ficheros de configuración simples funcionan bien si la configuración es simple. Si se vuelve compleja, es mejor pensar en usar un verdadero lenguaje de programación.

Una de las cosas que veo en la actualidad en el mundo Java es una cacofonía de ficheros de configuración, donde cada componente tiene su propio fichero de configuración, que es completamente diferente de todos los demás. Si usas una docena de estos componentes, necesitarás una docena de ficheros de configuración que debes mantener actualizados.

Mi consejo es que siempre proporciones una interface de ćodigo para configurar las cosas y que uses un fichero de configuración como una característica opcional. Se puede manejar leer fácilemente el fichero de configuración y luego usar la interface de configuración. Si estás haciendo componentes dejas así abierta la posibilidad al que la va a usar de decidir si configurar desde código, desde tu fichero de configuración o incluso la posibilidad de crear su propio fichero de configuración, hacerse su lector del fichero y usar la interface de código para realizar la configuración indicada en su propio fichero.


Separar la configuración del uso[editar]

Lo más importante es asegurarse de que la configuración de los servicios está separada de su uso. Este es un principio fundamental de diseño que se asienta en separar las interfaces de la implementación. Es algo que vemos en un programa orientado a objetos donde una condición decide qué clase instanciar y las futuras evaluaciones de esa condición se hacen a través del polimorfismo en vez de duplicar el código condicional.

Si esta separación es útil en un código simple, es vital cuando usamos componentes o servicios de otros. Lo primero que hay que decidir es si dejamos la decisión de qué implementación usar para cada despliegue en particular. Si decidimos que sí, tenemos que implementar alguna forma de admitir plugin. Una vez que decidimos usar plugins, es esencial que el ensamblado de estos plugins se haga en un sitio totalmente separado del resto de la aplicación, de forma que sea fácil cambiar la configuración para distintos despliegues. El cómo conseguirlo es secundario. Se puede hacer con un localizador de servicios o usando inyección.


Otros temas[editar]

En este artículo me he concentrado en las ideas básicas de configuración de servicios usando inyección de dependencias y localizadores de servicios. Hay otras cosas que también requieren atención, pero no he encontrado tiempo para profundizar en ellas. En concreto hay un asunto sobre el ciclo de vida. Algunos componentes tienen ciclos de vida distintos: stop y start, por ejemplo. Otra tema es el interés creciente en las ideas de la orientación a aspectos con estos contenedores. Aunque no he considerado meter todo este material en este artículo de momento, espero abordar estos temas ampliando este artículo o bien en uno nuevo.

Puedes encontrar más sobre estas ideas buscando en los sitios web dedicados a contenedores ligeros. Navegando en los sitios de PicoContainer y de Spring llegarás a más información sobre estos y otros temas relacionados.

Conclusiones[editar]

Los contenedores ligeros están todos basados en el modo de cómo ensamblar los servicios - el patrón de inyección de dependencia. La inyección de dependencia es una alternativa útil al localizador de servicios. Cuando construimos clases de nuestra aplicación, ambas aproximaciones son bastante similares, pero creo que el localizador de servicios tiene una ligera ventaja al tener un comportamiento más directo. De todas formas, si estás haciendo clases que sirvan para distintas aplicaciones, la inyección de dependencia es mejor elección.

Si te decides por la inyección de dependencia, tienes varios estilos para elegir. Te sugiero que empieces con la inyección de constructor hasta que empieces a encontrar los problemas típicos de esta aproximación. En ese caso, usa la inyección de setter. Si piensas elegir o hacerte un contenedor, busca uno que soporte tanto inyección de constructor como de setter.

La elección entre localizador de servicio o inyección de dependencia es menos importante que el hecho de separar la configuración de los servicios de su uso en una aplicación.