Strategy
Encapsuler des algorithmes interchangeables
Problème
Plusieurs variantes d'un algorithme doivent être utilisables de façon interchangeable. Plutôt qu'un switch qui grandit à chaque nouvelle variante, on encapsule chaque algorithme dans un objet.
Exemple : stratégies de tri
interface SortStrategy<T> {
sort(items: T[], compareFn: (a: T, b: T) => number): T[];
}
class QuickSort<T> implements SortStrategy<T> {
sort(items: T[], compareFn: (a: T, b: T) => number): T[] {
return [...items].sort(compareFn); // Array.prototype.sort utilise quicksort
}
}
class StableSort<T> implements SortStrategy<T> {
sort(items: T[], compareFn: (a: T, b: T) => number): T[] {
return [...items]
.map((item, index) => ({ item, index }))
.sort((a, b) => compareFn(a.item, b.item) || a.index - b.index)
.map(({ item }) => item);
}
}
class ProductList {
constructor(
private items: Product[],
private sortStrategy: SortStrategy<Product>
) {}
setSortStrategy(strategy: SortStrategy<Product>) {
this.sortStrategy = strategy;
}
getSorted(field: keyof Product): Product[] {
return this.sortStrategy.sort(
this.items,
(a, b) => String(a[field]).localeCompare(String(b[field]))
);
}
}Stratégies de validation
Pattern très courant en front-end :
type ValidationResult = { valid: true } | { valid: false; error: string };
type Validator = (value: string) => ValidationResult;
const required: Validator = (value) =>
value.trim() ? { valid: true } : { valid: false, error: 'Ce champ est requis' };
const email: Validator = (value) =>
/\S+@\S+\.\S+/.test(value)
? { valid: true }
: { valid: false, error: 'Email invalide' };
const minLength = (min: number): Validator => (value) =>
value.length >= min
? { valid: true }
: { valid: false, error: `Minimum ${min} caractères` };
const maxLength = (max: number): Validator => (value) =>
value.length <= max
? { valid: true }
: { valid: false, error: `Maximum ${max} caractères` };
// Composition de stratégies
function validate(value: string, validators: Validator[]): string | null {
for (const validator of validators) {
const result = validator(value);
if (!result.valid) return result.error;
}
return null;
}
// Chaque champ a ses propres stratégies de validation
const passwordValidators = [required, minLength(8), maxLength(100)];
const emailValidators = [required, email];
const passwordError = validate(password, passwordValidators);
const emailError = validate(emailValue, emailValidators);Strategy en React : hooks
// Stratégies de fetching interchangeables
interface FetchStrategy<T> {
fetch(params: unknown): Promise<T>;
}
class RestFetchStrategy<T> implements FetchStrategy<T> {
constructor(private url: string) {}
async fetch(params: Record<string, string>): Promise<T> {
const query = new URLSearchParams(params).toString();
return fetch(`${this.url}?${query}`).then(r => r.json());
}
}
class GraphQLFetchStrategy<T> implements FetchStrategy<T> {
constructor(private query: string) {}
async fetch(variables: unknown): Promise<T> {
return fetch('/graphql', {
method: 'POST',
body: JSON.stringify({ query: this.query, variables }),
}).then(r => r.json()).then(r => r.data);
}
}
function useDataFetch<T>(strategy: FetchStrategy<T>, params: unknown) {
return useQuery({
queryKey: ['data', params],
queryFn: () => strategy.fetch(params),
});
}
// En prod avec une REST API
const productsStrategy = new RestFetchStrategy<Product[]>('/api/products');
const { data } = useDataFetch(productsStrategy, { category: 'electronics' });Stratégies de paiement
interface PaymentStrategy {
pay(amount: number): Promise<{ success: boolean; transactionId: string }>;
}
class StripePayment implements PaymentStrategy {
constructor(private paymentMethodId: string) {}
async pay(amount: number) {
const intent = await stripe.confirmCardPayment(clientSecret, {
payment_method: this.paymentMethodId,
});
return { success: !!intent.paymentIntent, transactionId: intent.paymentIntent?.id ?? '' };
}
}
class PayPalPayment implements PaymentStrategy {
async pay(amount: number) {
const order = await paypal.createOrder({ amount });
return { success: order.status === 'COMPLETED', transactionId: order.id };
}
}
// Le processus de checkout ne connaît pas la stratégie de paiement
async function processOrder(order: Order, paymentStrategy: PaymentStrategy) {
const result = await paymentStrategy.pay(order.total);
if (!result.success) throw new Error('Payment failed');
await orderService.confirm(order.id, result.transactionId);
}Exercice : Strategy dans le Pokémon Builder
Contexte
Dans App.tsx, le filtrage des Pokémon est fait inline dans le rendu :
const filtered = pokemons.filter((p) => {
const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase());
const matchesType = selectedType === "all" || p.types.includes(selectedType);
return matchesSearch && matchesType;
});Ajouter un tri (par stats, par numéro de Pokédex, par nom) implique de modifier ce bloc directement. Il n'y a aucune séparation entre le "comment filtrer" et le "quand filtrer".
Objectif
Extraire les algorithmes de filtrage et de tri dans des stratégies interchangeables.
Instructions
-
Crée le fichier
src/strategy/filterStrategies.ts. -
Définis une interface
FilterStrategyavec une méthodeapply(pokemons: Pokemon[]): Pokemon[]. -
Implémente trois stratégies :
ByNameStrategy(query: string), filtre par nomByTypeStrategy(type: string), filtre par type ("all"retourne tout)BySortStrategy(criterion: "name" | "hp" | "attack" | "speed"), trie par critère
-
Dans
App.tsx, ajoute un étatsortByet compose les stratégies pour produire la liste filtrée et triée. -
Ajoute un
<select>dansFilterBarpour choisir le critère de tri.
Résultat attendu
Ajouter un nouveau critère de tri ou de filtrage ne nécessite d'écrire qu'une nouvelle classe, sans toucher à App.tsx.