Patrones Estructurales (VII): Patrón Proxy


Objetivo:

«Proporcionar un sustituto o intermediario para otro objeto de modo que pueda controlarse el acceso que se tiene hacia él».

Design Patterns: Elements of Reusable Object-Oriented Software

Supongo que todos conocemos el concepto de proxy, al menos en su acepción aplicada a la navegación web. Se trata de una máquina que actúa de intermediaria a la hora de servir páginas web (u otros servicios). En la configuración de área local podemos indicar la IP de esta máquina y será esta máquina la que se conecte a la URL por nosotros y la envíe a nuestro equipo.

De este modo, un equipo no se conectará directamente a la URL, sino que lo hará a través de este intermediario. ¿Por qué hacer esto? Por múltiples motivos: podemos, por ejemplo, restringir las URLs que nuestros clientes (ordenadores de la red local) pueden visitar. O cachear las páginas que se visitan con más frecuencia, haciendo innecesario el acceso a la web «real» en los casos en los que el acceso se repita, proporcionando un ahorro en ancho de banda. En resumen, un proxy será una entidad en la que delegaremos la ejecución de ciertas tareas y que decidirá, en última instancia, qué acciones realizar antes y después de éstas. Los proxies, por tanto, podrían dedicarse perfectamente a la política 🙂

Tipos de proxy

Dependiendo de las responsabilidades y del comportamiento del proxy, tendremos varios tipos que realizarán unos tipos de tarea u otras. Los proxies más comunes son los siguientes:

  • Proxy remoto: un proxy remoto se comporta como un representante local de un objeto remoto. Se encarga principalmente de abstraer la comunicación entre nuestro cliente y el objeto remoto. Es el embajador de los proxies.
  • Proxy virtual: se encarga de instanciar objetos cuyo coste computacional es elevado. Es capaz de sustituir al objeto real durante el tiempo que el verdadero objeto está siendo construido y proporcionar funcionalidades como el lazy loading (realizar operaciones computacionalmente costosas únicamente cuando el acceso a el elemento es requerido).
  • Proxy de protección: establece controles de acceso a un objeto dependiendo de permisos o reglas de autorización.

Estructura del patrón

Si echamos un vistazo al diagrama, vemos que la estructura de este patrón es bastante sencilla:

  • Tenemos una clase abstracta (o interfaz) Elemento que define las operaciones que deberá cumplimentar tanto nuestro objeto real (ElementoReal) como el proxy que actuará de intermediario (ElementoProxy). Ambos elementos, al heredar de Elemento, deberán ser, por tanto, intercambiables. De este modo, sustituir un objeto de la clase ElementoReal por un ElementoProxy debería de ser -idealmente- transparente.
  • La clase ElementoReal es aquella que contiene la verdadera funcionalidad, es decir, la clase que se quiere «proteger» a través del proxy. En el ejemplo de los navegadores, se correspondería al ordenador que realiza la petición HTTP. Este elemento tendrá un conjunto de operaciones, heredadadas desde Elemento. Usando el símil de los ordenadores, una operación podría ser HttpGet o HttpPost.
  • La clase ElementoProxy también hereda de Elemento, y como tal, posee todos sus métodos. La diferencia fundamental es que también incorpora una referencia a otro Elemento. Así, tal y como ocurría en otros patrones como Adapter o Decorator, el método codificado dentro de esta clase realizará ciertas operaciones de control y/o transformación para posteriormente invocar el método original del ElementoReal que tiene referenciado. Por ejemplo, si nuestro proxy no quisiera permitir las conexiones a una página en concreto, su comportamiento sería algo similar al siguiente:

    private class ElementoProxy : Elemento
    {
        // Incluímos una referencia a otro elemento.
        private Elemento elemento;

        // Inyectamos el elemento a través del constructor.
        public ElementoProxy(Elemento elemento)
        {
            this.elemento = elemento;
        }

        // El método HttpGet realizará comprobaciones y/o adaptaciones para
        // posteriormente realizar la llamada al método homónimo del objeto real
        public string HttpGet(string uri)
        {
            if (uri.ToLower().Contains("paginaprohibida.com"))
                return null;
            else
                return HttpGet(uri);
        }
    };

 

