El patr贸n de dise帽o de proxy en Java

    Introducci贸n

    El patr贸n de dise帽o proxy es un patr贸n de dise帽o que pertenece al conjunto de patrones estructurales. Los patrones estructurales son una categor铆a de patrones de dise帽o que se utilizan para simplificar el dise帽o de un programa en su nivel estructural.

    Como sugiere su nombre, el patr贸n de proxy significa usar un proxy para alguna otra entidad. En otras palabras, un proxy se utiliza como intermediario frente a un objeto existente o envuelto alrededor de 茅l. Esto se puede utilizar, por ejemplo, cuando el objeto real consume muchos recursos o cuando existen determinadas condiciones que deben comprobarse antes de utilizar el objeto real. Un proxy tambi茅n puede ser 煤til si queremos limitar el acceso o la funcionalidad de un objeto.

    En este art铆culo, describiremos el patr贸n de proxy y mostraremos algunos ejemplos en los que se puede utilizar.

    La idea detr谩s de Proxy

    El proxy se utiliza para encapsular funcionalidades de otro objeto o sistema. Considerar invocaci贸n de m茅todo remoto, por ejemplo, que es una forma de llamar a m茅todos en otra m谩quina. En Java, esto se logra a trav茅s de un proxy remoto que es esencialmente un objeto que proporciona una representaci贸n local de otro objeto remoto. Entonces, es posible llamar a un m茅todo desde otra m谩quina simplemente llamando a un m茅todo del objeto proxy.

    Cada proxy se realiza de tal manera que ofrece exactamente la misma interfaz al cliente que un objeto real. Esto significa que el cliente efectivamente no nota ninguna diferencia al usar el objeto proxy.

    Hay varios tipos de objetos proxy. Como probablemente se pueda inferir del ejemplo anterior, los proxies remotos se utilizan para acceder a algunos objetos o recursos remotos. Adem谩s de los proxies remotos, tambi茅n hay proxies virtuales y proxies de protecci贸n. Describamos brevemente cada uno de ellos para una mejor comprensi贸n.

    Proxies remotos

    Los servidores proxy remotos proporcionan una representaci贸n local de otro objeto o recurso remoto. Los proxies remotos son responsables no solo de la representaci贸n sino tambi茅n de algunos trabajos de mantenimiento. Dicho trabajo podr铆a incluir conectarse a una m谩quina remota y mantener la conexi贸n, codificar y decodificar los caracteres obtenidos a trav茅s del tr谩fico de red, an谩lisis, etc.

    Proxies virtuales

    Los proxies virtuales envuelven objetos costosos y los cargan a pedido. A veces, no necesitamos inmediatamente todas las funcionalidades que ofrece un objeto, especialmente si consume memoria o consume mucho tiempo. Llamar a objetos solo cuando sea necesario puede aumentar un poco el rendimiento, como veremos en el ejemplo siguiente.

    Proxies de protecci贸n

    Los proxy de protecci贸n se utilizan para comprobar determinadas condiciones. Algunos objetos o recursos pueden necesitar la autorizaci贸n adecuada para acceder a ellos, por lo que el uso de un proxy es una de las formas en que se pueden verificar tales condiciones. Con los proxies de protecci贸n, tambi茅n tenemos la flexibilidad de tener muchas variaciones de control de acceso.

    Por ejemplo, si intentamos proporcionar acceso a un recurso de un sistema operativo, generalmente hay varias categor铆as de usuarios. Podr铆amos tener un usuario al que no se le permite ver o editar el recurso, un usuario que puede hacer con el recurso lo que desee, etc.

    Tener proxies que act煤en como envoltorios de dichos recursos es una excelente manera de implementar un control de acceso personalizado.

    Implementaci贸n

    Ejemplo de proxy virtual

    Un ejemplo de proxy virtual es la carga de im谩genes. Imaginemos que estamos creando un administrador de archivos. Como cualquier otro administrador de archivos, este deber铆a poder mostrar im谩genes en una carpeta que un usuario decida abrir.

    Si asumimos que existe una clase, ImageViewer, responsable de cargar y mostrar im谩genes; podr铆amos implementar nuestro administrador de archivos usando esta clase directamente. Este tipo de enfoque parece l贸gico y sencillo, pero contiene un problema sutil.

    Si implementamos el administrador de archivos como se describe arriba, cargaremos im谩genes cada vez que aparezcan en la carpeta. Si el usuario solo desea ver el nombre o el tama帽o de una imagen, este tipo de enfoque cargar铆a toda la imagen en la memoria. Dado que cargar y mostrar im谩genes son operaciones costosas, esto puede causar problemas de rendimiento.

    Una mejor soluci贸n ser铆a mostrar im谩genes solo cuando sea realmente necesario. En este sentido, podemos usar un proxy para envolver el existente ImageViewer objeto. De esta manera, el visor de im谩genes real solo ser谩 llamado cuando la imagen deba ser renderizada. Todas las dem谩s operaciones (como obtener el nombre de la imagen, el tama帽o, la fecha de creaci贸n, etc.) no requieren la imagen real y, por lo tanto, se pueden obtener a trav茅s de un objeto proxy mucho m谩s ligero.

    Primero creemos nuestra interfaz principal:

    interface ImageViewer {
        public void displayImage();
    }
    

    A continuaci贸n, implementaremos el visor de im谩genes concretas. Tenga en cuenta que las operaciones que ocurren en esta clase son costosas:

    public class ConcreteImageViewer implements ImageViewer {
    
        private Image image;
    
        public ConcreteImageViewer(String path) {
            // Costly operation
            this.image = Image.load(path);
        }
    
        @Override
        public void displayImage() {
            // Costly operation
            image.display();
        }
    }
    

    Ahora implementaremos nuestro proxy de visor de im谩genes ligero. Este objeto llamar谩 al visor de im谩genes concretas solo cuando sea necesario, es decir, cuando el cliente llame al displayImage() m茅todo. Hasta entonces, no se cargar谩n ni procesar谩n im谩genes, lo que har谩 que nuestro programa sea mucho m谩s eficiente.

    public class ImageViewerProxy implements ImageViewer {
    
        private String path;
        private ImageViewer viewer;
    
        public ImageViewerProxy(String path) {
            this.path = path;
        }
    
        @Override
        public void displayImage() {
            this.viewer = new ConcreteImageViewer(this.path);
            this.viewer.displayImage();
        }
    }
    

    Finalmente, escribiremos el lado del cliente de nuestro programa. En el siguiente c贸digo, estamos creando seis visores de im谩genes diferentes. Primero, tres de ellos son los visores de im谩genes concretas que cargan autom谩ticamente las im谩genes al crearlas. Las 煤ltimas tres im谩genes no cargan ninguna imagen en la memoria durante la creaci贸n.

    Solo con la 煤ltima l铆nea el primer visor proxy comenzar谩 a cargar la imagen. En comparaci贸n con los espectadores concretos, los beneficios de rendimiento son obvios:

    public static void main(String[] args) {
        ImageViewer flowers = new ConcreteImageViewer("./photos/flowers.png");
        ImageViewer trees = new ConcreteImageViewer("./photos/trees.png");
        ImageViewer grass = new ConcreteImageViewer("./photos/grass.png");
    
        ImageViewer sky = new ImageViewerProxy("./photos/sky.png");
        ImageViewer sun = new ImageViewerProxy("./photos/sun.png");
        ImageViewer clouds = new ImageViewerProxy("./photos/clouds.png");
    
        sky.displayImage();
    }
    

    Otra cosa que podemos hacer es agregar un null-marcar en el displayImage() m茅todo del ImageViewerProxy:

    @Override
    public void displayImage() {
        if (this.viewer == null) {
           this.viewer = new ConcreteImageViewer(this.path);
        }
        this.viewer.displayImage();
    }
    

    Entonces, si llamamos:

    ImageViewer sky = new ImageViewerProxy("./photos/sky.png");
    
    sky.displayImage();
    sky.displayImage();
    

    Solo una vez new ConcreteImageViewer se ejecute la llamada. Esto reducir谩 a煤n m谩s la huella de memoria de nuestra aplicaci贸n.

    Nota: este ejemplo no contiene c贸digo Java totalmente compilable. Algunas llamadas a m茅todos, como Image.load(String path), son ficticios y est谩n escritos de forma simplificada, principalmente con fines ilustrativos.

    Ejemplo de proxy de protecci贸n

    En este ejemplo, volaremos una nave espacial. Antes de eso, necesitamos crear dos cosas: Spaceship interfaz y el Pilot modelo:

    interface Spaceship {
        public void fly();
    }
    
    public class Pilot {
        private String name;
    
        // Constructor, Getters, and Setters
    }
    

    Ahora vamos a implementar el Spaceship interfaz y crear una clase de nave espacial real:

    public class MillenniumFalcon implements Spaceship {
        @Override
        public void fly() {
            System.out.println("Welcome, Han. The Millennium Falcon is starting up its engines!");
        }
    }
    

    los MillenniumFalcon La clase representa una nave espacial concreta que puede ser utilizada por nuestro Pilot. Sin embargo, podr铆a haber algunas condiciones que nos gustar铆a comprobar antes de permitir que el piloto vuele la nave espacial. Por ejemplo, quiz谩s nos gustar铆a ver si el piloto tiene el certificado correspondiente o si tiene la edad suficiente para volar. Para comprobar estas condiciones, podemos utilizar el patr贸n de dise帽o de proxy.

    En este ejemplo, comprobaremos si el nombre del piloto es “Han Solo”, ya que es el propietario leg铆timo de la nave. Comenzamos implementando el Spaceship interfaz como antes.

    Vamos a usar Pilot y Spaceship como nuestras variables de clase ya que podemos obtener toda la informaci贸n relevante de ellas:

    public class MillenniumFalconProxy implements Spaceship {
    
        private Pilot pilot;
        private Spaceship falcon;
    
        public MillenniumFalconProxy(Pilot pilot) {
            this.pilot = pilot;
            this.falcon = new MillenniumFalcon();
        }
    
        @Override
        public void fly() {
            if (pilot.getName().equals("Han Solo")) {
                falcon.fly();
            } else {
                System.out.printf("Sorry %s, only Han Solo can fly the Falcon!n", pilotName);
            }
        }
    }
    

    El lado del cliente del programa se puede escribir como se muestra a continuaci贸n. Si “Han Solo” es el piloto, el Falcon podr谩 volar. De lo contrario, no se le permitir谩 salir del hangar:

    public static void main(String[] args) {
        Spaceship falcon1 = new MillenniumFalconProxy(new Pilot("Han Solo"));
        falcon1.fly();
    
        Spaceship falcon2 = new MillenniumFalconProxy(new Pilot("Jabba the Hutt"));
        falcon2.fly();
    }
    

    El resultado de las llamadas anteriores dar谩 como resultado lo siguiente:

    Welcome, Han. The Millennium Falcon is starting up its engines!
    Sorry Jabba the Hutt, only Han Solo can fly the Falcon!
    

    Pros y contras

    Pros

    • Seguridad: Al usar un proxy, se pueden verificar ciertas condiciones al acceder al objeto y se aplica el uso controlado de clases y recursos potencialmente “peligrosos”.
    • Actuaci贸n: Algunos objetos pueden ser muy exigentes en t茅rminos de memoria y tiempo de ejecuci贸n. Al usar un proxy, podemos envolver dichos objetos con operaciones costosas para que sean llamados solo cuando realmente se necesiten, o evitar la creaci贸n de instancias innecesarias.

    Contras

    • Actuaci贸n: S铆, el rendimiento tambi茅n puede ser una desventaja del patr贸n de proxy. 驴C贸mo, podr铆as preguntar? Digamos que un objeto proxy se usa para envolver un objeto existente en alg煤n lugar de la red. Dado que se trata de un proxy, puede ocultar al cliente el hecho de que se trata de una comunicaci贸n remota.

      Esto, a su vez, puede hacer que el cliente se sienta inclinado a escribir c贸digo ineficiente porque no se dar谩 cuenta de que se est谩 realizando una llamada de red costosa en segundo plano.

    Conclusi贸n

    El patr贸n de dise帽o de proxy es una forma inteligente de utilizar algunos recursos costosos o proporcionar ciertos derechos de acceso. Es estructuralmente similar a los patrones Adaptador y Decorador, aunque con un prop贸sito diferente.

    El proxy se puede utilizar en una variedad de circunstancias, ya que los recursos exigentes son algo com煤n en la programaci贸n, especialmente cuando se trata de bases de datos y redes.

    Por lo tanto, saber c贸mo acceder de manera eficiente a esos recursos y, al mismo tiempo, proporcionar un control de acceso adecuado, es crucial para crear aplicaciones escalables y seguras.

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *