Singleton
Garantir une instance unique et fournir un point d'accès global
Problème
Certains objets ne doivent exister qu'en une seule instance : une connexion à une base de données, une configuration applicative, un logger. Créer plusieurs instances causerait des incohérences ou du gaspillage de ressources.
Implémentation classique
class DatabaseConnection {
private static instance: DatabaseConnection | null = null;
private connection: unknown;
private constructor() {
// La connexion est initialisée une seule fois
this.connection = createConnection(process.env.DATABASE_URL!);
}
static getInstance(): DatabaseConnection {
if (!DatabaseConnection.instance) {
DatabaseConnection.instance = new DatabaseConnection();
}
return DatabaseConnection.instance;
}
query(sql: string) {
// ...
}
}
// Usage
const db = DatabaseConnection.getInstance();
db.query('SELECT * FROM users');Le constructeur privé empêche new DatabaseConnection() depuis l'extérieur.
Le module ES comme Singleton
En TypeScript/JavaScript, un module n'est chargé qu'une seule fois par le bundler. Les exports d'un module constituent naturellement un Singleton.
// config.ts, chargé une seule fois, shared partout
const config = {
apiUrl: process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000',
timeout: 5000,
maxRetries: 3,
} as const;
export type Config = typeof config;
export default config;
// Utilisation
import config from '@/config';
fetch(config.apiUrl + '/users');// http-client.ts, une instance d'axios partagée
import axios from 'axios';
import config from '@/config';
const httpClient = axios.create({
baseURL: config.apiUrl,
timeout: config.timeout,
});
httpClient.interceptors.request.use((req) => {
const token = localStorage.getItem('token');
if (token) req.headers.Authorization = `Bearer ${token}`;
return req;
});
export default httpClient;Singleton et React : les stores Zustand
En React, les stores Zustand sont des Singletons. create retourne un hook lié à un store unique partagé entre tous les composants.
import { create } from 'zustand';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
total: () => number;
}
export const useCartStore = create<CartStore>((set, get) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) => set((state) => ({
items: state.items.filter(i => i.id !== id)
})),
total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
}));
// Partout dans l'app, useCartStore() accède au même storeLimites
Le Singleton crée un état global. L'état global rend les tests difficiles : il persiste entre les tests si on ne le réinitialise pas.
// Problème en test
describe('CartStore', () => {
beforeEach(() => {
// Il faut réinitialiser le store entre chaque test
useCartStore.setState({ items: [] });
});
it('should add item', () => {
useCartStore.getState().addItem({ id: '1', name: 'T-shirt', price: 20, quantity: 1 });
expect(useCartStore.getState().items).toHaveLength(1);
});
});Le module ES comme Singleton a le même problème, le module est mis en cache par Node.js. Pour les tests, il vaut mieux injecter les dépendances (DIP) que de les importer directement.
Exercice : Singleton dans le Pokémon Builder
Contexte
Dans le projet pokemon-builder-starter, ouvre src/App.tsx. Tu y trouveras plusieurs valeurs codées en dur directement dans le composant :
fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
// ...
if (team.length >= 6) return;L'URL de l'API, le nombre de Pokémon à charger et la taille maximale de l'équipe sont éparpillés dans le code. Si on veut en changer un, il faut chercher dans tous les fichiers.
Objectif
Centraliser la configuration de l'application dans un module Singleton src/config.ts, puis mettre à jour App.tsx pour utiliser ces valeurs.
Instructions
-
Crée le fichier
src/config.tsavec un objetAppConfigexporté par défaut :API_URL: l'URL de base de la PokéAPIPOKEMON_LIMIT: le nombre de Pokémon à charger (151)MAX_TEAM_SIZE: la taille maximale de l'équipe (6)
-
Dans
App.tsx, importeAppConfiget remplace toutes les valeurs en dur par les propriétés de la config. -
Vérifie que l'application fonctionne exactement comme avant.
Résultat attendu
Un seul endroit dans le code à modifier pour changer la limite de l'équipe ou l'URL de l'API. Le reste de l'application n'a pas besoin d'être touché.