SOLDES de printemps

Médiateur

Alias : Intermédiaire, Contrôleur, Mediator

Intention

Médiateur est un patron de conception comportemental qui diminue les dépendances chaotiques entre les objets. Il restreint les communications directes entre les objets et les force à collaborer uniquement via un objet médiateur.

Patron de conception médiateur

Problème

Prenons une boîte de dialogue qui crée et modifie des profils client. Elle comporte plusieurs contrôles de formulaires comme des champs de texte, des cases à cocher, des boutons, etc.

Liens chaotiques entre éléments de l’interface utilisateur

Les dépendances entre les éléments de l’interface utilisateur peuvent devenir chaotiques avec l’évolution de l’application.

Certains éléments du formulaire peuvent interagir. Par exemple, en cochant la case « J’ai un chien », vous allez révéler un champ de texte caché qui vous permet d’écrire le nom du chien. Vous pourriez également avoir un bouton Envoyer qui doit valider les valeurs de tous les champs avant de sauvegarder les données.

Les éléments de l’UI sont interdépendants

Les éléments peuvent avoir beaucoup de liens. De plus, la modification de certains éléments peut influer sur d’autres.

En écrivant directement la logique dans le code des éléments du formulaire, vous rendez les classes de ces éléments bien plus difficiles à réutiliser dans l’application. Par exemple, vous ne pourrez pas utiliser la classe de la case à cocher dans un autre formulaire, car elle est couplée avec le champ de texte du chien. Vous êtes par conséquent obligé d’utiliser toutes les classes du formulaire du profil si vous voulez vous servir de l’une d’entre elles.

Solution

Le patron de conception médiateur vous propose de mettre fin à toutes les communications directes entre les composants et de rendre ces derniers indépendants les uns des autres. À la place, ces composants collaborent indirectement en utilisant un objet spécial médiateur qui redirige les appels vers les composants appropriés. Ainsi, les composants ne reposent que sur une seule classe médiateur plutôt que d’être couplés à de nombreux collègues.

Dans notre exemple qui porte sur l’édition du formulaire d’un profil, la classe dialogue peut prendre le rôle du médiateur. Elle connait déjà probablement tous ses sous-éléments, vous n’avez donc pas besoin d’y ajouter des dépendances.

Les éléments de l’UI doivent communiquer avec le médiateur

Les éléments de l’UI doivent communiquer directement avec l’objet médiateur.

Le changement le plus important intervient sur les éléments du formulaire. Prenons le bouton Envoyer. Avant, chaque fois qu’un utilisateur appuyait sur ce bouton, ce dernier devait valider les valeurs des éléments individuels du formulaire. À présent, il se contente d’informer la classe dialogue lorsqu’un clic a lieu. Après avoir reçu cette notification, la classe dialogue effectue la validation ou délègue la tâche aux différents éléments. Le bouton n’est ainsi couplé qu’avec la classe dialogue, au lieu d’être relié à un paquet d’éléments.

Vous pouvez pousser le vice plus loin et diminuer encore plus cette dépendance en extrayant l’interface commune pour toutes ces boîtes de dialogue. L’interface devrait déclarer la méthode de notification que tous les éléments du formulaire utilisent pour prévenir la classe dialogue des événements qui les affectent. Notre bouton Envoyer devrait dorénavant pouvoir manipuler n’importe quelle boîte de dialogue qui implémente cette interface.

Ainsi, le patron de conception médiateur vous permet d’encapsuler une toile complexe de relations entre divers objets à l’intérieur d’un simple objet médiateur. Moins une classe a de dépendances et plus il est facile de la modifier, de l’étendre ou de la réutiliser.

Analogie

Tour de contrôle du trafic aérien

Les pilotes d’avion ne communiquent pas directement ensemble pour déterminer qui sera le prochain à atterrir. Toute communication passe par la tour de contrôle.