A estas alturas, si habéis echado un ojo a los patrones anteriores, este concepto os resultará pan comido. El patrón Proxy realiza una tarea similar a la que realizaba el patrón Adapter, salvo que en lugar de transformar la interfaz del objeto (ambos objetos heredan de la misma clase y, por tanto, comparten interfaz), éste utilizará la misma, pero realizando una serie de operaciones adicionales antes (o después) de realizar la llamada al método de la clase original.

Es más, el concepto de añadir funcionalidad a una clase que puede hacerse pasar por otra añadiendo nueva funcionalidad inyectando el objeto original a través del constructor es un concepto que también hemos visto antes en el patrón Decorator. El patrón Proxy es muy similar a éste último, con la diferencia (nuevamente) de su objetivo: mientras que el patrón Decorator añade nuevas responsabilidades a un objeto de forma dinámica, el patrón Proxy únicamente realiza operaciones de control de acceso sobre ese objeto. De hecho, el Proxy tiene una capacidad que no tiene el Decorator: tiene la capacidad de instanciar objetos de la clase que encapsula. En el ejemplo aquí mostrado el objeto es inyectado, pero en determinados casos, este podría ser perfectamente instanciado. Esto suele ser común en el llamado Virtual Proxy, en el que el Proxy puede proporcionar un comportamiento «por defecto» mientras realiza la operación que requiera cierto coste computacional.

Por lo tanto, nos encontramos una colección de cosas que ya nos son familiares: una clase abstracta de la que heredan ambos elementos, inyectar un objeto en otro a través de su constructor, realizar una composición de objetos al añadir una referencia del objeto original dentro del objeto de la clase proxy… Como vemos, los mismos conceptos se repiten una y otra vez. Una vez comprendidos, la comprensión de los patrones se simplifica muchísimo.

Otra posible versión de este patrón resultaría en realizar la herencia de forma lineal, es decir, haciendo que el proxy herede del elemento real y sobrecargando los métodos que requieran supervisión.

Con este modelo evitaremos que nuestro proxy tenga que incluir una referencia al objeto real, ya que podemos realizar la misma operación sobrecargando el método y llamando al método de la clase base en lugar de invocar el método del objeto inyectado a través del constructor. Usando esta estructura podríamos incluso evitar que ElementoReal herede de una clase abstracta o implemente una interfaz, simplificando el código… pero forzando a nuestro diseño que dependa de una concreción en lugar de depender de una abstracción. En la medida de la posible, intentaremos respetar este principio.

Arrancando el vehículo

Atrás quedaron los días en que el sistema de arranque de nuestro vehículo consistía en una llave dentada que encajaba con los pernos de la cerradura. La electrónica y la informática hace tiempo que ha llegado a la automoción, y por ello la nueva serie de vehículos Dorfwagen incorpora una centralita con un complejo sistema de seguridad que es capaz de comprobar si el código de la llave coincide con el de la centralita. En los modelos de gama alta, además de realizar esta operación estándar, la centralita proporciona un sistema de seguridad capaz de detectar si la llave ha sido falsificada.

Nuestra centralita, por tanto, proporciona dos atributos: uno que se corresponderá con el código de la llave y otro que identificará al código de seguridad.



    public class CentralitaVehiculo
    {
        private int codigoLlave;
        private int codigoSeguridad;

        public CentralitaVehiculo(int codigoLlave, int codigoSeguridad)
        {
            this.codigoLlave = codigoLlave;
            this.codigoSeguridad = codigoSeguridad;
        }

        public int CodigoLlave {
            get { return codigoLlave; }
        }
        public int CodigoSeguridad {
            get { return codigoSeguridad; }
        }
    }

Dado que disponemos de dos gamas (alta y baja), necesitaremos implementar, partiendo de la misma centralita, un sistema que sea compatible con ambos pero que sea capaz de realizar una comprobación adicional si la llave del vehículo es de gama alta. Comenzaremos codificando la clase abstracta Llave de la cual heredará tanto la funcionalidad base de nuestra llave LlaveReal como la clase que hará de veces de proxy, LlaveProxy.



    // Clase abstracta de la que heredará el elemento original y el proxy
    public abstract class Llave
    {
        // Código de la llave
        protected int codigoLlave;

        // Propiedad de sólo lectura para obtener el código de la llave
        public int CodigoLlave
        {
            get { return codigoLlave; }
        }

        // Métodos abstractos que implementarán el elemento real y el proxy
        public abstract void RealizarContacto(CentralitaVehiculo centralita);
        public abstract bool LlaveCorrecta(int codigoLlave);
    }

