Category: Structural
El problema
Lograr que algo se haga requiere coordinar varios subsistemas en un orden específico — llamar a este servicio, luego a aquel, avanzar solo si cada paso tiene éxito. Cada llamador que necesita ese resultado o duplica esa lógica de orquestación, o tiene que aprender los internos de cada subsistema solo para usarlos correctamente. Los propios subsistemas están bien por sí solos; lo que falta es una puerta de entrada más simple para el caso común.
La solución
Agregar una clase que sepa cómo coordinar los subsistemas correctamente, y darle eso a los llamadores en vez de los propios subsistemas. Los subsistemas no cambian y siguen siendo utilizables directamente para llamadores con necesidades más específicas — la fachada es un punto de entrada más simple adicional, no un reemplazo.
classDiagram
class Facade {
+operation()
}
class SubsystemA
class SubsystemB
class SubsystemC
Facade --> SubsystemA
Facade --> SubsystemB
Facade --> SubsystemC
Client --> Facade
Ejemplo clásico
classic/HomeTheaterFacade
es el ejemplo canónico: watchMovie() enciende el Projector,
lo pone en modo panorámico, enciende el Amplifier
y ajusta su volumen, luego enciende el DvdPlayer
e inicia la película — seis llamadas a través de tres subsistemas, en el único orden que
realmente funciona, detrás de un solo método.
HomeTheaterFacadeTest
verifica la secuencia exacta.
Ejemplo aplicado: orquestación de portabilidad salarial
applied/SalaryPortabilityFacade
coordina AccountVerificationService
(esta cuenta es siquiera elegible), BacenLookupService
(dónde se paga actualmente el salario de este pagador, según el registro del banco central), y
NotificationService
(avisar al titular de la cuenta que está programado) — cortando en el momento en que cualquier
paso falla, de modo que una cuenta no elegible nunca dispara una consulta a BACEN, y un pagador
sin banco de nómina registrado nunca dispara una notificación. Ninguno de los tres servicios de
subsistema sabe que existen los otros dos; solo la fachada lo sabe.
SalaryPortabilityFacadeTest
cubre el camino feliz completo y ambos casos de corte.
Cuándo no usarlo
- Si los llamadores realmente necesitan control fino sobre los subsistemas (órdenes distintos, saltar pasos, parámetros distintos por llamada), una fachada que solo expone una operación gruesa estorba — exponga los subsistemas directamente para esos llamadores en su lugar.
- Una fachada que crece suficientes opciones y parámetros para cubrir la necesidad de cada llamador deja de ser una simplificación y se convierte en otro subsistema que aprender — si eso está pasando, la orquestación probablemente pertenece a un servicio de capa de aplicación en vez de una única clase "fachada".
- No use una fachada para ocultar un diseño de subsistema genuinamente malo. Eso disimula lo incómodo para los llamadores de la fachada, pero quien use los subsistemas directamente igual tiene que lidiar con ello.
Cobertura de pruebas
100% de cobertura de instrucciones, 100% de cobertura de ramas (JaCoCo). Reprodúzcalo usted mismo:
./gradlew :structural:facade:jacocoTestReport
Informe en structural/facade/build/reports/jacoco/test/html/index.html.
Lecturas adicionales
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. — el Capítulo 4 formaliza Facade.
- Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software.
Addison-Wesley. — describe los Application Services como la capa que orquesta objetos de
dominio e infraestructura para cumplir un caso de uso;
SalaryPortabilityFacadetiene exactamente esa forma, solo con el nombre del patrón del GoF en vez del nombre de la capa de DDD, ya que este repositorio enseña patrones de a uno en vez de una arquitectura en capas completa.
Pruebas unitarias
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");
}
}