Les pilotes qui vont décoller ou atterrir sur la piste ne communiquent pas ensemble directement. Ils s’adressent à un contrôleur aérien qui est assis dans une grande tour près de la piste d’atterrissage. Sans lui, les pilotes seraient obligés de savoir si d’autres avions sont à proximité et décider des priorités d’atterrissage avec un ensemble de pilotes. Les accidents d’avion augmenteraient probablement beaucoup.

La tour n’a pas besoin de contrôler la totalité d’un vol. Elle n’est là que pour faire respecter des contraintes au départ et à l’arrivée, pour faire en sorte que les pilotes n’aient pas trop de paramètres à gérer.

Structure

Structure du patron de conception MédiateurStructure du patron de conception Médiateur
  1. Les classes Composant contiennent de la logique métier. Chaque composant possède une référence vers un médiateur, déclaré avec le type de l’interface médiateur. Le composant ne connait pas la classe du médiateur, vous pouvez ainsi réutiliser les mêmes composants dans d’autres programmes en les liant à un autre médiateur.

  2. L’interface Médiateur déclare des méthodes pour communiquer avec les composants et n’est généralement dotée que d’une seule méthode de notification. Les composants peuvent passer n’importe quel contexte en paramètre de cette méthode (ce qui inclut leurs propres objets), mais en évitant de provoquer un couplage entre un composant récepteur et la classe du demandeur.

  3. Les Médiateurs Concrets encapsulent les relations entre les divers composants. Les médiateurs concrets gardent souvent des références vers les composants qu’ils gèrent et s’occupent même parfois de leur cycle de vie.

  4. Les composants ne doivent pas avoir de visibilité sur les autres composants. S’il arrive quelque chose d’important à un composant, il doit seulement en prévenir le médiateur. Quand ce dernier reçoit la notification, il doit pouvoir facilement identifier le demandeur, ce qui peut suffire pour déterminer le composant qui doit être déclenché en retour.

    De son point de vue, le composant ne voit qu’une boîte noire. Le demandeur ne sait pas qui va se charger de sa demande, et le récepteur ignore qui l’a envoyée.

Pseudo-code

Dans cet exemple, le Médiateur vous aide à éliminer les dépendances mutuelles entre différentes classes de l’UI (boutons, cases à cocher et libellés de texte).

Structure de l’exemple utilisé pour le patron de conception médiateur

Structure des classes des boîtes de dialogue de l’UI.

Un élément déclenché par un utilisateur ne communique pas directement avec les autres éléments, même si c’est l’impression qui s’en dégage. À la place, l’élément n’a besoin que de prévenir son médiateur qu’un événement a eu lieu, en lui donnant les informations contextuelles avec la notification.

Dans cet exemple, la boîte de dialogue d’identification prend le rôle du médiateur. Elle sait comment les éléments concrets sont censés collaborer et facilite leur communication indirecte. Lorsque la boîte de dialogue reçoit une notification d’événement, elle sait quel élément est concerné et redirige l’appel en conséquence.

// L’interface médiateur déclare une méthode qui permet aux
// composants d’avertir le médiateur au sujet de divers
// événements. Le médiateur peut réagir à ces événements et
// passer les appels aux autres composants.
interface Mediator is
    method notify(sender: Component, event: string)


