Structurels
Proxy
Contrôler l'accès à un objet
Problème
On veut contrôler l'accès à un objet : pour du cache, du lazy loading, des vérifications d'autorisation, ou du logging sans modifier l'objet lui-même.
Proxy de cache
interface ProductRepository {
findById(id: string): Promise<Product | null>;
findAll(): Promise<Product[]>;
}
class CachingProxy implements ProductRepository {
private cache = new Map<string, Product>();
private allProductsCache: Product[] | null = null;
constructor(private target: ProductRepository) {}
async findById(id: string): Promise<Product | null> {
if (this.cache.has(id)) return this.cache.get(id)!;
const product = await this.target.findById(id);
if (product) this.cache.set(id, product);
return product;
}
async findAll(): Promise<Product[]> {
if (this.allProductsCache) return this.allProductsCache;
const products = await this.target.findAll();
this.allProductsCache = products;
products.forEach(p => this.cache.set(p.id, p));
return products;
}
}Proxy d'autorisation
interface AdminRepository {
deleteUser(id: string): Promise<void>;
exportAllData(): Promise<Blob>;
updatePermissions(userId: string, permissions: string[]): Promise<void>;
}
class AuthorizationProxy implements AdminRepository {
constructor(
private target: AdminRepository,
private currentUser: { roles: string[] }
) {}
private assertAdmin() {
if (!this.currentUser.roles.includes('admin')) {
throw new Error('Access denied: admin role required');
}
}
async deleteUser(id: string) {
this.assertAdmin();
return this.target.deleteUser(id);
}
async exportAllData() {
this.assertAdmin();
return this.target.exportAllData();
}
async updatePermissions(userId: string, permissions: string[]) {
this.assertAdmin();
return this.target.updatePermissions(userId, permissions);
}
}Proxy JavaScript natif
JavaScript expose Proxy en natif, utile pour de la validation automatique ou de l'observation :
function createValidatedStore<T extends object>(target: T): T {
return new Proxy(target, {
set(obj, prop, value) {
// Validation : les strings ne peuvent pas être vides
if (typeof value === 'string' && value.trim() === '') {
throw new Error(`Property ${String(prop)} cannot be empty`);
}
obj[prop as keyof T] = value;
return true;
},
});
}
const user = createValidatedStore({ name: 'Alice', email: 'alice@example.com' });
user.name = ''; // Error: Property name cannot be emptyLazy Proxy
Pour des objets coûteux à initialiser :
function createLazy<T>(factory: () => T): T {
let instance: T | null = null;
return new Proxy({} as T, {
get(_, prop) {
if (!instance) instance = factory();
return (instance as Record<string | symbol, unknown>)[prop];
},
});
}
// La connexion à la DB n'est initialisée qu'au premier accès
const db = createLazy(() => new DatabaseConnection());
// ... plus tard dans le code
db.query('SELECT 1'); // initialisation ici seulementEn React : React.lazy
React.lazy est une implémentation du pattern Proxy pour le lazy loading de composants :
// Le composant n'est chargé qu'au premier rendu
const HeavyChart = React.lazy(() => import('./HeavyChart'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Afficher le graphe</button>
{showChart && (
<Suspense fallback={<Spinner />}>
<HeavyChart />
</Suspense>
)}
</div>
);
}Proxy vs Decorator
Les deux patterns enveloppent un objet et implémentent la même interface. La distinction :
- Decorator : ajoute des fonctionnalités, enrichit le comportement
- Proxy : contrôle l'accès, peut bloquer ou différer les appels
En pratique la frontière est floue. Un proxy de cache "décore" aussi l'objet. Ce qui compte c'est l'intention : contrôle d'accès (Proxy) vs enrichissement (Decorator).