Version sans JavaScript, en lecture seule : les ateliers, les quiz et l’exécution du code n’y sont pas. Version complète · Catalogue · Se connecter

Support de cours · chapitre 10

Les patrons de conception

Trois patrons à savoir reconnaître, onze autres présentés pour référence, et la question à se poser avant d'en utiliser un : quel problème faut-il résoudre ?

Ce chapitre prolonge les séquences 10 et 12.

Un patron de conception décrit un problème de conception qui revient souvent, les contraintes qui s'y opposent, une solution générique et ses inconvénients. Il ne s'agit donc pas d'un morceau de code à recopier.

Le vocabulaire vient de la Bande des Quatre (Gamma, Helm, Johnson, Vlissides, 1994), qui range les patrons en trois familles : création, structure, comportement.


Les trois patrons certifiés

Stratégie : un algorithme interchangeable

Le problème. Un comportement a plusieurs variantes, et l'on veut pouvoir en changer sans modifier le code qui l'utilise, parfois pendant l'exécution.

C'est le cas de VolStrategy, déjà vu aux chapitres 4 et 5.

interface VolStrategy { void voler(); }

class VolerAvecAiles implements VolStrategy {
    public void voler() { System.out.println("je vole"); }
}

class NePasVoler implements VolStrategy {
    public void voler() { System.out.println("je ne vole pas"); }
}

class Canard {
    private VolStrategy vol;
    Canard(VolStrategy vol) { this.vol = vol; }
    void effectuerVol()  { vol.voler(); }
    void equiper(VolStrategy v) { this.vol = v; }    // changement à chaud
}

Quand y penser. Un switch ou une suite de if sur un type ou un mode, répété à plusieurs endroits.

Fabrique : déporter la décision du type concret

Le problème. La classe à instancier dépend d'une donnée, et l'on ne veut pas que chaque appelant connaisse toutes les classes concrètes.

interface Paiement { void debiter(int centimes); }

class PaiementCarte  implements Paiement { public void debiter(int c) { } }
class PaiementPaypal implements Paiement { public void debiter(int c) { } }

class FabriquePaiement {
    static Paiement pour(String mode) {
        if ("carte".equals(mode))  return new PaiementCarte();
        if ("paypal".equals(mode)) return new PaiementPaypal();
        throw new IllegalArgumentException("mode inconnu : " + mode);
    }
}

Le switch existe toujours, mais il est regroupé en un seul endroit. Pour ajouter un moyen de paiement, il suffit de modifier la fabrique au lieu de reprendre chaque appelant.

Quand y penser. Le même new conditionnel recopié dans plusieurs classes.

Décorateur : ajouter une responsabilité à l'exécution

Le problème. On veut ajouter des fonctionnalités à un objet sans créer de sous-classe, et pouvoir combiner ces ajouts dans l'ordre voulu.

interface Document { byte[] octets(); }

class DocumentSimple implements Document {
    public byte[] octets() { return "coin".getBytes(); }
}

class DocumentCompresse implements Document {
    private final Document interne;
    DocumentCompresse(Document d) { this.interne = d; }
    public byte[] octets() { return compresser(interne.octets()); }
}

class DocumentChiffre implements Document {
    private final Document interne;
    DocumentChiffre(Document d) { this.interne = d; }
    public byte[] octets() { return chiffrer(interne.octets()); }
}

// L'ordre compte : on compresse, puis on chiffre.
Document d = new DocumentChiffre(new DocumentCompresse(new DocumentSimple()));

Avec l'héritage, il aurait fallu écrire DocumentCompresseEtChiffre, DocumentChiffreEtCompresse, et une nouvelle classe pour chaque combinaison. On retrouve l'argument des deux axes du chapitre 4, appliqué ici à l'ajout de fonctionnalités.

Quand y penser. Des sous-classes dont le nom contient un « et ».


Les autres patrons, pour référence

Ces patrons ne sont pas certifiés. Ils sont présentés pour que vous les reconnaissiez si vous les rencontrez, par exemple lors d'une revue de code.

Création, Singleton, Fabrique abstraite, Monteur

Singleton : une classe qui n'a qu'une instance, avec un accès contrôlé. En Java, on l'écrit avec une classe interne qui porte l'instance, ou avec une enum.

public final class Ressources {
    private Ressources() {}
    private static class Porteur { static final Ressources I = new Ressources(); }
    public static Ressources get() { return Porteur.I; }
}

Il vaut mieux l'éviter autant que possible : un singleton est en fait un état global, et il rend les tests difficiles. Passez plutôt l'objet en paramètre.

Fabrique abstraite : produire une famille d'objets qui vont ensemble (un thème clair, un thème sombre) sans exposer les classes concrètes.

Monteur (Builder) : construire étape par étape un objet qui a beaucoup de champs, au lieu d'appeler un constructeur à huit paramètres dont on oublie l'ordre.

Rapport r = new Rapport.Monteur()
        .titre("Bilan T3").auteur("Équipe BI").filigrane(true).monter();
Structure, Adaptateur, Façade, Composite

Adaptateur : rendre une bibliothèque compatible avec l'interface qu'attend votre code, sans modifier la bibliothèque.

interface VitesseKmH { double kmh(); }
class VoitureTierce { double mph() { return 120.0; } }

class AdaptateurVitesse implements VitesseKmH {
    private final VoitureTierce v;
    AdaptateurVitesse(VoitureTierce v) { this.v = v; }
    public double kmh() { return v.mph() * 1.60934; }
}

Façade : une méthode simple qui donne accès à un sous-système compliqué. Par exemple, rendreFacture(facture) se charge de la fusion du modèle, de l'incorporation des polices, de la signature et de la compression.

Composite : traiter un arbre et une feuille de la même façon. La taille d'un dossier est la somme des tailles de ses enfants, celle d'un fichier est sa propre taille, et la même méthode taille() sert pour les deux.

Comportement, Observateur, Commande, État, Patron de méthode

Observateur : prévenir des abonnés d'un changement, sans que l'émetteur ait à les connaître.

class BusEvenements {
    private final List<Abonne> abonnes = new ArrayList<>();
    void abonner(Abonne a) { abonnes.add(a); }
    void publier(Commande c) { for (Abonne a : abonnes) a.surCommande(c); }
}

Commande : représenter une action par un objet, pour pouvoir la mettre en file d'attente, la rejouer ou l'annuler.

État : remplacer un long switch sur un état interne par un objet pour chaque état, qui sait quoi faire et vers quel état passer.

Patron de méthode : la classe mère fixe le squelette d'un algorithme et laisse aux classes filles le soin d'en compléter certaines étapes. Ce patron repose sur l'héritage ; relisez le chapitre 4 avant de l'utiliser.


Choisir un patron

Question d'épreuve

On vous montre une classe avec un switch sur un mode de calcul, répété dans trois méthodes. Quel patron, et pourquoi ?

Il s'agit d'une Stratégie, car le comportement a des variantes interchangeables. Chaque branche du switch devient une implémentation d'une interface, la classe reçoit celle dont elle a besoin, et les trois répétitions disparaissent. Si c'était le choix de la classe à instancier qui était répété, et non le comportement, il faudrait une Fabrique.