// La classe concrète Médiateur. Les interconnexions entre les
// composants individuels ont été démêlées et transférées dans
// le médiateur.
class AuthenticationDialog implements Mediator is
    private field title: string
    private field loginOrRegisterChkBx: Checkbox
    private field loginUsername, loginPassword: Textbox
    private field registrationUsername, registrationPassword,
                  registrationEmail: Textbox
    private field okBtn, cancelBtn: Button

    constructor AuthenticationDialog() is
        // Crée les composants et passe le médiateur actuel dans
        // leurs constructeurs pour établir les liens.

    // Le médiateur est averti si un composant est affecté par
    // quoi que ce soit. Lorsqu’un médiateur reçoit une
    // notification, il peut lancer un traitement ou envoyer la
    // demande à un autre composant.
    method notify(sender, event) is
        if (sender == loginOrRegisterChkBx and event == "check")
            if (loginOrRegisterChkBx.checked)
                title = "Log in"
                // 1. Affiche les composants du formulaire
                // d’identification.
                // 2. Cache les composants du formulaire
                // d’inscription.
            else
                title = "Register"
                // 1. Affiche les composants du formulaire
                // d’inscription.
                // 2. Cache les composants du formulaire
                // d’identification.

        if (sender == okBtn && event == "click")
            if (loginOrRegister.checked)
                // Essaye de trouver un utilisateur à l’aide de
                // ses identifiants.
                if (!found)
                    // Montre un message d’erreur au-dessus du
                    // champ identifiant.
            else
                // 1. Crée un compte utilisateur en utilisant
                // les données saisies dans les champs
                // d’inscription.
                // 2. Connecte cet utilisateur.
                // ...


// Les composants communiquent avec un médiateur en utilisant
// son interface. Grâce à cela, vous pouvez utiliser les mêmes
// composants dans d’autres contextes en les associant avec
// d’autres objets médiateur.
class Component is
    field dialog: Mediator

    constructor Component(dialog) is
        this.dialog = dialog

    method click() is
        dialog.notify(this, "click")

    method keypress() is
        dialog.notify(this, "keypress")

// Les composants concrets ne communiquent pas ensemble. Leur
// unique canal de communication leur sert à envoyer des
// notifications au médiateur.
class Button extends Component is
    // ...

class Textbox extends Component is
    // ...

class Checkbox extends Component is
    method check() is
        dialog.notify(this, "check")
    // ...

Possibilités d’application

Utilisez ce patron si vous rencontrez des difficultés pour modifier certaines classes trop fortement couplées avec d’autres.

Le médiateur vous permet d’extraire toutes les relations entre les classes dans une classe séparée, en isolant les modifications appliquées à un composant spécifique du reste des composants.

Utilisez ce patron quand vous ne pouvez pas réutiliser un composant ailleurs, car il est trop dépendant des autres composants.

Après avoir mis en place le médiateur, les composants individuels n’ont plus de visibilité sur les autres composants. Ils peuvent toujours communiquer ensemble mais indirectement, via un objet médiateur. Pour réutiliser un composant dans une application différente, vous n’avez besoin que de lui fournir une classe médiateur.

Utilisez le médiateur lorsque vous créez des tonnes de sous-classes pour les composants, juste pour pouvoir bénéficier de leur comportement de base dans différents contextes.

Toutes les relations entre composants se trouvent à l’intérieur d’un médiateur. De nouvelles classes médiateur peuvent donc facilement définir de nouveaux moyens de collaboration pour ces composants sans avoir à les modifier.

Mise en œuvre

  1. Identifiez des classes qui sont fortement couplées et qui pourraient bénéficier de plus d’indépendance (pour une maintenance plus aisée ou pour faciliter la réutilisation de ces classes).

  2. Déclarez l’interface médiateur et écrivez le protocole de communication voulu entre les médiateurs et les différents composants. Dans la plupart des cas, une seule méthode est suffisante pour recevoir les notifications des composants.

    Cette interface est cruciale si vous voulez pouvoir réutiliser les classes des composants dans différents contextes. Tant que le composant communique avec le médiateur en passant par l’interface générique, vous pouvez relier le composant avec une implémentation différente du médiateur.

  3. Implémentez la classe concrète du médiateur. Faites en sorte que cette classe garde des références vers tous les composants qu’elle gère. Elle en tirera de gros bénéfices.

  4. Vous pouvez encore aller plus loin en rendant le médiateur responsable de la création et de la destruction des objets composant. Le médiateur ressemblera ainsi à une fabrique ou à une façade.

  5. Les composants doivent avoir une référence vers l’objet médiateur. La connexion est souvent établie dans le constructeur du composant, dans lequel un objet médiateur est passé en paramètre.

  6. Modifiez le code des composants afin qu’ils appellent la méthode de notification du médiateur plutôt que des méthodes écrites dans d’autres composants. Mettez tout le code qui appelle les autres composants dans la classe médiateur, puis appelez-le lorsque le médiateur reçoit des notifications de ce composant.

