Category: Structural
O problema
Fazer algo acontecer exige coordenar vários subsistemas numa ordem específica — chamar esse serviço, depois aquele, só prosseguir se cada passo tiver sucesso. Todo chamador que precisa desse resultado ou duplica essa lógica de orquestração, ou tem que aprender os detalhes internos de todo subsistema só pra usá-los corretamente. Os próprios subsistemas estão bem sozinhos; o que falta é uma porta de entrada mais simples pro caso comum.
A solução
Adicionar uma classe que sabe como coordenar os subsistemas corretamente, e dar aos chamadores ela em vez dos próprios subsistemas. Os subsistemas não mudam e continuam utilizáveis diretamente por chamadores com necessidades mais específicas — a fachada é um ponto de entrada mais simples adicional, não uma substituição.
classDiagram
class Facade {
+operation()
}
class SubsystemA
class SubsystemB
class SubsystemC
Facade --> SubsystemA
Facade --> SubsystemB
Facade --> SubsystemC
Client --> Facade
Exemplo clássico
classic/HomeTheaterFacade
é o exemplo canônico: watchMovie() liga o Projector,
coloca em modo widescreen, liga o Amplifier
e ajusta seu volume, então liga o DvdPlayer
e inicia o filme — seis chamadas através de três subsistemas, na única ordem que de fato
funciona, atrás de um único método.
HomeTheaterFacadeTest
verifica a sequência exata.
Exemplo aplicado: orquestração de portabilidade de salário
applied/SalaryPortabilityFacade
coordena AccountVerificationService
(essa conta é sequer elegível), BacenLookupService
(onde o salário desse pagador é pago atualmente, segundo o registro do banco central), e
NotificationService
(avisar o titular da conta que está agendado) — fazendo short-circuit no momento em que
qualquer passo falha, de modo que uma conta inelegível nunca dispara uma consulta ao BACEN, e
um pagador sem banco de folha registrado nunca dispara uma notificação. Nenhum dos três
serviços de subsistema sabe que os outros dois existem; só a fachada sabe.
SalaryPortabilityFacadeTest
cobre o caminho feliz completo e os dois casos de short-circuit.
Quando não usar
- Se os chamadores genuinamente precisam de controle fino sobre os subsistemas (ordens diferentes, pulando passos, parâmetros diferentes por chamada), uma fachada que só expõe uma operação grosseira atrapalha — exponha os subsistemas diretamente pra esses chamadores em vez disso.
- Uma fachada que cresce opções e parâmetros suficientes pra cobrir a necessidade de todo chamador para de ser uma simplificação e vira mais um subsistema pra aprender — se isso está acontecendo, a orquestração provavelmente pertence a um serviço de camada de aplicação em vez de uma única classe "fachada".
- Não use uma fachada pra esconder um design de subsistema genuinamente ruim. Isso disfarça a estranheza pros chamadores da fachada, mas quem usa os subsistemas diretamente ainda tem que lidar com ela.
Cobertura de testes
100% de cobertura de instrução, 100% de cobertura de branch (JaCoCo). Reproduza você mesmo:
./gradlew :structural:facade:jacocoTestReport
Relatório em structural/facade/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 Facade.
- Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software.
Addison-Wesley. — descreve Application Services como a camada que orquestra objetos de
domínio e infraestrutura pra cumprir um caso de uso;
SalaryPortabilityFacadetem exatamente essa forma, só com o nome do padrão do GoF em vez do nome da camada do DDD, já que este repositório ensina padrões um de cada vez em vez de uma arquitetura em camadas completa.
Testes unitários
src/test/java/com/designpatterns/structural/facade/classic/HomeTheaterFacadeTest.java
package com.designpatterns.structural.facade.classic;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class HomeTheaterFacadeTest {
@Test
void watchMovieOrchestratesEverySubsystemInTheRightOrder() {
HomeTheaterFacade homeTheater = new HomeTheaterFacade(new Amplifier(), new DvdPlayer(), new Projector());
var log = homeTheater.watchMovie("The Matrix");
assertThat(log).containsExactly(
"Projector on",
"Projector in widescreen mode",
"Amplifier on",
"Amplifier volume set to 5",
"DVD player on",
"Playing \"The Matrix\""
);
}
}
src/test/java/com/designpatterns/structural/facade/applied/SalaryPortabilityFacadeTest.java
package com.designpatterns.structural.facade.applied;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class SalaryPortabilityFacadeTest {
private final AccountVerificationService verification = new AccountVerificationService(Set.of("acc-1"));
private final BacenLookupService bacenLookup = new BacenLookupService(Map.of("111.111.111-11", "Bank A"));
private final SalaryPortabilityFacade facade =
new SalaryPortabilityFacade(verification, bacenLookup, new NotificationService());
@Test
void schedulesPortabilityWhenEverySubsystemAgrees() {
PortabilityResult result = facade.requestPortability("acc-1", "111.111.111-11");
assertThat(result.scheduled()).isTrue();
assertThat(result.fromBank()).isEqualTo("Bank A");
assertThat(result.message()).isEqualTo("Notice sent to account acc-1: portability from Bank A scheduled");
}
@Test
void rejectsAnIneligibleAccountWithoutEverCallingBacen() {
PortabilityResult result = facade.requestPortability("acc-unknown", "111.111.111-11");
assertThat(result.scheduled()).isFalse();
assertThat(result.message()).isEqualTo("account not eligible for portability");
}
@Test
void rejectsWhenBacenHasNoPayrollRegistrationForTheTaxId() {
PortabilityResult result = facade.requestPortability("acc-1", "999.999.999-99");
assertThat(result.scheduled()).isFalse();
assertThat(result.message()).isEqualTo("no payroll registration found at BACEN");
}
}