← Todos os padrões

Proxy

Structural · ver código-fonte no GitHub

Leia em: English · Português · Español

Category: Structural

O problema

Acessar um objeto diretamente às vezes é caro, lento, ou precisa de uma checagem aplicada toda vez — uma chamada de rede, o carregamento de um recurso grande, uma verificação de permissão. Fazer com que todo chamador se lembre de aplicar essa lógica por conta própria (checar o cache primeiro, verificar permissão, adiar o carregamento até realmente precisar) significa que a lógica acaba duplicada ou esquecida em algum ponto de chamada eventualmente.

A solução

Introduzir um substituto que implementa exatamente a mesma interface do objeto real, e colocar a lógica extra (cache, carregamento preguiçoso, controle de acesso) dentro do substituto em vez de em cada ponto de chamada. Chamadores seguram o proxy e o usam exatamente como a coisa real — eles não conseguem notar a diferença só pela interface.

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

Exemplo clássico

classic/ImageProxy implementa a mesma interface Image que RealImage, mas não constrói a imagem real (cara de carregar) até a primeira chamada de display() — o proxy virtual canônico, adiando um carregamento custoso até de fato ser necessário em vez de no momento da construção. ImageProxyTest verifica que a imagem real genuinamente não é carregada antes da primeira chamada de display(), e que uma segunda chamada reutiliza a mesma imagem já carregada em vez de recarregá-la.

Exemplo aplicado: cache de uma consulta cara a um bureau de crédito

applied/CachingCreditScoreProxy implementa o mesmo contrato CreditScoreBureau que ExternalCreditScoreBureau — um substituto pra uma chamada real a um bureau externo que é lenta e, em produção, cobrada por requisição. Um fluxo de aprovação de crédito que chama lookupScore() várias vezes pro mesmo solicitante (uma na entrada, outra no underwriting, outra na aprovação final, digamos) só dispara uma chamada externa real; toda chamada depois da primeira é atendida pelo cache do proxy. CachingCreditScoreProxyTest prova isso diretamente contando chamadas reais no bureau subjacente, e confirma que solicitantes diferentes ainda disparam cada um sua própria consulta real.

Quando não usar

Cobertura de testes

100% de cobertura de instrução, 100% de cobertura de branch (JaCoCo). Reproduza você mesmo:

./gradlew :structural:proxy:jacocoTestReport

Relatório em structural/proxy/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

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 relatório completo de cobertura JaCoCo →