← Todos los patrones

Proxy

Structural · ver código fuente en GitHub

Leer en: English · Português · Español

Category: Structural

El problema

Acceder a un objeto directamente a veces es costoso, lento, o necesita una verificación aplicada cada vez — una llamada de red, la carga de un recurso grande, una comprobación de permisos. Hacer que cada llamador recuerde aplicar esa lógica por sí mismo (verificar el caché primero, comprobar permisos, aplazar la carga hasta que realmente se necesite) significa que la lógica termina duplicada u olvidada en algún punto de llamada eventualmente.

La solución

Introducir un sustituto que implemente exactamente la misma interfaz que el objeto real, y poner la lógica extra (caché, carga perezosa, control de acceso) dentro del sustituto en vez de en cada punto de llamada. Los llamadores mantienen el proxy y lo usan exactamente como lo real — no pueden notar la diferencia solo por la interfaz.

classDiagram
    class Subject {
        <<interface>>
    }
    class RealSubject
    class Proxy {
        -realSubject
    }
    Subject <|.. RealSubject
    Subject <|.. Proxy
    Proxy --> RealSubject : controls access to
    Client --> Subject

Ejemplo clásico

classic/ImageProxy implementa la misma interfaz Image que RealImage, pero no construye la imagen real (costosa de cargar) hasta la primera llamada a display() — el proxy virtual canónico, aplazando una carga costosa hasta que realmente se necesita en vez de en el momento de la construcción. ImageProxyTest verifica que la imagen real genuinamente no se carga antes de la primera llamada a display(), y que una segunda llamada reutiliza la misma imagen ya cargada en vez de recargarla.

Ejemplo aplicado: caché de una consulta costosa a un buró de crédito

applied/CachingCreditScoreProxy implementa el mismo contrato CreditScoreBureau que ExternalCreditScoreBureau — un sustituto de una llamada real a un buró externo que es lenta y, en producción, se cobra por solicitud. Un flujo de aprobación de crédito que llama a lookupScore() varias veces para el mismo solicitante (una en la admisión, otra en el underwriting, otra en la aprobación final, digamos) solo dispara una llamada externa real; cada llamada posterior a la primera se atiende desde el caché del proxy. CachingCreditScoreProxyTest prueba esto directamente contando las llamadas reales al buró subyacente, y confirma que solicitantes distintos siguen disparando cada uno su propia consulta real.

Cuándo no usarlo

Cobertura de pruebas

100% de cobertura de instrucciones, 100% de cobertura de ramas (JaCoCo). Reprodúzcalo usted mismo:

./gradlew :structural:proxy:jacocoTestReport

Informe en structural/proxy/build/reports/jacoco/test/html/index.html.

Lecturas adicionales

Pruebas unitarias

src/test/java/com/designpatterns/structural/proxy/classic/ImageProxyTest.java
package com.designpatterns.structural.proxy.classic;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class ImageProxyTest {

    @Test
    void theRealImageIsNotLoadedUntilTheFirstDisplayCall() {
        ImageProxy proxy = new ImageProxy("photo.png");

        assertThat(proxy.isLoaded()).isFalse();

        String result = proxy.display();

        assertThat(proxy.isLoaded()).isTrue();
        assertThat(result).isEqualTo("Displaying photo.png");
    }

    @Test
    void repeatedDisplayCallsReuseTheAlreadyLoadedImage() {
        ImageProxy proxy = new ImageProxy("photo.png");

        proxy.display();
        String secondResult = proxy.display();

        assertThat(secondResult).isEqualTo("Displaying photo.png");
        assertThat(proxy.isLoaded()).isTrue();
    }
}
src/test/java/com/designpatterns/structural/proxy/applied/CachingCreditScoreProxyTest.java
package com.designpatterns.structural.proxy.applied;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class CachingCreditScoreProxyTest {

    @Test
    void repeatedLookupsForTheSameTaxIdHitTheRealBureauOnlyOnce() {
        ExternalCreditScoreBureau realBureau = new ExternalCreditScoreBureau();
        CachingCreditScoreProxy proxy = new CachingCreditScoreProxy(realBureau);

        int first = proxy.lookupScore("111.111.111-11");
        int second = proxy.lookupScore("111.111.111-11");
        int third = proxy.lookupScore("111.111.111-11");

        assertThat(first).isEqualTo(second).isEqualTo(third);
        assertThat(realBureau.callCount()).isEqualTo(1);
    }

    @Test
    void differentTaxIdsEachTriggerTheirOwnRealBureauCall() {
        ExternalCreditScoreBureau realBureau = new ExternalCreditScoreBureau();
        CachingCreditScoreProxy proxy = new CachingCreditScoreProxy(realBureau);

        proxy.lookupScore("111.111.111-11");
        proxy.lookupScore("222.222.222-22");

        assertThat(realBureau.callCount()).isEqualTo(2);
    }

    @Test
    void theProxyReturnsExactlyWhatTheRealBureauWouldHaveReturned() {
        ExternalCreditScoreBureau realBureau = new ExternalCreditScoreBureau();
        CachingCreditScoreProxy proxy = new CachingCreditScoreProxy(realBureau);
        String taxId = "333.333.333-33";

        int viaProxy = proxy.lookupScore(taxId);
        int direct = realBureau.lookupScore(taxId);

        assertThat(viaProxy).isEqualTo(direct);
    }
}

Ver informe completo de cobertura JaCoCo →