Avantages et inconvénients

  • Principe de responsabilité unique. Vous pouvez mettre les communications entre les différents composants au même endroit, rendant le code plus facile à comprendre et à maintenir.
  • Principe ouvert/fermé. Vous pouvez ajouter de nouveaux médiateurs sans avoir à modifier les composants déjà en place.
  • Vous diminuez le couplage entre les différents composants d’un programme.
  • Vous pouvez réutiliser les composants individuels plus facilement.

Liens avec les autres patrons

  • La Chaîne de responsabilité, la Commande, le Médiateur et l’Observateur proposent différentes solutions pour associer les demandeurs et les récepteurs.

    • La chaîne de responsabilité envoie une demande ordonnée qui est passée tout au long d’une chaîne dynamique de récepteurs potentiels, jusqu’à ce que l’un d’entre eux décide de la traiter.
    • La commande établit des connexions unidirectionnelles entre les demandeurs et les récepteurs.
    • Le médiateur élimine les liens directs entre les demandeurs et les récepteurs, et les force à communiquer indirectement via un objet médiateur.
    • L’observateur permet aux récepteurs de s’inscrire et de se désinscrire dynamiquement à la réception des demandes.
  • La Façade et le Médiateur ont des rôles similaires : ils essayent de faire collaborer des classes étroitement liées.

    • La façade définit une interface simplifiée pour un sous-système d’objets, mais elle n’ajoute pas de nouvelles fonctionnalités. Le sous-système lui-même n’a pas connaissance de la façade. Les objets situés à l’intérieur du sous-système peuvent communiquer directement.
    • Le médiateur centralise la communication entre les composants du système. Les composants ne voient que l’objet médiateur et ne communiquent pas directement.
  • La différence entre le Médiateur et l’Observateur est souvent très fine. Dans la majorité des cas, vous pouvez implémenter l’un ou l’autre, mais parfois vous pouvez les utiliser simultanément. Regardons comment faire.

    Le but principal du médiateur est d’éliminer les dépendances mutuelles entre un ensemble de composants du système. À la place, ces composants peuvent devenir dépendants d’un unique objet médiateur. Le but de l’observateur est d’établir des connexions dynamiques à sens unique entre les objets, où certains objets peuvent être les subordonnés d’autres objets.

    Il existe une implémentation populaire du médiateur qui repose sur l’observateur. L’objet médiateur joue le rôle du diffuseur et les composants agissent comme des souscripteurs qui s’inscrivent et se désinscrivent des événements du médiateur. Lorsque ce type de conception est mis en place, le médiateur ressemble de près à l’observateur.

    Si vous êtes un peu perdu, rappelez-vous qu’il y a plusieurs manières d’implémenter le médiateur. Par exemple, vous pouvez associer de manière permanente tous les composants au même objet médiateur. Cette implémentation ne ressemblera pas à l’observateur, mais sera tout de même une instance du patron de conception médiateur.

    Maintenant, imaginez un programme dont tous les composants sont devenus des diffuseurs, permettant des connexions dynamiques les uns avec les autres. Nous n’aurons pas d’objet médiateur centralisé, seulement un ensemble d’observateurs distribués.

Exemples de code

Médiateur en C# Médiateur en C++ Médiateur en Go Médiateur en Java Médiateur en PHP Médiateur en Python Médiateur en Ruby Médiateur en Rust Médiateur en Swift Médiateur en TypeScript