Pattern MMVC, ma vision de la synchro de données

Flattr this!

Ce matin, j'annonçais sur Twitter avoir mis en place un projet NodeJS basé sur une architecture/pattern MMVC. Suite aux questions reçues, il semble nécessaire d'expliquer ce que c'est. Avantages et inconvénients.

MMVC ?

Pour Model - ModelMapper - View - Controller. C'est un dérivé du design pattern classique MVC adapté au travail avec les bases de données.

Reprocher des choses au MVC

Le modèle classique du MVC part d'un bon fondement : déléguer les responsabilités à des éléments séparés. Les données d'un côté, l'affichage d'un autre et un élément central pour faire le lien.

Le problème de ce pattern, c'est que la gestion des données implique aussi bien :

  • Les fonctionnalités métier. Par exemple, peindre ma voiture veut dire que je change la valeur de la propriété couleur de l'objet voiture ;
  • La synchronisation avec la source de données. Par exemple, récupérer une instance voiture particulière en base ou charger toute une collection d'instances (tout un parking).

MMVC, une solution au problème

La logique du MVC est de déléguer une responsabilité par couche. Pourtant ici la couche Model hérite de deux responsabilités bien distinctes. La solution que je préfère, et celle que je vais présenter ici, est simplement d'ajouter une quatrième couche pour gérer la synchronisation des données avec la source. C'est l'arrivée du ModelMapper.

Dans les faits ?

C'est assez simple en fait. Admettons un modèle de voiture (allégé) sous MVC classique :

class Voiture {
    private $immatriculation = null;
    private $couleur = null;

    public __construct($immatriculation) {
        $this->immatriculation = $immatriculation;
    }

    public setImmatriculation ($nouvelleImmatriculation) {
        $this->immatriculation = $nouvelleImmatriculation;
    }

    public getImmatriculation () {
        return $this->immatriculation;
    }

    public setCouleur ($nouvelleCouleur) {
        $this->couleur = $nouvelleCouleur;
    }

    public getCouleur () {
        return $this->couleur;
    }

    public findOne() {
        $sql = 'SELECT couleur FROM voitures WHERE immatriculation = "' . $this->immatriculation . '";';
        // traitement de votre requête
        // renvoi dans un tableau associatif $res
        $this->couleur = $res['couleur'];
    }

    static findAll() {
        $sql = 'SELECT immatriculation, couleur FROM voitures;';
        // traitement de votre requête
        // renvoi dans un tableau associatif $res
        $resultat = array();
        foreach ($res => $aVoiture) {
                $voiture = new Voiture($aVoiture['immatriculation']);
                $voiture->setCOuleur($aVoiture['couleur']);
                $resultat[] = $voiture;
        }
        return $resultat;
    }
}

/**
 * Utilisation
 */
$voiture = new Voiture('toto');
$voiture->findOne();
echo $voiture->getImmatriculation() . ' -> ' . $voiture->getCouleur(); // toto -> rouge

$listeVoitures = Voiture::findAll();
foreach ($listeVoitures as $voiture) {
    echo $voiture->getImmatriculation() . ' -> ' . $voiture->getCouleur();
}
// toto -> rouge
// tata -> verte
// titi -> bleue

Il y a pas grand chose dans cette classe, 70% de vos modèles contiendront à l'aise dix fois  (estimations parfaitement arbitraires et assumées) plus de lignes de code de plus que celle là et pourtant, c'est déjà le bordel.

On a de la gestion métier (accesseurs/mutateurs), on a une fonction qui permet d'alimenter l'objet. Et on a une statique qui renvoie un tableau d'instances de la classe.

La petite blague, au passage, c'est qu'ici, on a deux façons d'accéder à l'immatriculation. Soit via l'accesseur, soit via la propriété. Et ça c'est dangereux. Admettons qu'on ait mis des contrôles, des opérations diverses et variées dans getImmatriculation(). On va tous les court-circuiter en accédant directement à la propriété. Ça ira plus vite mais si ces traitements ont été mis sur l'accesseur, c'est qu'il y a une raison : ils doivent être faits pour tout accès à la donnée.

