Licence Informatique 1 - GTK : conception Modèle-Vue-Contrôleur


Table of Contents

1 Modèle-Vue-Contrôleur

Le Modèle Vue Contrôleur (MVC) est une architecture et une méthode de conception pour le développement d'applications logicielles qui sépare le modèle de données, l'interface utilisateur et la logique de contrôle. Cette méthode a été mise au point en 1979 par Trygve Reenskaug, qui travaillait alors sur Smalltalk dans les laboratoires de recherche Xerox PARC.

Ce modèle d'architecture impose la séparation entre les données, les traitements et la présentation, ce qui donne trois parties fondamentales dans l'application finale : le modèle, la vue et le contrôleur.

  • Le Modèle représente le comportement de l'application : traitements des données, interactions avec la base de données, etc. Il décrit les données manipulées par l'application et définit les méthodes d'accès.
  • la Vue correspond à l'interface avec laquelle l'utilisateur interagit. Les résultats renvoyés par le modèle sont dénués de toute présentation mais sont présentés par les vues. Plusieurs vues peuvent afficher les informations d'un même modèle. Elle peut être conçue en html, ou tout autre " langage " de présentation. La vue n'effectue aucun traitement, elle se contente d'afficher les résultats des traitements effectués par le modèle, et de permettre à l'utilisateur d'interagir avec elles.
  • le Contrôleur prend en charge la gestion des évènements de synchronisation pour mettre à jour la vue ou le modèle. Il n'effectue aucun traitement, ne modifie aucune donnée, il analyse la requête du client et se contente de demander au modèle d'appliquer la requête et de demander à la vue d'afficher le modèle modifié.

En résumé, lorsqu'un client envoie une requête à l'application, celle-ci est analysée par le contrôleur, qui demande au modèle d'effectuer les traitements appropriés, puis renvoie la vue adaptée.

Un avantage apporté par cette architecture est sa clarté. Cela simplifie la tâche du développeur qui tenterait d'effectuer une maintenance ou une amélioration sur le projet. En effet, la modification des traitements ne change en rien la vue. Par exemple, on peut changer les règles d'un jeu de dames pour en faire un jeu de morpion en changeant simplement les traitements dans le modèle, et les vues ne s'en trouvent pas affectées.

On peut aussi facilement proposer plusieurs types de vues pour un même modèle :

  • adaptation aux possibilités de la machine : mode console / mode graphique
  • travail collaboratif ou jeu en réseau : chaque participant a sa propre vue d'un même modèle
  • gestion des préférences de l'utilisateur (par exemple, dans un navigateur de fichiers, affichage des fichiers en liste, liste détaillée, ou icônes)

1.1 Architecture Modèle/Vue/Contrôleur

L'architecture Modèle/Vue/Contrôleur (MVC) est une façon d'organiser un programme. Elle consiste à distinguer trois entités distinctes qui sont le modèle, la vue et le contrôleur ayant chacun un rôle précis dans le programme.

L'organisation globale d'une interface graphique est souvent délicate. Bien que la façon MVC d'organiser une interface ne soit pas la solution miracle, elle fournit souvent une première approche qui peut ensuite être adaptée. Elle offre aussi un cadre pour structurer une application.

Dans l'architecture MVC, les rôles des trois entités sont les suivants~:

  • modèle : données (accès et mise à jour)
  • vue : interface utilisateur (entrées et sorties)
  • contrôleur : gestion des évènements et synchronisation

1.1.1 Rôle du modèle

Le modèle contient les données manipulées par le programme. Il assure la gestion de ces données et garantit leur intégrité. Dans le cas typique d'une base de données, c'est le modèle qui la contient.

Le modèle offre des méthodes pour mettre à jour ces données (insertion, suppression, changement de valeur). Il offre aussi des méthodes pour consulter ces données. Dans le cas de données volumineuses, le modèle peut faciliter des vues partielles. Si, par exemple, le programme manipule une base de données pour les emplois du temps, le modèle peut fournir des méthodes pour récupérer tous les cours d'une salle, tous les cours d'une personne ou tous les cours d'un groupe de TD…

1.1.2 Rôle de la vue

La vue fait l'interface avec l'utilisateur. Sa première tâche est d'afficher les données qu'elle a récupérées auprès du modèle. Sa seconde tâche est de recevoir toutes les actions de l'utilisateur (clic de souris, sélection d'une entrées, boutons, …). Ces différents évènements sont ensuite envoyés au contrôleur.

