Table des matières
Au second chapitre, nous avons introduit le Design Patter Modèle-Vue-Contrôleur (MVC, Model-View-Controller), nous avons exploré sa composition, et nous avons vu en quoi il est adapté aux applications Web. J'avais alors fait remarquer que le concept le plus difficile à prendre en main était celui du Modèle.
Il existe de nombreuses façon d'interpréter le Modèle, mais, pour beaucoup de développeurs, le Modèle est associé à l'accès aux données, une erreur que la plupart des Frameworks encouragent par inadvertance, en n'admettant pas de façon clair qu'ils ne fournissent pas de Modèles complets. Dans notre communauté noyée sous les buzzwords, de nombreux Frameworks laissent dans leur documentation une définition vague et obscure de la notion de Modèle.
En réalité, un Modèle est une représentation d'une partie du domaine de Modèle. Le Domaine de Modèle est constitué de plusieurs objets Modèle interconnectés, qui représentent les données et comportements d'un système. Il est complètement indépendant du Contrôleur et de la Vue. Parler des Domaines de Modèles et de systèmes n'est pas vraiment évident, donc prenons comme exemple une analogie avec un supercalculateur faisant tourner une simulation de modélisation du climat. Bien que la simulation puisse fonctionner sur plusieurs types d'architectures backend, ses fonctionnalités de base incluent les données qui créent les conditions initiales du modèle de climat, et les règles logiques qui déterminent les lois gourvernant l'évolution de la modélisation en fonction du temps qui passe. Ce système global est composé de nombreux modèles interconnectés et interdépendants. Le M du MVC est appelé le Modèle pour des raisons similaires : il modélise le comportement d'un système, tout autant qu'il défini ses données.
Le mapping d'un Modèle vers une Base de Données, ou tout autre espace de stockage où ses données seraient enregistrées, peut être aussi simple qu'une correspondance où chaque Modèle ferait référence à une seule table de la Base de Données, ou, dans le cas où un Modèle est constitué de nombreux sous-Modèles, la correspondance peut être plus complexe, passant par plusieurs tables. Cependant, vous noterez que le mapping du Modèle vers une Base de Données ou un autre mécanisme de stockage ne défini pas le Modèle. En fait, vous pouvez tout à fait ajouter un accès aux données à un Modèle après qu'il ait été développé - et ce n'est pas une approche inhabituelle ! Le Modèle regroupe les règles métier, les comportements, et les contraintes des données qu'il représente, et, ceux-ci peuvent être stockés en Base de Données (de manière générale, après qu'ils aient été réduits à des règles quantifiables et configurables) après qu'ils aient été conçus et documentés.
L'autre aspect du problème est comment est-ce que les Modèles sont utilisés, et comment nous les utilisons par rapport aux autres composants du MVC. La règle de base, bien évidemment, est que les Modèles ne soient pas au courant de la manière dont ils sont représentés. La présentation est la responsabilité à la fois de la Vue et du Contrôleur, qui doivent connaître les Modèles avec lesquels ils interagissent. Mais c'est une communication à sens unique. Les Contrôleur et les Vues peuvent connaître le Modèle, mais le Modèle ne saura jamais quoi que ce soit à propos des deux autres composantes du MVC.
Comme je l'expliquais au Chapitre 2, le Modèle du MVC a deux objectifs principaux : maintenir l'état de l'application entre les requêtes, en stockant toutes les données de l'application avant que la requête ne se termine, de manière à pouvoir les ré-obtenir pour les requêtes suivantes, et contenir les règles métier et les contraintes qui s'appliquent aux données constituant notre état. Cependant, aucun Framework Web ne peut prédire les règles métiers, les types de données ou leurs contraintes, et, donc, les Frameworks ne peuvent pas fournir un Modèle complet. En conséquence, les développeurs doivent créer tous les Modèles eux-même, puisqu'ils sont spécifiques à leur application. Cela ne signifie nullement que les fonctionnalités liées au Modèle d'un Framework soient inutiles ! Les Frameworks offrent toujours des composants de grande valeur, pour aider à accèder aux données de Modèles depuis la Base de Données, des Web Services, ou d'autres sources.
Cela doit tout de même mettre en évidence un concept essentiel que vous devez garder à l'esprit : un Modèle n'est pas seulement responsable de l'accès à une Base de Données. Au contraitre, il encapsule toutes les données et comportements d'une unité spécifique de l'application, à l'exception de tout traitement lié à la présentation.
Tout cela est bien beau en théorie, donc pourquoi ne pas poursuivre avec un rapide exemple ? Notre exemple suit une conception simple de Modèle, où chaque Modèle correspond à une seule table de Base de Données. En plus de cela, notre Modèle est représenté comme une sous-classe de Zend_Db_Table_Abstract. Cela forme une relation "est-un" avec la couche d'accès aux données, plutôt que de choisir l'alternative "a-un", fréquente dans des domaines de Modèles plus complexes, où l'accès aux données est ajouté aux Modèles, et pas forcément utilisé comme fondation sur laquelle les Modèles sont construits.
<?php
class Order extends Zend_Db_Table_Abstract
{
protected $_name = 'orders';
protected $_limit = 200;
protected $_authorised = false;
public function setLimit($limit)
{
$this->_limit = $limit;
}
public function setAuthorised($auth)
{
$this->_authorised = (bool) $auth;
}
public function insert(array $data)
{
if ($data['amount'] > $this->_limit
&& $this->_authorised === false) {
throw new Exception('Unauthorised transaction of greater than '
. $this->_limit . ' units');
}
return parent::insert($data);
}
}
Dans la classe ci-dessus, nous avons implémenté un Modèle Order (Commande) très simple, qui se base sur le fait que les utilisateurs doivent avoir un niveau de privilège suffisant pour pouvoir effectuer des commandes d'un montant supérieur à 200. Si un tel pouvoir n'a pas été accordé par une puissance externe, une Exception est levée, qui devra être attrapée par un Contrôleur, qui sera chargé de gérer le problème. La limite est à 200 par défaut, mais elle peut être modifiée de l'extérieur (via un fichier de configuration, par exemple).
Une autre manière utile de considérer l'idée de Modèle est de supprimer toute la logique métier dans la classe ci-dessus, et de penser à où est-ce qu'elle devrait aller si ce n'est dans le Modèle. Cela nous mène vers un autre mode de pensée classique, que nous allons maintenant examiner.
Jamis Buck (l'auteur de
Capistrano, qui travaille actuellement chez 37signals) a
un jour écrit à propos d'un concept
intitulé "Contrôleur maigre,
Modèle enveloppé" (notre Chris Hartjes a
rédigé un blog-post
sur le même sujet). J'ai toujours apprécié la
simplicité de cette idée, puisqu'elle illustre un aspect clef du MVC. Avec
ce principe, il est considéré comme une bonne pratique d'avoir autant de
logique applicative que possible (comme notre logique métier plus haut) au
sein du Modèle, et non dans les couches Vue ou Contrôleur.
La Vue ne devrait avoir pour rôles que la génération et la présentation de l'interface utilisateur, permettant aux utilisateurs de recevoir la sortie demandée, et de communiquer des requêtes à l'application. Les Contrôleurs sont les opérateurs qui traduisent les saisies effectuées via l'interface utilisateur en actions au niveau du Modèle, et renvoient la sortie vers le client, à partir de la Vue qui a été sélectionnée pour présenter les Modèles. Les Contrôleurs ne doivent définir le comportement de l'application que dans le sens où ils effectuent la mise en relation des saisies effectuées au sein de l'interface utilisateur et des appels aux Modèles, et gèrent l'interaction avec le client ; mais, au-delà de ce rôle, il doit être clair à vos yeux que tout le reste de la logique applicative est conservée au niveau du Modèle. Les Contrôleurs ne sont que des créatures de bas niveau, qui ne font que préparer le terrain, et laissent les choses se dérouler de manière organisée pour l'environnement au sein duquel l'application fonctionne.
Cela crée un lien important, du fait que les Contrôleur et la Vue sont tous les deux orientés vers la présentation. C'est évident avec la Vue, mais moins avec le Contrôleur. Cela dit, puisque le Contrôleur est si concerné par la gestion des mécanismes HTTP ou Console de l'application, sa place est réellement sur la couche présentation de l'application.
Les développeurs PHP, du moins en majorité, ne comprennent généralement pas ces idées en profondeur. Beaucoup considérent que le Modèle est juste une façon cool de signifier accès à la Base de Données, et d'autres le confondent avec divers Design Patterns en rapport avec l'accès à une Base de Données, comme Active Record, Data Mapper, et Table Data Gateway. Nous ne pouvons, cela dit, tout mettre sur le dos des développeurs PHP : de nombreux développeurs Ruby ou Python font souvent la même erreur. C'est une erreur que les Frameworks encouragent souvent, sans le vouloir, en ne définissant pas clairement le Modèle dès le départ. En ne comprennant pas réellement ce qu'est un Modèle, pourquoi c'est une bonne idée, et comment il devrait être développé et déployé, les développeurs se tirent une balle dans le pieds, en adoptant d'autres alternatives.
Avec cette solution alternative, les développeurs restreignent leur Modèles à de l'accès aux données, et placent la logique métier dans le Contrôleur. Cette solution est problèmatique, puisqu'elle ajoute de la logique métier dans la portion du MVC qui est responsable de la présentation. Oui, encore une fois, les Contrôleurs font parti de la couche présentation ! Faire correspondre des entrées via l'interface utilisateur avec des actions de Modèles, localiser et rendre les vues, gérer les communications avec le client... Vous voyez : présentation !
La façon la plus simple de démontrer ce problème est de juger de la possibilité de mise en place de tests unitaires sur ce type de logique métier. Les Contrôleurs sont connus pour être difficiles à tester, du fait qu'il s'agit d'unités de présentations, qui, comme la Vue, nécessitent un passage par toute l'application pour chaque test. Vous ne pouvez alors tester que la sortie finale, ce qui signifie que les tests de Contrôleurs font des assertions sur les données passées à la Vue, ou même sur le contenu de la Vue finale lui-même. Si vous êtes familier avec la mise en place de tests unitaires, vous réaliserez que cela ne correspond pas au principe de base de tests isolés, et cela rend la Conception Dirigée par les Tests (TDD, Test-Driven Design), et le Développement Dirigé par le Comportement (BDD, Behavioural-Driven Development) difficile à appliquer aux règles métier. La plupart de ces tests unitaires sont en fait des tests fonctionnels, qui testent l'ensemble de l'application, et non seulement une partie spécifique de celle-ci.
Une autre démonstration pratique de ce problème est d'imaginer un nouveau projet que vous venez de terminer en utilisant Zend Framework. Le client est impressioné, mais annonce qu'il faut que toutes ses applications soient migrées vers symfony. Je l'admet, ce n'est pas un scénario commun, mais plus d'une application a dû être migrée vers une autre plate-forme dans le passé, ou a demandé une remise à neuf conséquente.
Si vous avez placé toutes les règles métiers dans le Modèle, alors, vous pouvez à peu de chose près vous lancer et effectuer la migration facilement. Les Modèles sont globalement indépendants des Frameworks Web, bien qu'ils puissent se reposer sur des composants d'accès aux données, qui, eux, ne sont pas aussi facilement migrés (ce qui n'est pas un problème pour Zend Framework, puisqu'il n'est que faiblement couplé), sauf si une relation "a-un" avec la couche d'accès aux données a été utilisée pour permettre aux composants d'accès aux données d'être plus facilement remplacés avec des solutions alternatives. Si vous avez placé la logique métier dans les Contrôleurs, vous allez réaliser votre erreur : symfony ne peut pas exécuter les Contrôleurs Zend Framework ! Vous allez alors devoir commencer à migrer des paquets entiers de code source d'un Framework à l'autre, plutôt que la logique minimale à laquelle un Contrôleur aurait du être limité. Considérez aussi cela d'un point de vue des difficultés que vous rencontrerez pour les tests unitaires, et vous pourrez aussi dire adieux aux tests fonctionnels que vous avez mis en place en vous basant sur Zend_Test.
Pour résumer, les Modèles sont des classes distinctes, qui peuvent être testées indépendamment de la couche présentation, et facilement migrées vers d'autres Frameworks. Les Contrôleurs sont à l'extrême opposée ! Ils sont tellement liés à la couche présentation et au Framework lui-même qu'ils ne sont pas aisément, ni immédiatement, portables.
Dans le passé, les développeurs PHP ont employé deux design patterns courant pour créer leurs applications : les design patterns Page Controller, et Transaction Script. Magré leurs jolis noms, et le fait qu'ils soient présentés comme des Design Patterns, ils font tout juste référence à la manière la plus intuitive qui soit de créer des pages de contenu en PHP, où chaque page est représentée par un script PHP contenant toutes les opérations nécessaires à la création de celle-ci, organisées de manière procédurale. Bien que nous n'en soyons plus à ces techniques, et que nous nous soyons orientés vers une approche MVC, il est difficile de se débarasser des vieilles habitudes, et les idées utilisées pour ces deux pratiques ont trouvé moyen de renaitre au sein de l'approche Modèle-Vue-Contrôleur.
Comme c'est une appelation qui saute aux yeux, colorée, et qui pourrait attirer votre attention, je fais référence à ces éléments ressuscités sous le terme de Gros, moche, et stupide Contrôleurs (FSUCs, Fat Stupid Ugly Controllers). J'avais initialement pensé à Stupid Fat Ugly Controllers (SFUC), mais cela me semblait être un peu trop aggressif, même si c'était plus parlant.
Le FSUC typique est le résultat d'un développeur resté dans l'état d'esprit Page Controller et Transaction Script lorsqu'il a adopté l'approche MVC, et de la diminution continue du rôle du Modèle, en les restreignant à un seul rôle d'accès aux données. Cela entraine des Contrôleurs qui brisent la règle de "Contrôleurs réduits, Modèles larges", et l'inversent totalement. Avec cette approche, la logique métier est totalement placée au sein des Contrôleurs.
Le cas typique de FSUC met en place toutes les opérations de logique métier, et rencontre des difficultés avec le concept même de Modèles, puisque les Contrôleurs ne sont pas des classes distinctes et réutilisables. Les développeurs leurs utilisent aussi naturellement et fréquemment qu'ils le faisaient avec les Page Controllers, avec à peu près la même conséquence. Maintenant, chaque page de l'application correspond à un énorme paquet de code spaghetti, qui ne bénéficie guère des avantages d'une bonne conception orientée objet. Les méthodes des Contrôleurs s'allongent, les classes d'aide se multiplient au fur et à mesure que l'on essaye de penser aux Modèles parci-parlà, et les tests unitaires sont souvent abandonnés ou limités à des tests fonctionnels, puisque vous devez initialiser tout votre MVC pour pouvoir faire quoi que ce soit - ce qui est difficile puisqu'il n'y a pas encore de Vue, puisque votre Web Designer n'a pas encore été embauché.
Les FSUCs sont gros, lourds, moches, et gras. Le terme qui s'approcherait le mieux, en un langage proche de la programmation, est "bloated". Ils jouent un rôle pour lequel ils n'avaient pas été pensé. Ils vont totalement à l'encontre de ce qui vous a été enseigné lorsqu'il s'agit de mettre en pratique des principes d'approche Orientée Objet. Mais, pour une raison inconnue, les développeurs semblent préférer les FSUCs aux Modèles, malgré le fait que ces Contrôleurs ne soient réellement que des Page Controllers ou des Transaction Scripts déguisés.
Vous vous souvenez des problèmes que j'évoquais lorsque je parlais de remplacer les Modèles par des Contrôleurs ? Les tests unitaires deviennent difficiles, appliquer des méthodes TDD ou BDD devient quasiment impossible sans une obstination sans faille, et vous ne pourrez jamais migrer efficacement ce paquet vers un autre Framework sans avoir à redévelopper la moitié de l'application.
Ce type de couplage fort et de conception de code confuse est le type de chose que les fans de Kent Beck sont supposés identifier avec le terme de "code smell" lors de phases de Refactoring. Mais ce n'est pas la seule conséquence douteuse des FSUCs. Les FSUCs comportent souvent du code dupliqué, manquent de classes refactorisées et isolées, et peuvent être un cauchemard à maintenir. En bref ils sont pour ainsi dire le mal absolu, sauf peut-être pour le plus simple prototype d'application.
Regardons cet exemple de combinaison d'un Modèle simple et d'un FSUC :
<?php
class Order extends Zend_Db_Table_Abstract
{
protected $_name = 'orders';
}
class OrderController extends Zend_Controller_Action
{
protected $_limit = 200;
protected $_authorised = false;
public function init()
{
// magically set authorised flag or limit
// restriction here somehow
}
public function createAction()
{
// keeping it simple, if insecure
$data = $_POST;
$model = new Order;
if ($data['amount'] > $this->_limit
&& $this->_authorised === false) {
// setup View to report the error
}
$model->insert($data);
}
}
Maintenant, imaginez que vous vouliez implémenter une nouvelle fonctionnalité nommée Order Batch où de nombreuses commandes sont gérées et créées par lots. Vous tombez de suite sur le problème : toute la logique métier de la Commande est placée dans OrderController::createAction(), qui n'est pas réutilisable. Allez-vous copier les règles métier vers le nouveau BatchController ? Créer une file de tâches pour effectuer plusieurs appels à OrderController ? Peut-être pouvez-vous créer un nouveau Helper d'Action commun tous les Contrôleurs ? Continuez à rayer les options jusqu'à ce que vous réalisiez qu'une classe indépendante (un Modèle) est la solution qui fonctionne le mieux. C'est de loin la solution la plus flexible, et elle obéit au concept de Conservez-le Simple Stupide (KISS, Keep It Simple Stupid).
L'autre aspect de ce manque de considération pour les Modèles est que lorsque les développeurs sont convaincus que les Modèles devraient être minimalistes et que les Contrôleurs sont les plus importants, cela renforce la dépendance envers les Contrôleurs, et les pousse à assumer un nouveau rôle de "Police des Données" (une des principales cause de leur mutation en FSUCs).
Dans ce rôle, toutes les requêtes d'accès aux données passent par un Contrôleur. Cela dit, ce n'est pas toujours une stratégie appropriée, notamment lorsqu'elle est utilisée dans des scénarios où d'autres portions de l'application veulent seulement accéder aux données.
Voici un exemple que j'ai rencontré sur les mailing-lists Zend Framework alors que j'écrivais ce livre. Dans une quelconque application inconnue, toutes les pages affichent le nombre total d'utilisateurs quelque part dans une barre sur le côté de l'écran. Pour pouvoir injecter cette donnée dans un template, toutes les Actions de Contrôleurs à travers toute l'application chargent cette donnée et la passent à la Vue courante. La plupart du code source permettant d'accomplir ceci est copié dans la méthode d'initialisation de chaque Contrôleur.
Le problème évident avec ceci est la duplication de code. Au fur et à mesure que le code source est copié vers chaque classe Contrôleur, il va devenir de plus en plus difficile à maintenir. Il y aussi un autre élément à prendre en compte : en se basant sur cette description, la seule partie de l'application qui utilise cette donnée est la Vue. Le Contrôleur n'en n'a jamais directement besoin, ne la manipule jamais, et n'effectue jamais d'opération d'écriture vers le Modèle avec celle-ci. Considérant que le Contrôleur n'utilise jamais cette donnée, pourquoi est-ce c'est ce composant qui fait tout le travail pour la récupérer ?
La solution que j'ai proposé sur la mailing-list était de supprimer l'intermédiaire. Puisque le Contrôleur n'utilise jamais la donnée, prenons un peu de recul, et laissons la Vue charger la donnée elle-même, à l'aide d'une simple classe assistante (une seule classe, réutilisable à l'infini). Cette petite histoire illustre une erreur commune : les Contrôleurs ne sont pas la seule source d'interaction avec le Modèle. Ils ne sont pas exclusivement la Police des Données. Les Vues peuvent elles-même travailler avec un Modèle sans en demander la permission au Contrôleur, et charger des données pour la présentation de manière plus efficace.
Si vous creusez un peu plus cette idée, vous réaliserez vite que c'est une couche de complexité supplémentaire que vous pouvez supprimer des Contrôleurs, en réduisant encore un peu leur taille. Plus ils sont petits, mieux c'est !
Notez que le terme "View Helper" est associé à JAVA, en tant que Design Pattern, dans le livre "Core J2EE Design Patterns". Pour ce qui est du MVC, tout laisse à croire que les Vues devraient connaitre les Modèles qu'elles représentent, et pas seulement quelques listes de données que nous leur passerions depuis les Contrôleurs.
Lire ce chapitre vous aura, avec un peu de chance, inspiré quelques nouvelles idées. L'idée qui est à la base de tout le reste au-dessus est l'application d'une Programmation Orientée Objet élégant. Des Contrôleurs énormes, dépendants du Framework sous-jacent, posent des problèmes évidents quand on y pense, alors que les Modèles, qui sont fortement découplés, et structurés comme un système de classes indépendantes, sont beaucoup plus maintenables, testables, et portables. Ce découplage des Modèles les rend facile d'accès depuis n'importe où, y compris depuis les Vues.
Ce chapitre marque la fin de notre examen du MVC, et je crois que se pencher un peu plus en détails sur le Modèle au sein de cette architecture était du temps sagement investi. Au chapitre 4, qui suit, nous passerons quelques temps à examiner Zend Framework comme implémentation du MVC, et ferons connaissance avec quelques uns de ses composants principaux, lorqu'il s'agit d'implémenter des applications en suivant une approche MVC.