Et sous MMVC ?

On divise ça en deux classes :

class Voiture {
    private $immatriculation = null;
    private $couleur = null;

    public __construct($immatriculation) {
        $this->immatriculation = $immatriculation;
    }

    public setImmatriculation ($nouvelleImmatriculation) {
        $this->immatriculation = $nouvelleImmatriculation;
    }

    public getImmatriculation () {
        return $this->immatriculation;
    }

    public setCouleur ($nouvelleCouleur) {
        $this->couleur = $nouvelleCouleur;
    }

    public getCouleur () {
        return $this->couleur;
    }
}

class VoitureMapper {
    public findOne(Voiture $voiture) {
        $sql = 'SELECT couleur FROM voitures WHERE immatriculation = "' . $voiture->getImmatriculation() . '";';
        // traitement de votre requête
        // renvoi dans un tableau associatif $res
        $voiture->setCouleur($res['couleur']);
    }

    public findAll() {
        $sql = 'SELECT immatriculation, couleur FROM voitures;';
        // traitement de votre requête
        // renvoi dans un tableau associatif $res
        $resultat = array();
        foreach ($res => $aVoiture) {
                $voiture = new Voiture($aVoiture['immatriculation']);
                $voiture->setCOuleur($aVoiture['couleur']);
                $resultat[] = $voiture;
        }
        return $resultat;
    }
}

/**
 * Utilisation
 */
$voiture = new Voiture('toto');
$voitureMap = new VoitureMapper();
$voitureMap->findOne($voiture);
echo $voiture->getImmatriculation() . ' -> ' . $voiture->getCouleur(); // toto -> rouge

$listeVoitures = $voitureMap->findAll();
foreach ($listeVoitures as $voiture) {
    echo $voiture->getImmatriculation() . ' -> ' . $voiture->getCouleur();
}
// toto -> rouge
// tata -> verte
// titi -> bleue

C'est un poil plus long à coder mais c'est plus clair.

Ici la voiture se gère elle-même et se limite à cette responsabilité. Quand à VoitureMapper, elle n'a pas à gérer la structure interne de Voiture pour pouvoir la synchroniser avec la base de données.

Pour revenir sur le point de l'accès à l'immatriculation, ici, vu que la donnée est privée, VoitureMapper n'y a pas accès directement. On est obligés par le code même de passer par l'accesseur.

Vous gagnez en isolation des responsabilités et en qualité de code. Et vous n'aurez plus besoin de râler auprès de vos collègues parce qu'il y en a encore un qui a fait le fainéant et n'a pas été cherché l'accesseur.

Qui l'utilise ?

Vous allez me demander si je suis pas un peu tout seul à me prendre la tête à ce point là. Et bien non, je ne l'ai pas appris en cours ce pattern moi non plus.

Je l'ai découvert en utilisant le project-builder du Zend Framework 1.11. Il génère une architecture basée sur ce pattern. Je l'ai de suite adopté.

De ce que j'ai compris, Spring, un framework Java "un peu" connu, s'en sert aussi.

Conclusion

Vous devez écrire un peu plus de code que dans le MVC, et il y a deux fois plus de classes à manipuler.

Côté testabilité, vous pouvez cependant séparer du coup vos tests de la partie métier de ceux de la partie synchronisation, vu que ce sont désormais deux classes séparées.

Vous pouvez abstraire plus facilement votre métier ainsi que votre synchronisation sans que ça devienne une usine à gaz dans les couches abstraites.

Et enfin grand avantage à mes yeux : la séparation des responsabilités est vraiment respectée.

Il y a quand même un truc foireux dans cet article

Je vous lance sur le sujet à propos d'un projet NodeJS, donc codé en JavaScript et je vous colle des exemples en PHP.

