Introducción
Contenido
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.
Te puede interesar:Nube de Spring: HystrixHay 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.
Te puede interesar:Tutorial de Spring ReactorTener 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:
Te puede interesar:Manejo de excepciones en Springpublic 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:
Te puede interesar:Validación de contraseña personalizada de Springpublic 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.