Decorator
Ajouter du comportement à un objet sans modifier sa classe
Problème
On veut ajouter des fonctionnalités à un objet existant (logging, caching, validation, authentification) sans modifier sa classe ni créer une explosion de sous-classes.
Decorator de service
interface UserRepository {
findById(id: string): Promise<User | null>;
findAll(): Promise<User[]>;
save(user: User): Promise<User>;
}
// Implémentation de base
class ApiUserRepository implements UserRepository {
async findById(id: string) { /* appel API */ }
async findAll() { /* appel API */ }
async save(user: User) { /* appel API */ }
}
// Decorator : ajoute du cache sans modifier ApiUserRepository
class CachedUserRepository implements UserRepository {
private cache = new Map<string, User>();
constructor(private repository: UserRepository) {}
async findById(id: string): Promise<User | null> {
if (this.cache.has(id)) {
return this.cache.get(id)!;
}
const user = await this.repository.findById(id);
if (user) this.cache.set(id, user);
return user;
}
async findAll(): Promise<User[]> {
return this.repository.findAll();
}
async save(user: User): Promise<User> {
const saved = await this.repository.save(user);
this.cache.set(saved.id, saved); // met à jour le cache
return saved;
}
}
// Decorator : ajoute du logging
class LoggedUserRepository implements UserRepository {
constructor(private repository: UserRepository, private logger: Logger) {}
async findById(id: string) {
this.logger.info(`findById: ${id}`);
const result = await this.repository.findById(id);
this.logger.info(`findById: ${id} → ${result ? 'found' : 'not found'}`);
return result;
}
async findAll() {
this.logger.info('findAll');
return this.repository.findAll();
}
async save(user: User) {
this.logger.info(`save: ${user.id}`);
return this.repository.save(user);
}
}
// Composition de decorators
const repository = new LoggedUserRepository(
new CachedUserRepository(
new ApiUserRepository()
),
logger
);Higher-Order Components en React
Les HOC sont le pattern Decorator appliqué aux composants React :
// HOC : ajoute une garde d'authentification
function withAuth<P extends object>(WrappedComponent: React.ComponentType<P>) {
return function AuthGuard(props: P) {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) return <Spinner />;
if (!isAuthenticated) return <Navigate to="/login" />;
return <WrappedComponent {...props} />;
};
}
// HOC : ajoute un boundary d'erreur
function withErrorBoundary<P extends object>(WrappedComponent: React.ComponentType<P>) {
return function WithErrorBoundary(props: P) {
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<WrappedComponent {...props} />
</ErrorBoundary>
);
};
}
// Composition de HOC
const ProtectedDashboard = withAuth(withErrorBoundary(Dashboard));Custom hooks comme decorators
En React moderne, les custom hooks sont souvent préférés aux HOC pour décorer le comportement :
// Hook de base
function useUsers() {
return useQuery({ queryKey: ['users'], queryFn: fetchUsers });
}
// "Decorator" : ajoute du logging autour du hook
function useUsersWithLogging() {
const query = useUsers();
useEffect(() => {
if (query.isError) {
logger.error('Failed to fetch users', query.error);
}
}, [query.isError, query.error]);
return query;
}TypeScript decorators
TypeScript supporte les décorateurs de classe (stage 3). Utiles avec des frameworks comme NestJS :
function Log(target: unknown, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
console.log(`${key} called with`, args);
const result = await original.apply(this, args);
console.log(`${key} returned`, result);
return result;
};
return descriptor;
}
class UserService {
@Log
async getUser(id: string): Promise<User> {
return fetch(`/api/users/${id}`).then(r => r.json());
}
}Exercice : Decorator dans le Pokémon Builder
Contexte
Certains Pokémon méritent un traitement spécial : afficher un score de puissance calculé depuis les stats, ou marquer visuellement les Pokémon légendaires. Ajouter ces données directement dans le type Pokemon polluerait le modèle de base avec des informations qui ne concernent que l'affichage.
Objectif
Implémenter deux décorateurs fonctionnels qui enrichissent un Pokemon sans modifier son type de base.
Instructions
-
Crée le fichier
src/decorator/pokemonDecorators.ts. -
Définis un type
DecoratedPokemonqui étendPokemonavec deux champs optionnels :powerLevel: number, somme de toutes les stats de baseisLegendary: boolean
-
Implémente deux fonctions décorateur :
withPowerLevel(pokemon: Pokemon): DecoratedPokemon, calcule et ajoute lepowerLevelwithLegendary(pokemon: Pokemon, legendaryIds: number[]): DecoratedPokemon, marque le Pokémon comme légendaire si son id est dans la liste
-
Dans
PokemonCard, affiche lepowerLevelsi présent. -
Dans
App.tsx, applique les deux décorateurs aux Pokémon après le chargement.
Les ids légendaires de la génération 1 : [144, 145, 146, 150, 151].
Résultat attendu
La carte d'un Pokémon légendaire affiche un indicateur visuel et son score de puissance, sans que le type Pokemon ni la PokemonFactory n'aient été modifiés.