Esta llave contiene un atributo que almacenará el código de la llave, más dos métodos que deberán ser implementados por las clases hija: RealizarContacto, que arrancará el vehículo, y LlaveCorrecta, que será invocada por el método anterior y comprobará si el código de la centralita y de la llave coinciden.

Nuestra clase LlaveReal, por tanto, mostrará el comportamiento básico que se espera de ella: comprobará que llave y centralita comparten código y realizará el arranque del vehículo en caso de que así sea.



    public class LlaveReal : Llave
    {
        // Constructor base: asigna el código de la llave a la llave
        public LlaveReal(int codigoLlave)
        {
            this.codigoLlave = codigoLlave;
        }

        // Realizar contacto: comprueba que el código de la llave sea correcto.
        // En caso de que lo sea, arranca el vehículo.
        public override void RealizarContacto(CentralitaVehiculo centralita)
        {
            if (LlaveCorrecta(centralita.CodigoLlave))
                Console.WriteLine("Contacto realizado");
            else
                Console.WriteLine("Código de llave inválido");
        }

        // Comprueba que el código proporcionado coincide con el de la llave
        public override bool LlaveCorrecta(int codigoLlave)
        {
            return codigoLlave == this.codigoLlave;
        }
    }

Hasta aquí, nada que se separe de un diseño convencional. Es hora de implementar nuestro proxy, que también hereda de Llave. La diferencia fundamental con la clase anterior radica en que:

  • Su constructor acepta un objeto de la clase Llave, que será inyectado para ser invocado en los métodos LlaveCorrecta y RealizarContacto.
  • Debido a lo anterior, nuestra clase deberá incluir una referencia a Llave.
  • El método LlaveCorrecta se limitará a invocar el método LlaveCorrecta de la llave que se ha inyectado a través del constructor.
  • Finalmente, el método RealizarContacto será el encargado de realizar la comprobación adicional: antes de comparar los códigos de la llave y de la centralita invocando a LlaveCorrecta realizará una comprobación previa, utilizando algún tipo de algoritmo para asegurarse de que el código de seguridad de la centralita permite el código de la llave insertada. En caso de que sea correcto, el control de acceso habrá sido superado y se ejecutará el código correspondiente a LlaveReal. En caso contrario, se denegará el arranque:

    public class LlaveProxy : Llave
    {
        // Referencia a la llave original
        private Llave llaveOriginal;

        // Constructor en el que se inyectará el objeto real
        public LlaveProxy(Llave llave)
        {
            llaveOriginal = llave;
        }

        // Este método realizará el control de acceso sobre el método original.
        // Realizará una comprobación previa comparando el código de seguridad y, si este es
        // correcto, invocará el método del objeto real.
        public override void RealizarContacto(CentralitaVehiculo centralita)
        {
            // Realizamos una comprobación adicional de seguridad. En caso de no cumplirse, se
            // aborta la operación. Esta operación podría ser la ejecución de un algoritmo para
            // comprobar la autenticidad del código de la llave, una comprobación de nombre de
            // usuario y contraseña... o cualquier otra comprobación que queramos realizar.
            if (centralita.CodigoSeguridad > llaveOriginal.CodigoLlave.ToString().GetHashCode())
            {
                Console.WriteLine("Código de seguridad incorrecto. Abortanto arranque");
                return;
            }

            if (LlaveCorrecta(centralita.CodigoLlave))
                Console.WriteLine("Contacto realizado");
            else
                Console.WriteLine("Código de llave inválido");
        }

        // Este método no realizará comprobaciones adicionales. Se limitará a invocar el método
        // del objeto real.
        public override bool LlaveCorrecta(int codigoLlave)
        {
            return llaveOriginal.LlaveCorrecta(codigoLlave);
        }
    }

La llave simple realizará un arranque normal y correcto siempre que el código de centralita y de la llave coincidan. No obstante, en los vehículos de gama alta, si utilizamos un código de seguridad erróneo, el motor no arrancará:


            int codigoLlave = 532543463;
            int codigoSeguridad = 1038948470;

            CentralitaVehiculo centralita = new CentralitaVehiculo(codigoLlave, codigoSeguridad);

            Llave llaveSimple = new LlaveReal(codigoLlave);
            llaveSimple.RealizarContacto(centralita);

            Llave proxy = new LlaveProxy(llaveSimple);
            proxy.RealizarContacto(centralita);

