Polar Code 🎭

Command Palette

Search for a command to run...

03
Pièce N°03

CHAPITRE 2 : PRINCIPES DE CONCEPTION (PRÉ-REQUIS IMPLICITES)

Le lendemain matin. La tasse de café tenait plus de l'anesthésiant que de la boisson. Le code de la veille restait là, accusateur. Avant de sortir les grands moyens — les Patterns —, il fallait comprendre le terrain. Les lois non-écrites. Les principes qui murmurent à l'oreille des architectures qui tiennent debout. Pas des règles, des tendances. La gravité du monde objet.

SOLID (vue synthétique)

Cinq lettres. Une formule de flic intègre dans un commissariat pourri. SOLID. Pas un dogme. Une check-list pour éviter que votre système ne finisse en garde à vue.

SRP – Responsabilité unique

Une classe, une raison de changer. Une seule.

// Le coupable d'hier
class Paiement {
    // ... fait du paiement, du mail, du SMS, du logging, de la persistance...
}

// Le suspect propre
class ProcesseurPaiement {
    traiter(montant: number) { /* UNE chose : le cœur du métier */ }
}

class Notificateur {
    envoyerConfirmation(transaction: Transaction) { /* UNE chose : notifier */ }
}

class Journaliseur {
    logTransaction(transaction: Transaction) { /* UNE chose : logger */ }
}

Chacun son rôle. Chacun son alibi. Quand le client demande un changement dans les notifications, un seul fichier transpire. Pas tout le quartier.

OCP – Ouvert/fermé

Ouvert à l'extension. Fermé à la modification. Le code propre ne se réécrit pas, il s'étend.

// AVANT : Fermé à l'extension, ouvert aux coups de marteau
class CalculateurTaxe {
    calculer(montant: number, pays: string) {
        if (pays === 'FR') return montant * 0.2;
        if (pays === 'US') return montant * 0.08;
        if (pays === 'DE') return montant * 0.19;
        // Un nouveau pays ? On fouille dans les entrailles.
        throw new Error('Pays non supporté');
    }
}

// APRÈS : On ajoute sans toucher au coeur
interface StrategieTaxe {
    calculer(montant: number): number;
}

class CalculateurTaxe {
    constructor(private strategie: StrategieTaxe) {}

    calculer(montant: number) {
        return this.strategie.calculer(montant); // Fermé. Stable.
    }
}

// Extensions ? On ajoute à la périphérie.
class TaxeFR implements StrategieTaxe { calculer(m) { return m * 0.2; } }
class TaxeUS implements StrategieTaxe { calculer(m) { return m * 0.08; } }
class TaxeDE implements StrategieTaxe { calculer(m) { return m * 0.19; } }
// Nouveau pays ? Nouvelle classe. L'ancien code dort tranquille.

On ne viole pas le code existant. On l'épouse avec de nouvelles implémentations.

LSP – Substitution de Liskov

Une sous-classe doit pouvoir remplacer sa parente sans tout casser. L'héritage, c'est un contrat, pas un alibi.

class Rectangle {
    constructor(public largeur: number, public hauteur: number) {}

    setLargeur(l: number) { this.largeur = l; }
    setHauteur(h: number) { this.hauteur = h; }

    aire() { return this.largeur * this.hauteur; }
}

class Carre extends Rectangle {
    constructor(cote: number) {
        super(cote, cote);
    }

    setLargeur(l: number) {
        super.setLargeur(l);
        super.setHauteur(l); // Violation ! Un Carré brise le contrat de Rectangle.
    }

    setHauteur(h: number) {
        super.setHauteur(h);
        super.setLargeur(h); // Le système qui compte sur Rectangle va saigner.
    }
}

// Fonction innocente qui croit manipuler un Rectangle
function redimensionner(rect: Rectangle, nouvelleLargeur: number) {
    rect.setLargeur(nouvelleLargeur);
    // Sous-entendu : la hauteur reste inchangée.
    // Avec un Carré, tout est foutu.
}

Si ça ressemble à un canard, cancane comme un canard, mais fait planter le système… C'est pas un canard. C'est une violation LSP.

ISP – Ségrégation des interfaces

Ne forcez pas vos clients à dépendre de méthodes qu'ils n'utilisent pas. Des interfaces maigres, spécifiques.

// L'interface obèse qui sent le desperado
interface MachineToutEnUn {
    imprimer(document: Document): void;
    scanner(document: Document): void;
    faxer(document: Document): void;
}

class VieilleImprimante implements MachineToutEnUn {
    imprimer() { /* OK */ }
    scanner() { throw new Error('Pas implémenté'); } // Le client trébuche
    faxer() { throw new Error('Pas implémenté'); } // Sur du vide.
}

// Des interfaces fines, précises. Comme des couteaux.
interface Imprimante {
    imprimer(document: Document): void;
}

interface Scanner {
    scanner(document: Document): void;
}

interface Fax {
    faxer(document: Document): void;
}

class ImprimanteBasique implements Imprimante {
    imprimer() { /* Fait son job, rien d'autre */ }
}
// Le client ne dépend que de ce dont il a besoin.

On ne porte pas un flingue si on va juste chercher le pain.

DIP – Inversion des dépendances

Dépendre des abstractions, pas des concretions. C'est le principe qui retourne tout.

