Pattern Facade
Fournissez une interface simplifiée unique vers un sous-système complexe.
Le Problème
Vous avez un flux d'inscription utilisateur complexe : valider l'input, hasher le mot de passe, sauvegarder en base, envoyer un email de bienvenue. L'approche naïve disperse tous ces appels à chaque point d'inscription.
// Dispersé dans les controllers, routes, tests...
const validated = validateUser(data);
const hashed = await hashPassword(validated.password);
const user = await saveToDb({ ...validated, password: hashed });
await sendWelcomeEmail(user.email);
Chaque endroit qui inscrit un utilisateur répète la même séquence. Changer l'ordre ou ajouter une étape ? Il faut traquer chaque site d'appel.
La Solution
Une fonction qui orchestre les sous-systèmes. C'est tout.
const validateUser = (data: UserInput) => { /* ... */ };
const hashPassword = (password: string) => { /* ... */ };
const saveToDb = (user: User) => { /* ... */ };
const sendWelcomeEmail = (email: string) => { /* ... */ };
// Facade : une fonction, un seul endroit
async function registerUser(data: UserInput): Promise<User> {
const validated = validateUser(data);
const hashed = await hashPassword(validated.password);
const user = await saveToDb({ ...validated, password: hashed });
await sendWelcomeEmail(user.email);
return user;
}
// Le client ne voit que registerUser
await registerUser({ name: "Alice", email: "alice@example.com", password: "..." });
Pas besoin de classe. Une fonction qui appelle des fonctions est déjà une facade.
Cette solution n'utilise pas Pithos. C'est justement le point.
En TypeScript fonctionnel, toute fonction qui simplifie une opération complexe est une facade. Eidos exporte une fonction @deprecated createFacade() qui n'existe que pour vous guider ici.
Démo
Tapez un ID utilisateur et cliquez sur Fetch. Basculez entre "Without Facade" (6 étapes s'exécutant visuellement une par une) et "With Facade" (un seul appel fetchUser(id)). Même résultat, expérience radicalement différente.
- facade.ts
- Usage
/**
* API Request Facade.
*
* 6 subsystem steps vs 1 facade function — same result, different experience.
* fetchUser() is the facade: one call orchestrates everything.
*/
import type { User } from "./types";
const USERS: Record<number, User> = {
1: { id: 1, name: "Alice Martin", email: "alice@example.com", role: "admin", lastLogin: "2026-03-25T14:30:00Z" },
2: { id: 2, name: "Bob Chen", email: "bob@example.com", role: "editor", lastLogin: "2026-03-24T09:15:00Z" },
3: { id: 3, name: "Clara Dupont", email: "clara@example.com", role: "viewer", lastLogin: "2026-03-20T18:45:00Z" },
42: { id: 42, name: "Douglas Adams", email: "douglas@example.com", role: "admin", lastLogin: "1979-10-12T00:00:00Z" },
};
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
export async function validateInput(userId: number): Promise<string> {
await delay(300);
if (!Number.isInteger(userId) || userId < 1) throw new Error(`Invalid user ID: ${userId}`);
return `ID ${userId} is valid`;
}
export async function buildAuthHeaders(): Promise<string> {
await delay(400);
return `Authorization: Bearer ${btoa(`demo-token-${Date.now()}`).slice(0, 20)}…`;
}
export async function serializeRequest(userId: number): Promise<string> {
await delay(200);
return `POST /api/users → ${JSON.stringify({ id: userId, fields: ["name", "email", "role", "lastLogin"] }).slice(0, 40)}…`;
}
export async function executeFetch(userId: number): Promise<User> {
await delay(500);
const user = USERS[userId];
if (!user) throw new Error(`User ${userId} not found`);
return user;
}
export async function parseResponse(user: User): Promise<string> {
await delay(250);
if (!user.name || !user.email) throw new Error("Malformed response");
return `Parsed: ${user.name} <${user.email}>`;
}
export async function formatResult(user: User): Promise<User> {
await delay(150);
return { ...user, lastLogin: new Date(user.lastLogin).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }) };
}
/** The Facade — one call, same result */
export async function fetchUser(userId: number): Promise<User> {
await validateInput(userId);
await buildAuthHeaders();
await serializeRequest(userId);
const raw = await executeFetch(userId);
await parseResponse(raw);
return formatResult(raw);
}
import { useState, useCallback } from "react";
import { validateInput, buildAuthHeaders, serializeRequest, executeFetch, parseResponse, formatResult, fetchUser } from "@/lib/facade";
import { STEP_KEYS, makeInitialSteps } from "@/data/steps";
import type { User, StepState, Mode } from "@/lib/types";
export function useApiFacade() {
const [mode, setMode] = useState<Mode>("expanded");
const [userId, setUserId] = useState("42");
const [steps, setSteps] = useState(makeInitialSteps);
const [result, setResult] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
const [running, setRunning] = useState(false);
const [facadeDuration, setFacadeDuration] = useState<number | null>(null);
const [totalExpandedDuration, setTotalExpandedDuration] = useState<number | null>(null);
const reset = useCallback(() => {
setSteps(makeInitialSteps());
setResult(null);
setError(null);
setFacadeDuration(null);
setTotalExpandedDuration(null);
}, []);
const updateStep = (key: string, update: Partial<StepState>) => {
setSteps((prev) => ({ ...prev, [key]: { ...prev[key], ...update } }));
};
const runStep = async (key: string, fn: () => Promise<string>) => {
updateStep(key, { status: "running" });
const t0 = performance.now();
const detail = await fn();
updateStep(key, { status: "done", detail, durationMs: performance.now() - t0 });
};
const runExpanded = useCallback(async () => {
reset();
setRunning(true);
const id = Number(userId);
const t0 = performance.now();
try {
await runStep("validate", () => validateInput(id));
await runStep("auth", () => buildAuthHeaders());
await runStep("serialize", () => serializeRequest(id));
let raw: User | undefined;
await runStep("fetch", async () => { raw = await executeFetch(id); return `Found: ${raw.name}`; });
const fetched = raw as User;
await runStep("parse", () => parseResponse(fetched));
let formatted: User | undefined;
await runStep("format", async () => { formatted = await formatResult(fetched); return "Dates & fields formatted"; });
setTotalExpandedDuration(performance.now() - t0);
setResult(formatted as User);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
setError(msg);
setSteps((prev) => {
const updated = { ...prev };
for (const key of STEP_KEYS) {
if (updated[key].status === "running") updated[key] = { ...updated[key], status: "error", detail: msg };
}
return updated;
});
} finally { setRunning(false); }
}, [userId, reset]);
const runFacade = useCallback(async () => {
reset();
setRunning(true);
const id = Number(userId);
try {
const t0 = performance.now();
const user = await fetchUser(id);
setFacadeDuration(performance.now() - t0);
setResult(user);
} catch (e) { setError(e instanceof Error ? e.message : String(e)); }
finally { setRunning(false); }
}, [userId, reset]);
const doneCount = STEP_KEYS.filter((k) => steps[k].status === "done").length;
return {
mode, setMode, userId, setUserId, steps, result, error, running,
facadeDuration, totalExpandedDuration, doneCount,
reset, handleFetch: mode === "expanded" ? runExpanded : runFacade,
};
}
API
- facade
@deprecated— écrivez simplement une fonction
Liens connexes
- Eidos : Module Design Patterns Les 23 patterns GoF réimaginés pour le TypeScript fonctionnel
- Pourquoi la FP plutôt que la POO ? La philosophie derrière Eidos : pas de classes, pas d'héritage, juste des fonctions et des types
- Zygos Result Combinez vos facades avec
Resultpour une gestion d'erreurs typée