lunes, 5 de mayo de 2014

Patrones estructurales: Facade

Este post corresponde a la serie Patrones de diseño - Patrones estructurales

Facade

"Proporciona una interfaz unificada para las interfaces de un subsistema. Facade define una interfaz de alto nivel que hace que el subsistema sea más fácil de usar".

El patrón Facade es probablemente uno de los más usados. En mi opinión, que sea tan ampliamente usado, se debe al gran valor que aporta frente a un bajo coste de implementación.



En palabras llanas, el patrón Facade permite al cliente de un sistema complejo usarlo de manera más simple.

Veamos un ejemplo que nos ayudará a clarificar más el concepto del patrón Facade.

El problema

Aprovechando que en la reciente conferencia Build 2014 Microsoft hizo público el proyecto "Roslyn" (un proyecto open-source de compilador escrito en C# y VB incluido en la iniciativa .NET Foundation) usaremos como ejemplo de sistema complejo con el que interactuar un compilador. 

Un compilador es un sistema que tiene objetos como símbolos, analizadores léxicos, analizadores sintácticos, analizadores semánticos, generadores de código, optimizadores de código, etc...

Un sin fin de objetos que interactúan unos con otros para convertir nuestro código fuente en código binario. 

Para algunos programas clientes muy específicos todos estos objetos tienen sentido y son de mucha utilidad. Sin embargo, la mayoría de nosotros sólo queremos compilar nuestro código fuente nada más. Y si tenemos que tratar con toda esa complejidad para compilar nuestros programas mucho me temo que para muchos de nosotros será misión imposible crear una build.

Bien, en este punto tenemos un sistema de compilación muy complejo y la necesidad de poder usarlo de forma más simple, al menos para las operaciones más habituales. Aquí es donde el patrón Facade cobra todo su sentido. 
El patrón Facade nos ofrece una interfaz de alto nivel para facilitarnos el uso del sistema complejo. Por ejemplo una forma simple de compilar nuestro código a través de una línea de comando:


O de forma aún más simple a través de Visual Studio:


Bien a través la línea de comandos o de Visual Studio usamos Facade que nos simplifica el uso de sistema, en este caso un compilador.
Roslyn implementa el método estático Run en la clase Csc. El método Run recibe los argumentos de la línea de comandos y "pone en marcha la maquinaria" que compila el código fuente.

Veamos un gráfico muy simplificado que explica cómo funcionaría el patrón Facade en un compilador:


Los clientes que sólo necesitan un uso "simplificado" del compilador usan los métodos de la clase Compiler (Csc de Roslyn). Pero esto no quiere decir que no podamos usar el resto de clases del sistema. Un cliente  específico que lo necesite puede usar igualmente objetos de tipo CodeGenerator, Scanner, Parser, etc.

De forma genérica esta sería la representación del patrón Facade:


Estos son los participantes del patrón:
  • Facade (clase Csc). Conoce las clases y subsistemas responsables de responder a una petición de un cliente y delega esa petición a las clases o subsistemas responsables de resolverla.
  • Subsystem Classes (CodeGenerator, Scanner, Parser, etc). Implementan la funcionalidad del subsistema. No tienen ningún conocimiento sobre el Facade, es decir, no tienen ninguna referencia a él.

Ventajas del uso del patrón Facade

  • Abstrae al cliente de los componentes del subsistema reduciendo el número de objetos con los que el cliente tiene que tratar y haciendo que el subsistema sea más fácil de usar.
  • Promueve un bajo acoplamiento entre el cliente y el subsistema. Es habitual que los componentes de un subsistema estén fuertemente acoplados. Mantener un acoplamiento bajo entre el cliente y el subsistema permite hacer cambios en el subsistema sin afectar al cliente. Pensemos en un patrón Facade aplicado a la capa de acceso a datos de nuestra aplicación. Esto nos permitiría reducir al mínimo los efectos que pudiera tener un cambio en la capa de acceso a datos sobre la capa de negocio.
  • El patrón Facade no impide que se pueda acceder a los componentes del subsistema si es necesario. El cliente puede elegir entre el uso normal del subsistema o el uso fácil a través del Facade.

A tener en cuenta a la hora de implementar el patrón Facade

Aunque creo que el concepto del patrón Facade es bastante simple, hay que tener una serie de consideraciones a la hora de implementarlo. 

Reducir el acoplamiento Cliente-Subsistema. Podemos reducir aún más el acoplamiento entre el cliente y el subsistema si empleamos como Facade una clase abstracta con subclases concretas para distintas implementaciones del subsistema. 

Volviendo al ejemplo del Facade para la capa de acceso a datos. Podríamos tener la clase abstracta DataAccessFacade como Facade y una subclase SqlDataAccessFacade para una capa de acceso a datos que usa SqlServer, o XmlDataAccessFacade para una capa de acceso a datos que usa archivos Xml para guardar datos. En ambos casos el cliente usaría el subsistema de acceso a datos a través de la clase abstracta DataAccessFacade.

Clases del subsistema públicas o privadas. En cierto sentido un subsistema es análogo a una clase. Ambos exponen una interfaz pública y ambos encapsulan funcionalidad y estado.

La parte pública de un subsistema son las clases a las que todos los clientes tienen acceso, mientras que la parte privada es sólo de uso del propio subsistema. Las clases Facade son evidentemente públicas, pero no son las únicas. Clases como CodeGenerator, Scanner, Parser en nuestro ejemplo son también clases públicas ya que un cliente puede necesitar manejar esos objetos de bajo nivel el lugar del Facade.

A la hora de implementar un Facade en un subsistema debemos elegir detenidamente que clases formarán parte de la interfaz pública del subsistema además de las clases Facade. 

Implementación 

Ahora que hemos visto que es un patrón Facade vamos a hacer ver un ejemplo más práctico de este patrón en funcionamiento.

Supongamos que en nuestra empresa "ACME Domótica" hemos creado un sistema de software capaz de manejar diferentes aparatos de casa como un proyector, la pantalla del proyector, la luz, el reproductor de DVD, el sistema de sonido del salón, etc.
Además hemos desarrollado una aplicación móvil que permite al usuario reproducir una película desde su teléfono.

Este sería un esbozo del código de nuestro sistema de domótica:

public class LightController
{
    public void Dim(int percentage)
    {
        // Establece la intensidad de la luz 
        // a un porcentaje determinado.
    }
}

public class ScreenController
{
    public void Down()
    {
        // Baja la pantalla del proyector.
    }
}

public class ProjectorController
{
    public void On()
    {
        //Enciende el proyector
    }

    public void Input(string inputSource)
    {
        // Establece el origen de la señal
        // a reproducir
    }

    public void WideScreenMode()
    {
        // Establece el formato panorámico.
    }
}

public class AmplifierController
{
    public void SetVolume(int p)
    {
        // Establece el volumen
    }
    public void SetSurroundSound()
    {
        // Establece el sonido envolvente
    }
    public void Input(string inputSource)
    {
        // Establece el origen de la señal
    }
    public void On()
    {
        // Enciende el amplificador
    }
}

public class DvdController
{
    public void On()
    {
        // Enciende el Dvd
    }

    public void Play(object movie)
    {
        //Inicia la película
    }
}

Y este sería el código del cliente que usa el sistema de domótica desde el móvil.

class Program
{
    static void Main(string[] args)
    {
        object movie = null;
        // Aquí obtendriamos el stream de la película.

        PlayMovie(movie);
    }

    private static void PlayMovie(object movie)
    {
        var lights = new LightController();

        // Establece la intensidad de la luz al 10%
        lights.Dim(10);

        var screen = new ScreenController();
        // Baja la pantalla del proyector
        screen.Down();

        var proyector = new ProjectorController();
        // Enciende el proyector, pone el origen de la señal en DVD
        // y establece el modo panorámico para la película.
        proyector.On();
        proyector.Input("Dvd");
        proyector.WideScreenMode();

        var amp = new AmplifierController();
        // Enciende el sistema de sonido, pone el origen de la señal en DVD
        // enciende el sonido envolvente y pone el volumen al 5
        amp.On();
        amp.Input("Dvd");
        amp.SetSurroundSound();
        amp.SetVolume(5);

        var dvd = new DvdController();
        // Enciende el reproductor de DVD e inicia la película.
        dvd.On();
        dvd.Play(movie);
    }
}

Como podemos ver el cliente móvil necesita interactuar con cinco componentes distintos del sistema de domótica sólo para reproducir una película. Además hay un acoplamiento muy fuerte entre el cliente y el sistema de domótica.

Vamos a seguir avanzando para ver las consecuencias.
Ahora nuestra empresa "ACME Domótica" ha desarrollado un dispositivo capaz de subir y bajar las persianas. Como consecuencia nos piden que modifiquemos el sistema de domótica para que gestione también las persianas.

A primera vista lo que debemos hacer es crear la clase BlindController con un método Up y un método Down que nos permita subir y bajar las persianas. Algo así:

public class BlindController
{
    public void Up()
    {
        // Sube las persianas
    }
    public void Down()
    {
        // Baja las persianas.
    }
}

Esta clase BlindController nos soluciona el problema de manejar las ventanas.
Sin embargo tenemos un problema añadido. Debido a que nuestra aplicación cliente para móvil está muy acoplada al sistema de domótica, si queremos bajar las persianas al reproducir una película deberemos también modificar la aplicación móvil. Quedaría de la siguiente forma:

class Program
{
    static void Main(string[] args)
    {
        object movie = null;
        // Aquí obtendriamos el stream de la película.

        PlayMovie(movie);
    }

    private static void PlayMovie(object movie)
    {
        var lights = new LightController();

        // Establece la intensidad de la luz al 10%
        lights.Dim(10);

        var blinds = new BlindController();
        // Baja las persianas.
        blinds.Down();

        var screen = new ScreenController();
        // Baja la pantalla del proyector
        screen.Down();

        var projector = new ProjectorController();
        // Enciende el proyector, pone el origen de la señal en DVD
        // y establece el modo panorámico para la pelicula.
        projector.On();
        projector.Input("Dvd");
        projector.WideScreenMode();

        var amp = new AmplifierController();
        // Enciende el sistema de sonido, pone el origen de la señal en DVD
        // enciende el sonido envolvente y pone el volumen al 5
        amp.On();
        amp.Input("Dvd");
        amp.SetSurroundSound();
        amp.SetVolume(5);

        var dvd = new DvdController();
        // Enciende el reproductor de DVD e inicia la película.
        dvd.On();
        dvd.Play(movie);
    }
}

Para solamente reproducir una película necesitamos manejar demasiados componentes y el cliente está acoplado al sistema de domótica de tal forma que un cambio en el sistema nos ha obligado a cambiar también el cliente.

¿Cómo nos podría ayudar el patrón Facade en este caso?
Veamos cómo sería la implementación original (sin el controlador de persianas) usando un Facade para acceder al sistema de domótica.
Éste sería el sistema de domótica

public class LightController
{
    public void Dim(int percentace)
    {
        // Establece la intensidad de la luz 
        // a un porcentaje determinado.
    }
}

public class ScreenController
{
    public void Down()
    {
        // Baja la pantalla del proyector.
    }
}

public class ProjectorController
{
    public void On()
    {
        //Enciende el proyector
    }

    public void Input(string inputSource)
    {
        // Establece el origen de la señal
        // a reproducir
    }

    public void WideScreenMode()
    {
        // Establece el formato panorámico.
    }
}

public class AmplifierController
{
    public void SetVolume(int p)
    {
        // Establece el volumen
    }
    public void SetSurroundSound()
    {
        // Establece el sonido envolvente
    }
    public void Input(string inputSource)
    {
        // Establece el origen de la señal
    }
    public void On()
    {
        // Enciende el amplificador
    }
}

public class DvdController
{
    public void On()
    {
        // Enciende el Dvd
    }

    public void Play(object movie)
    {
        //Inicia la película

    }
}

public class DomoticFacade
{
    public void WatchMovie(object movie)
    {
        var lights = new LightController();

        // Establece la intensidad de la luz al 10%
        lights.Dim(10);

        var screen = new ScreenController();
        // Baja la pantalla del proyector
        screen.Down();

        var projector = new ProjectorController();
        // Enciende el proyector, pone el origen de la señal en DVD
        // y establece el modo panorámico para la película.
        projector.On();
        projector.Input("Dvd");
        projector.WideScreenMode();

        var amp = new AmplifierController();
        // Enciende el sistema de sonido, pone el origen de la señal en DVD
        // enciende el sonido envolvente y pone el volumen al 5
        amp.On();
        amp.Input("Dvd");
        amp.SetSurroundSound();
        amp.SetVolume(5);

        var dvd = new DvdController();
        // Enciende el reproductor de DVD e inicia la película.
        dvd.On();
        dvd.Play(movie);
    }
}

Y este el cliente:

class Program
{
    static void Main(string[] args)
    {
        object movie = null;
        // Aquí obtendriamos el stream de la película.

        var domoticFacade = new DomoticFacade();
        domoticFacade.WatchMovie(movie);
    }

}

¿Qué ha cambiado?
Ahora el sistema de domótica implementa una interfaz de alto nivel, la clase DomoticFacade, que aísla al cliente de los componentes concretos del sistema. Hay un acoplamiento bajo entre el sistema de domótica y el cliente. Además para la clase cliente es mucho más simple usar el sistema de domótica.

Ahora los cambios necesarios para manejar las persianas sólo son necesarios en el sistema de domótica y no en el cliente. Agregamos la clase BlindController y modificamos el método WatchMovie de Facade. Quedaría así:


public class BlindController
{
    public void Up()
    {
        // Sube las persianas
    }
    public void Down()
    {
        // Baja las persianas.
    }
}

public class DomoticFacade
{
    public void WatchMovie(object movie)
    {
        var lights = new LightController();

        // Establece la intensidad de la luz al 10%
        lights.Dim(10);

        var blinds = new BlindController();
        // Baja las persianas.
        blinds.Down();

        var screen = new ScreenController();
        // Baja la pantalla del proyector
        screen.Down();

        var projector = new ProjectorController();
        // Enciende el proyector, pone el origen de la señal en DVD
        // y establece el modo panorámico para la pelicula.
        projector.On();
        projector.Input("Dvd");
        projector.WideScreenMode();

        var amp = new AmplifierController();
        // Enciende el sistema de sonido, pone el origen de la señal en DVD
        // enciende el sonido envolvente y pone el volumen al 5
        amp.On();
        amp.Input("Dvd");
        amp.SetSurroundSound();
        amp.SetVolume(5);

        var dvd = new DvdController();
        // Enciende el reproductor de DVD e inicia la pelicula.
        dvd.On();
        dvd.Play(movie);
    }
}

No es necesario modificar nada en el cliente por que el Facade mantiene un acoplamiento bajo entre el sistema de domótica y el cliente móvil.

Hasta aquí el patrón Facade. Continuaremos con el patrón Flyweight en el próximo post.


5 comentarios:

  1. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  2. Excelente ejemplo, muy preciso y claro. Grs...

    ResponderEliminar
    Respuestas
    1. Gracias Rafael, te invito a que sigas leyendo post anteriores y aportando tus opiniones y sugerencias.

      Eliminar
  3. Hola, primero quiero agradecerte por los post, he aprendido un montón, he leído todos y algunos mas de tres veces porque no me queda claro a la primera, pero no porque no este explicado correctamente sino por la falta de conocimiento de parte mía.

    Y segundo ¿Por qué no volviste a escribir? pensé que ibas a escribir mas sobre patrones de comportamiento y esas cosas, te lo agradecería muchísimo, no te imaginas lo que he aprendido con tus post.

    Graicas.




    ResponderEliminar
    Respuestas
    1. Hola CriCarBa.
      Muchas gracias por leer mi blog.
      Tienes razón, he tenido que aparcar este proyecto hace bastante, pero comentarios como el tuyo y algunos otros (no digo que haya sido aclamación popular... pero si unos cuantos) han hecho que vuelva a animarme y ya estoy empezando a trabajar en algunos borradores. Espero publicar pronto.

      Gracias de nuevo.

      Eliminar