// AVANT : Dépendance directe, couplage fort.
class Paiement {
    private notifieur = new EmailService(); // Dépend d'un détail concret.

    traiter() {
        // ...
        this.notifieur.envoyerEmail(); // Lié à l'implémentation.
    }
}

// APRÈS : L'inversion. On regarde vers le haut d'abstraction.
interface Notifieur {
    notifier(utilisateur: Utilisateur, message: string): void;
}

class Paiement {
    // Dépend d'une abstraction. Peu importe qui l'implémente.
    constructor(private notifieur: Notifieur) {}

    traiter() {
        // ...
        this.notifieur.notifier(utilisateur, "Paiement accepté");
    }
}

// Les détails viennent après, en périphérie.
class EmailService implements Notifieur { notifier() { /* ... */ } }
class SmsService implements Notifieur { notifier() { /* ... */ } }
class ServiceVocale implements Notifieur { notifier() { /* ... */ } }

Le module de haut niveau (Paiement) ne plie pas devant les détails de bas niveau (EmailService). Ce sont les détails qui s'adaptent à l'abstraction. Le monde à l'envers. Le seul qui tienne debout.

Composition vs Héritage

"Préférez la composition à l'héritage." Le mantra. L'héritage lie du sang. La composition, du business.

// Héritage : Relation rigide "EST-UN"
class Canard {
    voler() { console.log('Je vole !'); }
    cancanner() { console.log('Coin coin !'); }
}

class CanardEnPlastique extends Canard {
    voler() { throw new Error('Je ne vole pas !'); } // On brise le contrat.
}

// Composition : Relation flexible "A-UN"
interface ComportementVol {
    voler(): void;
}

interface ComportementCancan {
    cancanner(): void;
}

class VraiVol implements ComportementVol { voler() { console.log('Je vole !'); } }
class NePasVoler implements ComportementVol { voler() { /* Rien */ } }

class CancanNormal implements ComportementCancan { cancanner() { console.log('Coin coin !'); } }
class Couic implements ComportementCancan { cancanner() { console.log('Couic !'); } }

class Canard {
    constructor(
        private comportementVol: ComportementVol,
        private comportementCancan: ComportementCancan
    ) {}

    effectuerVol() { this.comportementVol.voler(); }
    effectuerCancan() { this.comportementCancan.cancanner(); }

    // On peut changer les comportements à la volée. Dynamicité.
    setComportementVol(nvComportement: ComportementVol) {
        this.comportementVol = nvComportement;
    }
}

const canardSauvage = new Canard(new VraiVol(), new CancanNormal());
const leurre = new Canard(new NePasVoler(), new Couic());

L'héritage verrouille à la compilation. La composition négocie à l'exécution.

Programmation orientée interface

Coder contre des contrats, pas contre des implémentations. L'interface est la loi. Les classes sont ses citoyens — ou ses hors-la-loi.

// On parle à l'interface, pas à l'objet.
interface Repository<T> {
    trouverParId(id: string): Promise<T | null>;
    sauvegarder(entite: T): Promise<void>;
}

class ServiceUtilisateur {
    // Peu importe si c'est PostgreSQL, MongoDB, ou un fichier JSON.
    constructor(private repo: Repository<Utilisateur>) {}

    async getUtilisateur(id: string) {
        return this.repo.trouverParId(id); // La loi est respectée.
    }
}

Encapsulation des variations

Identifiez ce qui change, et enfermez-le. Isolez l'instable du stable.

// La variation (l'algorithme de tri) est encapsulée.
interface StrategieTri {
    trier(donnees: number[]): number[];
}

class TriRapide implements StrategieTri { trier(donnees) { /* ... */ } }
class TriFusion implements StrategieTri { trier(donnees) { /* ... */ } }
class TriBulles implements StrategieTri { trier(donnees) { /* ... */ } }

class ContexteTri {
    constructor(private strategie: StrategieTri) {}

    executerTri(donnees: number[]) {
        return this.strategie.trier(donnees); // La variation est cachée ici.
    }
}

Séparation des responsabilités

Une dernière fois. Parce que tout part de là, et que tout y revient.

  • Qui fait quoi ?
  • Qui sait quoi ?
  • Qui décide de quoi ?

Ne mélangez pas les rôles. Dans un polar, le flic n'est pas le juge. Le témoin n'est pas le bourreau. Dans le code, c'est pareil.


Je posai ma tasse. Vide.

Ces principes, ce n'était pas la solution. C'était la grammaire. L'alphabet. Sans ça, les Design Patterns ne seraient que des incantations vides, du jargon pour épater en réunion.

Mais avec ça… Avec ça, on pouvait commencer à parler le vrai langage. Celui qui structure l'ombre et contient le chaos.

Le chapitre 1 montrait la maladie. Le chapitre 2 donnait l'hygiène de vie.

Le chapitre 3 apporterait les remèdes. Les Patterns. Mais pour l'heure, il fallait digérer ces pré-requis. Ces lois du monde objet qui, quand on les ignore, vous laissent avec un cadavre dans le code et les mains pleines de dette technique.

La pièce était silencieuse. Mais dans ma tête, les principes commençaient à s'agencer, à former des patterns familiers, annonçant la suite du plan…