Por el contrario, si el código es válido, se procederá al arranque de forma normal.


            int codigoLlave = 532543463;
            int codigoSeguridad = -1098948470;

            CentralitaVehiculo centralita = new CentralitaVehiculo(codigoLlave, codigoSeguridad);

            Llave llaveSimple = new LlaveReal(codigoLlave);
            llaveSimple.RealizarContacto(centralita);

            Llave proxy = new LlaveProxy(llaveSimple);
            proxy.RealizarContacto(centralita);


Como podemos observar, ambos objetos pueden ser intercambiados, ya que al utilizarse una clase común (Llave), lo único que deberá hacer el cliente es invocar el método necesario para realizar el arranque (RealizarContacto). El código implementado por cada clase queda fuera del alcance del cliente, que lo único que espera es que el vehículo arranque o muestre un mensaje de error.

 

¿Cuándo utilizar este patrón? Ejemplos reales

Este patrón es aconsejable en los siguientes supuestos:

  • Retrasar una invocación a una petición con alto coste computacional hasta que sea necesario (lazy loading). Un ejemplo de este comportamiento sería, por ejemplo, cumplimentar un diccionario interno que requiera realizar varias conexiones a base de datos únicamente cuando se solicite información de uno de los elementos contenidos en el diccionario.
  • Simplificar la interacción con elementos remotos: si el objeto es local, las invocaciones serán las habituales. Si el objeto es remoto, el proxy implementará los mecanismos de comunicación haciéndolos transparentes al cliente.

Ejemplos de este patrón pueden ser la tecnología RMI de Java, en el que la invocación de un objeto remoto se realiza a través de un Stub. Este Stub no es más que una clase que expone las mismas operaciones que el objeto remoto, pero que hace que la petición sea transparente para el cliente, realizando de forma interna las operaciones de comunicación.

Otro ejemplo serían las pantallas de carga de ciertas aplicaciones. En este caso, se tratará de un Proxy Virtual, que mostrará el típico mensaje de «Cargando» mientras recupera la información solicitada por el usuario, sobreescribiendo el mensaje una vez que la petición al objeto real ha concluido.

 

Fuentes:

2 comentarios

  1. Hola Daniel,

    Muchas gracias por la explicación de este post. La idea ya la conocía y me ha resultado sencillo de comprender.

    He visto que escribías varias veces el concepto «lazy loading» dentro del post. Podría este patrón aplicarse a un sistema de buzón de emails. Por ejemplo, cuando accedes a tu cuenta de correo únicamente descargas el asunto y el remitente. Y cuando pulsas sobre el correo que te interesa descargas el body, adjuntos, … que son elementos más pesados.

    No interesa descargar todo en la primera conexión porque igual luego no consultas muchos de ellos y estarías consumiendo ancho de banda y penalizando tiempo de espera.

    Saludos.

    1. Hola, José Miguel.

      Efectivamente, el que comentas es un buen ejemplo de aplicación de aplicación de este patrón, de hecho, mucho mejor que el que yo he propuesto en el artículo.

      Yo lo suelo aplicar a servicios web. Si no se hace uso de tecnologías como Entity o similares, soy partidario de mapear el objeto de forma habitual (a través de un POCO o un DTO de toda la vida), para posteriormente crear una clase proxy que herede de él y encapsule exclusivamente los getters de aquellos atributos que representen un enlace a otro elemento. En estos getters, antes de devolver su contenido compruebo si el atributo correspondiente de la clase base existe y, en caso contrario, lanzo la consulta, almacenando en el atributo los valores devueltos por la llamada (objeto o colección de objetos). De este modo se implementa el «lazy loading»: la petición al servicio web no se realiza hasta que se accede al valor de la propiedad. Si la propiedad no se consulta nunca, nos hemos ahorrado una petición. Y creando el proxy, hemos dejado impoluta la clase base, respetando además el principio de responsabilidad única (el POCO únicamente almacenará datos, mientras que el proxy será el encargado de realizar las llamadas «extra»).

      Como siempre, muchas gracias por leerme, y sobre todo, por tus comentarios.

      ¡Un saludo!

Deja un comentario