IoC vs ServiceLocator

Es un tema que tenía bastante claro y nunca me había parado a pensar mucho sobre ello, pero a raíz de una interesante discusión en la lista de Alt.Net Hispano, he estado dándole una vuelta y he llegado a una conclusión: por mucho que la gente se empeñe en asimilarlos, IoC y ServiceLocator son conceptos opuestos.

IoC: Inversion of Control

La base de Inversion of Control (IoC) es cambiar la forma en que se interactúa con un framework. De hecho, según algunos como Martin Fowler es un factor clave a la hora de diferenciar un framework de una librería. La idea es que las clases diseñadas por el usuario del framework (es decir, por nosotros) no realizan llamadas directas al framework, sino que es el framework quien las llama a ellas. Esto se conoce también como Principio de Hollywood: no nos llames, nosotros te llamaremos.

Una cosa que se deduce de lo anterior es que, en principio IoC no tiene nada que ver con Dependency Injection (DI). IoC y DI son conceptos ortogonales: se puede tener DI sin IoC y se puede usar IoC para cosas que no son DI.

Un ejemplo claro en .NET es la programación con Windows Forms. En Windows Forms se definen controles a base de heredar de clases que ya existen y redefinir métodos concretos (OnPaint, OnMouseDown, etc.). No es frecuente acceder directamente a la cola de mensajes de windows para responder a un WM_PAINT, el framework se encarga de eso e invoca directamente al método apropiado de nuestra clase cuando es necesario.

También podemos tener (aunque no sea muy recomendable) DI para pobres, en la que «más o menos» inyectamos las dependencias «a mano» a partir de los constructores por defecto.

Pese a ser conceptos ortogonales, existe un nexo de unión: los contenedores de inversión de control, siendo este nexo el que muchas veces crea la confusión. Un contenedor de inversión de control (nombre que, por otra parte, suena genial, como a transfuncionador del continuo) permite, mediante el uso de inversión de control, aplicar inyección de depencias de manera genérica a un conjunto de componentes o servicios.

Los contenedores de inversión de control se basan en registrar componentes (clases o interfaces con su implementación), cuyas dependencias están declaradas explícitamente en el constructor. Generalmente estas dependencias están especificadas en forma de interfaces para poder obtener lo que se conoce como inversión de dependencia, pero esa es otra historia. El caso es que el contenedor es capaz de crear instancias de los componentes satisfaciendo sus dependencias a partir del resto de componentes que están registrados en el contenedor.

De esta forma es consigue la inversión de control, puesto que ya no será necesario hacer new Component(new Dependency1(), new Dependency2()), sino que será el contenedor el que se encargue de llamar a los constructores adecuados (Principio de Hollywood, ¿recuerdas?).

Service Locator

Service Locator es un patrón que nos permite (¡sorpresa!) localizar servicios. ¿Qué quiere decir esto? Pues que tenemos una clase, el ServiceLocator a la cual le podemos pedir instancias de un servicio concreto.

Esto es útil para poder tener distintas implementaciones de un mismo servicio y cambiar, mediante configuración, la implementación que queremos que devuelva el ServiceLocator cuando le pidamos la instancia del servicio. ServiceLocator actúa como un catálogo central de instancias de servicios al que le podemos solicitar la instancia del servicio que necesitemos.

Como vemos, usando Service Locator no hay inversión de control por ninguna parte. Simplemente hay un lugar al que podemos acudir a buscar servicios, pero acudimos nosotros (el código que usa ServiceLocator), no es él quien nos llama.

¿De dónde viene la confusión?

La confusión entre los dos conceptos nace de los contenedores de inversión de control. El «problema» es que son una herramienta perfecta para implementar los dos patrones, tanto la inyección de depencias por inversión de control, como el Service Locator.

Si usamos un contenedor de inversión de control para realizar inyección de dependencias, tendremos algo así:

// Ejemplo de uso de un contenedor de inversión
// de control para implementar inyección de 
// dependencias

public class Sample
{
    public interface IDependency {}
    public class Dependency : IDependency {}

    public interface IComponent {}
    public class Component : IComponent
    {
        private readonly IDependency dependency;

        public Component(IDependency dependency)
        {
            this.dependency = dependency;
        }
    }

    public static void Main()
    {
        var container = new WindsorContainer();
	    container.Register(Component.For<IDependency>().ImplementedBy<Dependency>(),
                           Component.For<IComponent>().ImplementedBy<Component>());
					   
        var component = container.Resolve<IComponent>();
    }
}

En este caso, las dependencias de Component se declaran en su contructor y el contenedor se encarga de satisfacerlas, invocando los constructores adecuados. En ningún momento nuestros componentes utilizan el contenedor, de hecho Component ni siquiera sabe que existe ni necesita para nada un contenedor.

La otra opción, errónea en mi opinión, es utilizar el contenedor como ServiceLocator:

// Ejemplo de uso de un contenedor de inversión
// de control para implementar un ServiceLocator
// OJO: No hagáis esto en casa. Hay soluciones 
//      mejores

public class Sample
{
    public interface IDependency {}
    public class Dependency : IDependency {}

    public interface IComponent {}
    public class Component : IComponent
    {
        private readonly IDependency dependency;

        public Component()
        {
            this.dependency = ServiceLocator.Get<IDependency>();
        }
    }

    public static class ServiceLocator
    {
	    private static IWindsorContainer container;
		
        static ServiceLocator()
        {
            container = new WindsorContainer();
			container.Register(Component.For<IDependency>().ImplementedBy<Dependency>(),
                               Component.For<IComponent>().ImplementedBy<Component>());
        }
	
        public static T Get<T>()
        {
            container.Resolve<T>();
        }		
    }
	
    public static void Main()
    {
        var component = new Component();
    }
}

En esta implementación Component usa directamente ServiceLocator para obtener sus dependencias, por lo que no hay inversión de control por ninguna parte.

Por eso decía al principio del artículo que IoC y Service Locator son cosas opuestas. Cuando hablamos de emplear IoC para inyección de dependencias y lo comparamos con ServiceLocator, estamos hablando de dos aproximaciones completamente antagónicas:

ServiceLocator hace justo lo contrario de la inversión de control, en lugar de permitir a las clases ser llamadas por el framework con las dependencias que necesitan, las clases tienen que llamar al framework para obtener sus dependencias.