La vue peut aussi donner plusieurs vues, partielles ou non, des mêmes données. Par exemple, l'application baobab de gestion de l'espace disque (sous Linux) affiche une liste de répertoires dans la partie gauche de sa fenêtre et une représentation graphique des mêmes répertoires dans sa partie droite. Cette représentation peut au choix être une vue "camembert" ou une vue sous forme de rectangles imbriqués dont les surfaces sont proportionnelles aux tailles des répertoires représentés.

1.1.3 Rôle du contrôleur

Le contrôleur est chargé de la synchronisation du modèle et de la vue. Il reçoit tous les évènements de l'utilisateur et enclenche les actions à effectuer. Si une action nécessite un changement des données, le contrôleur demande la modification des données au modèle et ensuite avertit la vue que les données ont changé pour que celle-ci se mette à jour. Certains événements de l'utilisateur ne concernent pas les données mais la vue. Dans ce cas, le contrôleur demande à la vue de se modifier.

Dans le cas d'une base de données des emplois du temps, une action de l'utilisateur peut être l'entrée (saisie) d'un nouveau cours. Le contrôleur ajoute ce cours au modèle et demande sa prise en compte par la vue. Une action de l'utilisateur peut aussi être de sélectionner une nouvelle personne pour visualiser tous ses cours. Ceci ne modifie pas la base des cours mais nécessite simplement que la vue s'adapte et offre à l'utilisateur une vision des cours de cette personne.


Source : http://www.techno-science.net/?onglet=glossaire&definition=5331


2 Modélisation

2.1 Problématique

Le but est de modéliser un concept du monde réel pour le représenter sous format informatique. Cette modélisation peut être initalement motivée par un but d'utilisation précis. Il faut toujours penser : que ce but peut changer, évoluer, s'enrichir ; et donc, s'en rendre le plus indépendant possible. Prenons l'exemple du concept "nombres complexes". Un nombre complexe est la donnée d'une partie réelle et d'une partie imaginaire. On peut pour le représenter utiliser la structure "struct s_complexe" suivante :

struct s_complexe 
{
  float reel;
  float img;
};

typedef struct s_complexe complexe;     

Pour manipuler les nombres complexes, on doit pouvoir (liste non exhaustive) : lire (consulter) la partie réelle / la partie imaginaire d'un nombre complexe, écrire (affecter) la partie réelle / la partie imaginaire d'un nombre complexe, déterminer le module / l'argument / le conjugué d'un nombre complexe, sommer, soustraire, multiplier, diviser deux nombres complexes entre eux, initialiser à 0 un nombre complexe ; soit, on doit disposer de la liste suivante de fonctions :

float complexe_get_reel(complexe z); 
float complexe_get_img(complexe z);
complexe complexe_set_reel(complexe z, float reel);
complexe complexe_set_img(complexe z, float img);
complexe complexe_set_zero(complexe z);
float complexe_get_module(complexe z);
float complexe_get_argument(complexe z);
complexe complexe_get_conjugue(complexe z);
complexe complexe_get_somme(complexe z1, complexe z2);
complexe complexe_get_difference(complexe z1, complexe z2);
complexe complexe_get_produit(complexe z1, complexe z2);
complexe complexe_get_division(complexe z1, complexe z2);     

On donne l'exemple de la définition de ces fonctions, pour quelques fonctions seulement :

float complexe_get_reel(complexe z) 
{
  return z.reel;
}

complexe complexe_set_img(complexe z, float img)
{
  z.img = img;
  return z;
}

complexe complexe_set_zero(complexe)
{
  z.reel = 0;
  z.img = 0;
  return z;
}

float complexe_get_module(complexe z)
{
  float module;
  module = sqrt( pow(z.reel, 2) + pow(z.img, 2) );
  return z;
}

complexe complexe_get_difference(complexe z1, complexe z2)
{
  complexe z;
  z.reel = z1.reel -z2.reel;
  z.img = z1.img -z2.img;
  return z;
}

