Polar Code 🎭

Command Palette

Search for a command to run...

11
Pièce N°11

CHAPITRE 10 : PROBLÈMES DE STRUCTURE

Le dossier s’ouvrit sur une photo d’une tuyauterie industrielle. Un écheveau de conduites, de vannes, de raccords de toutes tailles. Certaines soudées, d’autres bricolées avec des adaptateurs rouillés. Le système avait grandi, greffé de nouvelles fonctionnalités sur une vieille charpente. Résultat : des hiérarchies monolithiques, des composants qui ne se parlaient pas, une masse impossible à déplacer. Le problème n’était plus la création. C’était la structure elle-même. L’architecture était malade.

Hiérarchies rigides

L’héritage, c’était la promesse. Une belle hiérarchie claire, des concepts généraux qui se spécialisent. Sauf que le monde réel est têtu.

// L'arbre généalogique qui se transforme en monstre.
abstract class ComposantUI {
    abstract afficher(): void;
}

class Conteneur extends ComposantUI {
    private enfants: ComposantUI[] = [];
    ajouter(enfant: ComposantUI) { this.enfants.push(enfant); }
    afficher() { /* Affiche le conteneur et ses enfants */ }
}

class Bouton extends ComposantUI {
    afficher() { /* Affiche un bouton */ }
}

class ChampTexte extends ComposantUI {
    afficher() { /* Affiche un champ texte */ }
}

// Jusque-là, tout va bien. Et puis arrive le besoin...
class Onglet extends ComposantUI {
    // Un onglet EST-IL un ComposantUI ? Oui.
    // Mais il DOIT aussi pouvoir contenir d'autres composants, comme un Conteneur.
    // Dois-je hériter de Conteneur ? Alors il n'est plus un ComposantUI "simple".
    // Dois-je dupliquer la logique de conteneur ? C'est la merde.
    private contenu: ComposantUI; // Bricolage.
}

L’héritage force une relation « EST-UN » rigide et unique. La vie réelle est faite de « A-UN », « PEUT-FAIRE », « SE-COMPOSE-DE ». Quand les besoins évoluent, l’arbre d’héritage se tord, craque. On hérite pour une fonctionnalité et on en hérite cinquante autres en prime. On finit avec des classes obèses, des hiérarchies profondes où un changement en haut fait trembler toute la base.

Interfaces incompatibles

Deux systèmes doivent collaborer. Ils ont été conçus séparément, par des équipes différentes, à des époques différentes. Leurs langages ne sont pas les mêmes.

// Le vieux système de logging (legacy)
class VieuxLogger {
    ecrireSurDisque(message: string, niveau: 'INFO' | 'ERREUR'): void {
        console.log(`[${niveau}] ${message} -> fichier.log`);
    }
}

// Le nouveau système, moderne, avec une interface standardisée.
interface NouveauSystemeDeLog {
    log(niveau: number, message: string): void; // Niveau numérique, méthode 'log'.
}

class MonService {
    // Conçu pour le nouveau système.
    constructor(private logger: NouveauSystemeDeLog) {}

    faireUnTruc() {
        this.logger.log(1, "Début de l'opération"); // Niveau 1 = INFO
    }
}

// COMMENT FAIRE COLLABORER MonService ET VieuxLogger ?
// Ils ne sont pas compatibles. Leurs signatures ne matchent pas.
// const service = new MonService(new VieuxLogger()); // ERREUR : Type mismatch.

C’est le mur de l’incompatibilité. Réécrire l’un ou l’autre est trop coûteux. Il faut un traducteur. Un adaptateur. Sans cela, les systèmes vivent en silos, la duplication de code explose, et l’intégration devient un champ de bataille.

Objets trop lourds

Parfois, un objet est conceptuellement simple mais coûte extrêmement cher en mémoire. Chaque instance porte un poids immense.

class TextureHD {
    // Imaginez 100 Mo de données pixels en mémoire ici.
    private pixels: Uint8Array = new Uint8Array(100_000_000); // ~100MB

    constructor(nomFichier: string) {
        console.log(`⚠️ CHARGEMENT LOURD de ${nomFichier} en mémoire`);
        // Chargement long depuis le disque/réseau.
    }

    afficher(x: number, y: number) {
        // Affiche la texture aux coordonnées données.
        console.log(`Affichage texture à (${x}, ${y})`);
    }
}

// Dans un jeu, des milliers d'arbres, d'herbes, de cailloux identiques...
const foret: TextureHD[] = [];
for (let i = 0; i < 1000; i++) {
    foret.push(new TextureHD('texture_arbre_identique.png')); // DÉSASTRE : 1000 * 100MB = 100GB !
}

La mémoire n’est pas infinie. Créer mille instances identiques d’un objet lourd, c’est un gaspillage criminel. La structure naïve est inefficace. Il faut un mécanisme pour partager l’état intrinsèque (ce qui est identique) tout en gardant la possibilité de personnaliser l’état extrinsèque (ce qui varie, comme la position).

Couplage excessif

C’est le mal rampant. Deux classes qui se connaissent trop intimement. L’une dépend des détails d’implémentation de l’autre. Un changement en entraîne dix.

class ServiceFacturation {
    // Couplage FORT à une implémentation concrète de base de données.
    private connexion: ConnexionMySQL;

    constructor() {
        this.connexion = new ConnexionMySQL('host', 3306, 'user', 'pass');
        this.connexion.etablirConnexion();
    }

    creerFacture(commande: Commande) {
        // Utilise des spécificités MySQL.
        const id = this.connexion.executerRequete(
            'INSERT INTO factures ...', 
            [commande.id, commande.total]
        );
        // Logique métier mélangée à des détails techniques.
    }
}

class ServiceRapports {
    // Du même couplage, ailleurs.
    private autreConnexion: ConnexionMySQL;
    // ...

    genererRapport() {
        // Utilise la MÊME table, avec sa propre connexion.
        // Changement de schéma de base ? Il faut modifier les deux services.
    }
}

Le couplage, c’est de la colle. Au début, ça tient. Puis ça durcit. Et un jour, vous ne pouvez plus bouger un composant sans casser les deux voisins. La structure devient un bloc de béton. Les tests unitaires sont impossibles (comment tester ServiceFacturation sans une vraie base MySQL ?). La réutilisation est un rêve.


Je fermai le dossier. Ces problèmes de structure n’étaient pas des bugs. C’étaient des pathologies architecturales. Elles ne se résolvaient pas en corrigeant une ligne, mais en repensant les relations entre les objets.

La famille des Design Patterns Structurels n’apportait pas de nouvelles fonctionnalités. Elle apportait des moyens de réorganiser. Des techniques pour assouplir les hiérarchies, faire dialoguer les incompatibles, alléger la mémoire, découpler les dépendances.

Le premier de ces moyens serait l’Adapter. L’artisan de l’interopérabilité. Celui qui prend un vieux connecteur rouillé et lui soude une prise universelle. Une solution simple, parfois sale, mais qui évite le bain de sang d’une réécriture complète.

La structure, c’était le squelette du système. Et un squelette mal aligné, ça finit par faire souffrir tout l’organisme.