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
- Se a operação "cara" genuinamente precisa rodar toda vez (os dados subjacentes mudam entre chamadas e desatualização é inaceitável), colocá-la em cache atrás de um proxy introduz um bug de correção, não uma otimização. Conheça a tolerância a dados desatualizados antes de recorrer a isso.
- Um cache que nunca descarta nada é um vazamento de memória esperando pra acontecer assim que
o espaço de chaves for ilimitado (ver o
RateLimitDecoratordo módulo Decorator deste repositório pra o mesmo tipo de preocupação com contadores por pagador) — um cache real precisa de uma política de descarte ou expiração, que este exemplo deliberadamente mínimo não inclui. - Se o objetivo é adicionar comportamento novo em cima de um objeto em vez de controlar acesso a ele, isso é Decorator, não Proxy — os dois padrões têm diagramas de classe quase idênticos e se distinguem pela intenção, não pela estrutura.
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
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of
Reusable Object-Oriented Software. Addison-Wesley. — o Capítulo 4 formaliza o Proxy,
nomeando explicitamente o proxy virtual (criação de objeto cara, preguiçosa —
ImageProxyaqui) e o proxy de proteção (controle de acesso) como duas de suas variantes principais, e contrasta Proxy com Decorator pela intenção, não pela estrutura. - Belady, L. A. (1966). "A Study of Replacement Algorithms for a Virtual-Storage Computer."
IBM Systems Journal, 5(2), 78–101. — o artigo fundacional sobre política de substituição de
cache; diretamente relevante ao aviso de "Quando não usar" acima, já que o cache do
CachingCreditScoreProxydeliberadamente não tem política de descarte nenhuma, que é a primeira coisa que uma versão de produção precisaria.
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);
}
}