2.2 Trois bonne raisons de définir l'interface d'une structure de donnée

  • Pour permettre et faciliter la manipulation de la structure par des programmes tierces. Concernant les nombres complexes : on s'imagine que dès lors que l'on manipule des nombres (complexes ou autres), on peut vouloir les sommer ; il semble alors naturel de disposer d'une fonction "somme".
  • Pour contrôler l'accès au données et en maintenir la cohérence. Par exemple, si l'on manipule un type de donnée "personne" implémenté par une structure "struct s_personne" qui contient un champ "date de naissance" et un champ "âge", et si l'on définit l'accesseur en écriture "personne_set_naissance", alors cette fonction ne doit pas seulement mettre à jour le champ "date de naissance", mais aussi le champ "âge" de la variable du type "personne" passée en paramètre (notons que ce n'est pas forcément une bonne idée que de manipuler un champ "âge", mais passons).
  • Pour se rendre le plus possible indépendant de l'implémentation. Concernant les nombres complexes, la structure utiliséee et la définition des fonctions de manipulation peuvent changer, sans pour autant que cela impacte les programmes qui utilisent la bibliothèque "complexe". Voyons ce qui se passe si l'on remplace la structure s_complexe par la structure s_complexe_2 suivante :
    struct s_complexe_2
    {
      float module;
      float argument;
    };
    
    typedef struct s_complexe_2 complexe;  
    

    Et considérons alors les deux programmes suivants (où le fichier "complexe.h" regroupe la spécifiation du type "complexe" et la déclaration des fonctions de manipulation des nombres complexes) :

    /* programme_1  */
    #include<stdlib.h> /* pour la constante symbolique EXIT_SUCCESS */
    #include<stdio.h>  /* pour la fonction printf */
    #include"complexe.h" /* pour manipuler les nombres complexes */
    
    int main()
    {
      complexe z;
      z.reel=0;
      z.img=0;
      printf("Le zero complexe est %f + %f i", z.reel, z.img);
      return EXIT_SUCCESS;
    };
    
    /* programme_2 */
    #include<stdlib.h> /* pour la constante symbolique EXIT_SUCCESS */
    #include<stdio.h>  /* pour la fonction printf */
    #include"complexe.h" /* pour manipuler les nombres complexes */
    
    int main()
    {
      complexe z;
      z = complexe_set_zero(z);
      printf("Le zero complexe est %f + %f i", 
             complexe_get_reel(z), complexe_get_img(z));
      return EXIT_SUCCESS;
    };
    
    

    Le premier programme ne compile pas après la modification apportée à la bibliothèque "complexe", tandis que le second programme, lui, n'a pas besoin d'être modifié.

2.2.1 Quelques règles pour concevoir l'interface d'une structure de donnée

  • Règle numéro 0 : définir les règles de cohérence des informations. Par pour l'exemple du cours, si une case a été choisie deux fois par le joueur, elle est forcément dévoilée.
  • Règle numéro 1 : en tenant compte de la règle numéro 0, pour toute information, écrire une méthode "get" et une méthode "set" permettant d'accéder en lecture et en écriture à cette information. Pour l'exemple du cours, par exemple, on se rend compte que l'état "dévoilé" d'une case ne peut être modifié que lorsque le joueur sélectionne cette case ou une autre de la même ligne ou même colonne. Il ne faut pas écrire de méthode "mod_case_set_visible", car une telle méthode permettrait d'agir sur la visibilité d'une case sans passer par le contrôle effectué par la méthode "mod_case_jouer".
  • Règle numéro 2 : moduler la règle numéro 1, en fonction de l'utilisation que l'on imagine qu'il sera faite de la bibliothèque.
  • Règle numéro 3 : moduler la règle numéro 2, en cherchant à rester le plus évolutif possible.

2.2.2 Liste de bonnes pratiques

  • Bien organiser le code en fichiers, pour le rendre ré-utilisable, évolutif et robuste (programmation modulaire, encapsulation).
  • Rendre son code lisible :
    • bien nommer (variables, fonctions),
    • commenter (pourquoi on fait les choses),
    • préciser le comportement attendu (pré- et post-conditions des fonctions, évolution de la valeur des variables au fil du temps, etc.).
  • Favoriser les passages par adresse (performances à l'exécution).
  • Tester systématiquement et indépendemment.
    • Une structure ⇒ fonctions de test associées : chaque accès au champ d'une structure, chaque fonction doit être au moins techniquement testée (tests unitaires).
    • Un programme ⇒ des scénarios de test fonctionnels. Il faut s'assurer du maintien de la cohérence des données au fil du temps.

3 Exemple d'implémentation en C

3.1 Description du concept modélisé

  • Le modèle est un damier de dix case sur dix.
  • Chaque case est "voilée" au démarrage.
  • L'utilisateur choisit des cases, une par une.
  • Lorsqu'une case est choisie, il ne se passe rien la première fois. Si elle est choisie à nouveau, plus tard, toutes les cases sur la même ligne et celles sur la même colonne sont dévoilées.
  • Une case dévoilée ne peut plus être choisie.
  • Le "jeu" est terminé lorsque toutes les cases ont été dévoilées.

3.3 Fichiers

Le modèle, la vue et le contrôleur sont chacun implémentés dans deux fichiers source. Le programme est donc constitué de six fichiers :

  • modele.h : déclarations des structures et accesseurs du modèle
  • modele.c : définitions des méthodes du modèle
  • vue.h : déclarations des structures et fonctions de la vue
  • vue.c : définitions des fonctions de la vue
  • ctr.h : déclarations des structures et fonctions du contrôleur
  • ctr.c : définitions des fonctions du contrôleur

3.4 Nommage des fonctions

Les fichiers d'en-tête modele.h, vue.h et ctr.h sont destinés à être inclus dans les fichiers source .c à l'aide de directives #include. Ceci permet par exemple d'intégrer les déclarations des accesseurs du modèle dans les fichiers correspondant au contrôleur ou à la vue, et par conséquent ces fonctions peuvent être appelées depuis d'autres fichiers que celui où elles sont définies.

Cela peut poser un problème à la lecture d'un code source : le lecteur peut avoir du mal à trouver la définition d'une fonction, ou même à savoir à quoi cette fonction sert (si elle agit sur le contrôleur, la vue ou le modèle).

Pour aider la lecture et la compréhension du programme, il est bon d'adopter des conventions de nommage pour les fonctions et les types (à la façon de GTK, où le préfixe des fonctions indiquent clairement les objets sur lesquelles elles agissent).

Dans les exemples, les fonctions de la vue commencent par le préfixe vue_, celles du contrôleur par le préfixe ctr_ et les accesseurs du modèle par le préfixe mod_.

Le modèle possède de plus des accesseurs privés, qui ne sont pas déclarés dans modele.h car ils ne sont pas destinés à être utilisés ailleurs que dans modele.c. En effet, ils pourraient compromettre la cohérence des données et ne sont utilisés que par des fonctions du modèle, dans lesquelles cette cohérence est vérifiée. Les noms de ces accesseurs sont préfixés par private_.

3.5 Inclusions et protection de redondance

  • Observez les différentes directives #include dans ces six fichiers.
  • Remarquez que le fichier vue.h inclut le fichier ctr.h. Ceci est nécessaire pour le compilateur, car la fonction vue_creation déclarée dans vue.h prend en paramètre l'adresse d'une structure de type ctr_t qui, elle, est déclarée dans ctr.h.

    Or les fichiers vue.h et ctr.h sont tous deux inclus dans le fichier ctr.c. Comme ctr.h est déjà inclus dans vue.h, que va-t-il se passer lorsque l'on va compiler ctr.c ? Le fichier ctr.h va être inclus deux fois, et toutes ses déclarations vont être lues deux fois par le compilateur, qui produira alors une erreur.

    C'est pour éviter ce type d'erreur que les trois fichiers d'en-tête commencent par les directives de compilation :

    #ifndef _NOM_DU_FICHIER_H_
    #define _NOM_DU_FICHIER_H_
    

    et se terminent par la directive de compilation :

    #endif
    

    De la sorte, le contenu de chaque fichier ne sera lu qu'une fois par le compilateur.

3.6 Déclaration de type sans définition

  • L'inclusion d'un fichier d'en-tête dans un autre fichier d'en-tête peut poser un problème de cyclicité si le second inclut également le premier.

    Dans ctr.h, en particulier, on déclare la structure ctr_t, qui contient un modèle (struct modele) et une vue (struct vue). Mais, pour éviter tout cycle d'inclusion, on ne peut pas inclure le fichier vue.h qui contient la définition de struct vue.

    La solution adoptée ici est la suivante : on ne définit pas les types dans les fichiers d'en-tête, on ne fait que les y déclarer.

    Par exemple, le type structuré ctr_t est déclaré sans être défini en écrivant simplement la ligne :

    typedef struct ctr ctr_t;
    

    Comme il n'y a que des adresses sur des ctr_t dans le fichier ctr.h, on peut se permettre de ne définir le contenu de la structure ctr_t que dans le fichier ctr.c, où ce contenu est utilisé.

    De même, dans vue.h, on n'utilise que l'adresse d'un ctr_t dans la déclaration de la fonction vue_creation. On peut donc se passer de l'inclusion de ctr.h et se contenter de répéter la déclaration du type ctr_t ci-dessus dans le fichier vue.h.

3.7 Interactions entre modèle, vue et contrôleur

  • le modèle (i.e. tout ce qui se trouve dans modele.h et modele.c) est autonome. Il n'appelle aucune fonction des autres composants du programme.
  • la vue doit signaler les actions de l'utilisateur au contrôleur. Elle peut donc appeler des fonctions du contrôleur. Elle n'interragit pas avec le modèle.
  • le contrôleur se charge de l'interaction entre la vue et le modèle; il est donc autorisé à appeler les fonctions de tous les modules du programme. En revanche il ne doit pas accéder directement aux structures de données des autres composants : il est forcé de passer par des appels de fonctions.

    En particulier, dans l'exemple, on s'est arrangé pour que le contrôleur soit totalement indépendant de GTK, de manière à ce qu'il soit compatible avec une interface en mode texte. Les fonctions de callback passées à GTK ont donc été définies dans vue.c.

3.8 retour sur les versions

Lorsque l'architecture est propre et bien définie, il devient beaucoup plus facile de modifier certains éléments. Regardez maintenant les autres versions du programme :

  • avec plusieurs interfaces graphiques
  • avec interface en mode texte Seuls le fichier vue.c a été modifié pour ce changement d'interface.
  • avec une implémentation différente du modèle Dans la première version, le modèle était représenté par la structure suivante :
    typedef struct modele
    {
      char nbchoix[10][10];
      char visible[10][10];
      int fini;
    } modele_t;
    

    Dans cette version, il est maintenant représenté par les structures :

    typedef struct {
      char nbchoix;
      char visible;
    } mod_case_t ;
    
    typedef struct modele
    {
      mod_case_t cases[100];
    } modele_t;       
    

    Les mêmes informations sont stockées, mais différemment. Pour rendre le programme fonctionnel, il suffit alors de modifier uniquement les définitions des accesseurs du modèle : mod_case_get_visible, mod_case_get_nb_choix, private_case_init et private_case_set_visible.

  • avec des "règles du jeu" différentes Dans cette version, il suffit de choisir une case une fois pour la dévoiler, et cela dévoile également toutes ses cases adjacentes, plutôt que celles de sa ligne et de sa colonne.

    C'est uniquement la fonction du modèle mod_case_jouer qui a été modifiée pour atteindre ce résultat.

3.9 Compilation

  • La compilation se fait en quatre étapes
    • chaque fichier .c est compilé séparément (3 fichiers, donc 3 étapes) sans que le résultat soit exécutable. Pour cela, on utilise l'option -c de gcc. On obtient trois fichiers objets : modele.o, ctr.o et vue.o.
    • le fichier exécutable est produit à partir des trois fichiers objets et des librairies GTK. Cette étape s'appelle l'édition de liens.
  • Un fichier Makefile est présent avec les fichiers source. Ce fichier contient des règles de dépendance et des instructions de compilation, du type :
    ctr.o: ctr.c ctr.h vue.h modele.h
            gcc -c ctr.c -Wall -o ctr.o
    

    Une règle de ce type signifie :

    • si les fichiers ctr.c, ctr.h, vue.h ou modele.h sont plus récents que le fichier ctr.o (donc s'ils ont été modifiés depuis la dernière compilation), il faut recréer le fichier ctr.o.
    • pour créer le fichier ctr.o, il faut lancer la commande gcc -c ctr.c -Wall -o ctr.o

    La présence du fichier Makefile dans le répertoire des sources permet de lancer les quatre étapes de compilation en tapant une seule commande : make

    De plus, seuls les fichiers qui doivent être recompilés le seront : si modele.h et modele.c n'ont pas été modifiés depuis la dernière compilation, alors modele.o n'a pas besoin d'être recompilé.

  • La fonction main se trouve dans ctr.c.

Date: mardi 17 février 2015

Author: Antoine Rozenknop

Org version 7.8.03 with Emacs version 23

Validate XHTML 1.0