C'est tout simplement parce que c'était plus rapide pour moi de vous écrire le code PHP simplifié comme ça sur le coude plutôt que de reprendre mon code JS et d'essayer de le nettoyer. Ceci dit, je vais créer un projet GitHub où je vous montrerai comment je fais en JS. Bien entendu, quand ça sera fait, je vous le signalerai sur ce blog.

Flattr this!

A propos de Mathieu

Ingénieur développeur web dans la vente par correspondance B2B, adepte de nouvelles technologies et d'innovation. Vous pouvez aussi me retrouver sur Twitter @mathrobin
Cette entrée a été publiée dans Non classé, avec comme mot(s)-clef(s) , , , , , , , , . Vous pouvez la mettre en favoris avec ce permalien.
  • Maxime

    Propel utilise ce pattern MMVC de part son utilisation de classe Model et de classe Query/Peer pour la récupération en base des Model.

    En fait j’en faisais déjà sans m’en rendre compte, good to know.

    • http://www.mathieurobin.com/ Mathieu

      Ah je ne savais pas non plus tiens. Pourtant j’avais eu le droit à toute une conférence de démo par son créateur. Merci de l’info !

  • MaitrePylos

    manque un type dans findOne


    public findOne(Voiture $voiture) {
    $sql = 'SELECT couleur FROM voitures WHERE immatriculation = "' . $voiture->getImmatriculation() . '";';
    // traitement de votre requête
    // renvoi dans un tableau associatif $res
    $voiture->setCouleur($res['couleur']);
    }

    • http://www.mathieurobin.com/ Mathieu

      C’est très juste. Merci de la correction :-) J’editerai l’article en conséquence dès que je serai devant un écran plus grand que mon téléphone 😉

  • http://neolao.com neolao

    Je ne savais pas que des gens construisent des instances de cette façon.
    Le MVC ne définit pas du tout comment ça se passe dans le « M ». Dans le cas du MMVC, le ModelMapper ressemble un peu à un DAO (Data Access Object) et l’objet Voiture devient un VO (Value Object) voire un BO (Business Object).

    Ce que je trouve curieux, c’est de créer une instance de Voiture avant de demander à VoitureMapper de le remplir. Pourquoi ne pas laisser VoitureMapper créer l’instance, comme pour la méthode findAll ? Par exemple :
    $voiture = $voitureDao->getByImmatriculation(‘toto’);

    Comme ça, la responsabilité de l’instanciation est entièrement donné au DAO.

    En fait, je ne vois pas pourquoi un nouveau sigle MMVC est « inventé », c’est de la cuisine dans le « M » du MVC. On va utiliser d’autres pattern à l’intérieur du « M ».

    • http://www.mathieurobin.com/ Mathieu

      Effectivement, vu les commentaires qui ont suivi, on peut considérer que c’est de la cuisine interne au Modèle. Merci pour tous les sigles (et les infos liées bien sûr).

    • http://www.mathieurobin.com/ Mathieu

      Ah et en fait, passer l’instance vide permet en fait de pouvoir lui assigner plutôt une interface ou une classe abstraite en paramètre qu’une classe précise. Ainsi on découple un peu l’application. Ce qui peut être pratique 😉

      • http://neolao.com neolao

        Ce qui compte, ce n’est pas de découpler à tout prix, il faut découpler par responsabilité.

        Si effectivement tu peux avoir différents objets de la même famille, tu en retrouves un par son identifiant (sinon ça ne serait pas la même méthode), ensuite le DAO sait quelle class utiliser. Au final, tu manipules des instances et leurs créations sont délégués.

        Dans ton exemple, est-ce que tu dois savoir qu’il faut instancier une voiture Renault avant de le remplir ?

        • http://www.mathieurobin.com/ Mathieu

          Je pensais à une initialisation par niveaux dans le cas du découplage. En fait, admettons que tu aies un mapper de véhicule, un mapper de moto et un mapper de voiture. Et une seule table où tu stockés ce beau monde. Avec des colonnes spécifiques en fonction si c’est moto ou voiture (pourquoi pas?).
          Tu peux alors décider de passer ton instance en paramètre de tes fonctions de mappers pour permettre de les appeler ensuite via leurs filles. D’ailleurs, j’ai oublié de porter la même logique à mon findAll, en passant le nom de la classe à instancier.

          • http://neolao.com neolao

            ok, donc tu parles de la cuisine qui se fait pour récupérer les données (à l’intérieur du DAO). Moi je parlais de ce que tu exposes (comme méthode publique de DAO) pour avoir ton instance finale.

            Bref, de toute façon ça serait trop long d’en discuter et sans exemple plus réel, c’est difficile d’argumenter.

  • Jérémy

    Bonjour,
    Ce que vous appelez MMVC, c’est le design pattern DataMapper. Je vous invite à vous renseigner sur le sujet, notamment au travers de doctrine 2, qui base son implémentation dessus.

    • http://www.mathieurobin.com/ Mathieu

      J’avais eu une démo de Doctrine 2 justement, pendant une soirée sur les ORM organisée par l’AFUP mais c’est pas ce que j’avais pas compris. Je vais me repencher sur le sujet.

  • thesorrow

    Je pense que tu devrais lire Martin Fowler plutôt que d’inventer des nom de pattern, on en a déjà assez comme cela :)

    • http://www.mathieurobin.com/ Mathieu

      Je n’ai pas eu la prétention de dire que j’avais inventé un pattern, le paragraphe intitulé « Qui l’utilise ? » était censé expliquer que cette idée ne venait non seulement pas de moi mais qu’en plus ça avait l’air plus qu’utilisé. Après effectivement, il semblerait que c’est une partie non négligeable, si ce n’est tout le DataMapper, et dans ce cas là, j’ai appris plus qu’un truc.
      Les livres de Martin Fowler et du Gang Of Four sont au planning mais je n’ai jamais eu le temps de m’y pencher malheureusement. Ton commentaire m’encourage à les remonter dans les priorités 😉 Merci !

  • http://jelix.org Laurentj

    La couche ORM de Jelix utilise aussi un principe similaire. C’est en fait issue du pattern DAO (Data Access Object). On a d’un coté un factory qui permet, à partir d’une base de donnée, de récupérer un ou plusieurs objets, et elle permet aussi de sauvegarder ou supprimer les objets. Et les objets en question, ce sont les objets métiers qui ne contiennent que les propriétés.

    Bon ben Jelix est aussi MMVC :-)

    • http://www.mathieurobin.com/ Mathieu

      Apparemment, cela correspondrait au DataMapper, pattern célèbre même si je ne l’ai que mal et peu étudié visiblement. Donc rien de surprenant à ce qu’il soit aussi repris chez Jelix 😉 Je vais mettre à jour la liste des frameworks l’utilisant. Merci de l’info !

  • http://totalement.geek.oupas.fr Renaud

    Doctrine 1 & 2 aussi avec Model/ModelTable pour Doctrine 1 et Entity/Repository pour Doctrine 2.

    En fait, je pense que n’importe quel ORM de base utilise ce bout de pattern MM et permet de faire du MMVC.

    • http://www.mathieurobin.com/ Mathieu

      C’est ce qu’il semblerait en effet. Merci de confirmer l’info toi aussi !

  • silver account

    Notez enfin que chaque classe est isolée, c’est à dire qu’il n’existe pas de code logique qui permette une quelconque interaction entre elles.

    • http://www.mathieurobin.com/ Mathieu

      Oui et non justement. Effectivement, il y a un isolement très important mais pas une étanchéité totale.

  • nbl_

    Bonjour,

    Les classes ne sont pas isolées car elle sont associées par le passage en paramètre de la classe Voiture vers la classe VoitureMapper par la fonction findOne().

    Cordialement,

    nbl_

Articles liés