De l'intérêt des itérateurs pour la clarté du code
- 05/09/2008
Développement://
De l'intérêt des itérateurs pour la clarté du codeJ'ai commencé cette année à utiliser des itérateurs partout dans mon code. C'est extrêmement pratique dès que l'on doit parcourir des éléments : fichiers, enregistrements d'une base de données, entrées d'un tableau, etc...
Passons tout de suite à une implémentation rapide en PHP
Tout d'abord, le code initial, développé en TDD et donc facilement modifiable, extrait d'un projet réel :
function VolumetrieEnquete($id_enquete)
{
$result = array();
$sql = 'SELECT id, label FROM table1 WHERE id_enquete = %d';
$sql = sprintf($sql, $id_enquete);
$req = mysql_query($sql) or user_error("erreur SQL : $sql / enquête : $id_enquete");
if($req) while($tab = mysql_fetch_assoc($req))
{
$sql = 'SELECT count(*) AS nb FROM table_%d_data';
$sql = sprintf($sql, $tab['id']);
$reqNb = mysql_query($sql) or user_error("erreur SQL : $sql / enquête : $id_enquete / formulaire : " . $tab['id']);
if($reqNb) if($tabNb = mysql_fetch_assoc($reqNb))
$taille = $tabNb['nb']; else $taille = 0;
mysql_free_result($reqNb);
$result[$tab['id']] = array(
'nom'=>$tab['label'],
'taille'=>$taille);
}
mysql_free_result($req);
return $result;
}
On a deux SELECT imbriqués, des requêtes SQL composées à coup de sprintf(), des libérations de ressources, de la gestion basique d'erreurs, le tout très imbriqué, peu lisible. Voilà ce que j'ai obtenu après ré-écriture :
function VolumetrieEnquete($id_enquete)
{
$result = array();
$sql = 'SELECT id, label FROM table1 WHERE id_enquete = %d';
$questionnaires = new DataIterateur($sql, $id_enquete);
while($questionnaires->Suivant())
{
$sql = 'SELECT count(*) AS nb FROM table_%d_data';
$volume = new DataIterateur($sql, $questionnaires->id);
$taille = $volume->Suivant() ? $volume->nb : $taille = 0;
$result[$questionnaires->id] = array(
'nom'=>$questionnaires->label,
'taille'=>$taille);
}
return $result;
}
Ne trouvez-vous pas que c'est un poil plus clair?
Et voilà la classe toute simple qui permet cette réduction :
class DataIterateur
{
var $req = NULL;
function __construct($sql)
{
$args = array();
for($ct = 1; $ct < func_num_args(); $ct++) $args[] = mysql_escape_string(func_get_arg($ct));
$sql = vsprintf($sql, $args);
$this->req = mysql_query($sql) or user_error("erreur SQL : $sql");
}
function __destruct()
{
mysql_free_result($this->req);
}
function Suivant()
{
return $this->data = mysql_fetch_assoc($this->req);
}
function __get($champ)
{
if(isset($this->data[$champ])) return $this->data[$champ];
else SetErreur("erreur SQL : champ $champ inconnu");
}
}
L'intérêt, c'est que j'échange une complexité diffuse, présente à chaque parcours de résultats de requête contre une légère complexité technique très localisée, pour obtenir au final un code plus simple.
Quelques astuces :
- le constructeur prend un nombre variable de paramètres, avec le premier sous forme d'une chaine de type sprintf(); cela évite l'appel explicite à cette fonction précieuse à chaque utilisation
- la libération des ressources se fait dans le destructeur, elle est donc transparente dans le code "métier"
- la méthode __get() permet d'utiliser la formulation $iter->nom_de_champ pourvu que ce champ soit présent dans la requête.
Conclusion : les itérateurs c'est bon, d'ailleurs je vais en manger encore longtemps. Je pense que j'aurais tout intérêt à me rapprocher de l'interface iterator de PHP pour rendre mes objets itérables, mais d'une part je préfère le simple while(next) et de l'autre j'aime bien le $iter->champ, très naturel et très simple. Enfin, je vais essayer.
On peut aussi aller plus loin, et avoir des itérateurs métiers spécifiques à l'application développé. Le SQL disparait alors totalement du code courant (il est enfermé dans la classe itérateur), et on peut ajouter à cette classe des fonctions calculant toute sorte de choses à partir des champs récupérées. Cela fournit une couche d'accès aux données très simple et solide, en tout cas c'est mon ressenti dans mon projet actuel.
Rubriques des billets
- Agilité - 16 billets
- Archerie - 8 billets
- Avis - 53 billets
- Cultures - 11 billets
- Délires - 37 billets
- Démocrachie - 6 billets
- Développement - 32 billets
- Développement web - 23 billets
- Ergonomie - 17 billets
- Geekerie - 11 billets
- Inclassable - 5 billets
- Informatique - 22 billets
- Japon - 3 billets
- Littératures - 34 billets
- PHP - 5 billets
- Poor Lonesome Coder - 19 billets
- Régalons-nous - 6 billets
- Sortons! - 2 billets
- Travail - 16 billets
- Voyages - 2 billets
- Webmasteriat - 18 billets
Commentaires(s)
- 2008-09-05 17:52:59 - Laurentj
Rien ne t'empèche t'implémenter Iterator et de pouvoir continuer à utiliser les résultats en tant qu'objet. Suffit que tu remplaces mysql_fetch_assoc par mysql_fetch_object ;-)
Quand à l'implémentation, elle est franchement simple. Rajoute implements Iterator, et les methodes suivantes
class DataIterateur implements Iterator {
// ici tes méthodes originales
// implémentation de Iterator
protected $_recordIndex = 0;
public function current () {
return $this->data;
}
public function key () {
return $this->_recordIndex;
}
public function next () {
$this->data = mysql_fetch_object ($this->req);
if($this->data)
$this->_recordIndex++;
}
public function rewind () {
mysql_data_seek ( $this->req, 0);
$this->_recordIndex = 0;
$this->data = mysql_fetch_object ($this->req);
}
public function valid () {
return ($this->data != false);
}
}
Et voilà, dans ton exemple, tu peux remplacer ton while par foreach($questionnaires as $questionnaire).
Bon, mais à vouloir implémenter ce genre de chose, revient à réinventer la roue : tu as PDO qui fait ça déjà tout seul dans PHP ;-) (sans compter les nombreuses lib similaires qui traînent sur le web) - 2008-09-05 18:07:19 - Cédric
C'est vrai, et je vais étudier la manière dont PDO et PHP5 utilisent ou implémentent les itérateurs. Toujours intéressant de lire le code des autres.
Ceci étant, utiliser sans comprendre me semble dangereux, et clairement l'exemple que j'ai extrais montre qu'un itérateur c'est tout simple. Ensuite PDO est surement bien, mais est-ce intégrable dans un projet de bonne taille et avec des collègues qui ne sont pas très fans? Enfin, vu la simplicité d'écrire soi même les itérateurs, je ne suis pas convaincu de l'utilité d'utiliser une librairie générique donc forcément plus complexe.
Surtout dans un développement agile où le conseil est de partir léger, avec uniquement ce qui est utile. Le framework doit s'imposer parce qu'il rend les choses plus simple. Le choisir à priori est déconseillé.
Dans ton exemple, je ne vois pas à quoi me servent rewind(), valid(), key() et $_recordIndex. Ils sont utiles en général, pas forcément pour mon besoin précis. - 2008-09-05 19:25:07 - K
Dans la même veine que Laurentj : SPL powa ! (http://www.php.net/~helly/php/ext/spl/)
Par contre pour un iterator je n'ai jamais compris l'intérêt (sauf pour un appel de next() conditionné) de passer par un while(). Je pense qu'un foreach est ce qu'il y a de mieux pour cela (ceci n'étant que mon avis). Pourquoi foreach ? Parce que ça évite d'écrire 3 lignes supplémentaires, c'est-à-dire une ligne pour récupérer la valeur, une pour la clef et une pour passer à l'élément suivant.
L'appel des méthodes reste possible à l'intérieur de la boucle foreach donc on ne perd rien par rapport à un while.
Et pour ce qui est de "Le framework doit s'imposer parce qu'il rend les choses plus simple. Le choisir à priori est déconseillé." > PDO n'est pas un framework mais une extension largement implémentée dans PHP5, il n'y a pas à choisir mais juste à utiliser :p (sauf si l'on utilise déjà la brique d'un framework). C'est juste que quitte à utiliser des fonctionnalités "natives" (pas vraiment puisque liées à une extension), autant passer par PDO.
En dernier je dirais que c'est inutile de dire : "je ne vois pas à quoi me servent rewind(), valid(), key() et $_recordIndex". C'est comme intégrer l'extension mysql et se plaindre qu'on en utilise pas toutes les fonctions... - 2008-09-05 22:48:28 - Laurentj
Euh... PDO, c'est *intégré* dans PHP, livré *en standard*.
>mais est-ce intégrable dans un projet de bonne taille
pourquoi ça ne le serait pas ?
> et avec des collègues qui ne sont pas très fans
Mouai bof, qu'ils changent de langage alors. Encore une fois, c'est un truc livré en standard, autant l'utiliser.
Je dirais même que ça facilite la vie : le développeur qui connait bien PHP5 et qui débarque dans ton projet, il n'a pas à apprendre une énième API d'accès à la base de donnée, si PDO est utilisé.
>une librairie générique donc forcément plus complexe.
Ouah le raccourci que tu nous fait ?!! je vois pas le rapport entre "générique" et "complexe". Au contraire, ça apporte de la simplicité. Que tu bosses sur un projet utilisant mysql, ou utilisant postgresql par exemple, c'est la même API. Pas de question à te poser !
>Surtout dans un développement agile où le conseil est de partir léger
Justement, réécrire ses propres classes d'accés aux bases de données, ça rend forcément ton code plus lourd (des fichiers en plus à parser et à interpreter) que si tu utilises les API intégrés au langage.
>Dans ton exemple, je ne vois pas à quoi me servent rewind(), valid(), key()
Ce sont les méthodes qu'impose l'interface Iterator (en d'autres termes, tes methodes Suivant et cie font doublon). De plus, utiliser une interface, surtout quand elle est "standard" dans le langage, cela permet d'utiliser ton objet partout ou l'interface est requise (ici Iterator). Avec ta classe qui a son API propre, tu peux pas, et c'est dommage. - 2008-09-08 10:47:21 - Cédric
SPL fait partie de ce que je dois étudier plus en détails, tout comme PDO.
Ce que j'aime bien dans le while($iter->next), c'est d'éviter la création d'une variable/objet, ce que me semble imposer le foreach(). Mais peut être est-ce là méconnaissance, et je vais tâcher de vérifier cela.
En clair, le while me semble plus léger, moins complexe que le foreach() pour un itérateur. J'adore le foreach pour les tableaux, je n'ai pas encore ce réflexe pour le reste.
Bref, préférence personnelle et surtout pas règle générique.
L'extension Mysql ne m'impose pas de définir toutes les méthodes d'une interface. Les fonctions sont là, si je ne les utilise pas je ne les vois pas. Au contraire des méthodes qui doivent être implémentées même si elles ne servent à rien.
PDO est certes fourni en standard et largement utilisée, et je ne crois pas à la solution unique. PDO est une solution, sans doute une bonne solution, mais pas la seule. C'est bien en cherchant à côté de ce qui existe que l'on fini par créer le futur outil.
L'intégrer dans un projet de bonne taille : ma question est de savoir si selon votre expérience on peut commencer à utiliser PDO sur une petite partie du projet tout en ayant du Mysql natif partout ailleurs, pour migrer petit à petit le code. Sachant qu'il n'y a qu'un seul mysql_connect effectué systématiquement au démarrage.
C'est possible je n'en doute pas, mais y'a t'il des pièges à éviter, des bons conseils à appliquer?
Quant à la relation complexité et généricité, je la maintiens. Quelque chose de générique est forcément plus complexe. Pas forcément plus compliqué à utiliser, mais il y aura nécessairement des fonctions inutiles pour MON cas particulier. Ce n'est pas une raison pour délaisser PDO je suis d'accord. Cela simplifie ensuite la portabilité et une partie du travail, mais au prix d'une librairie plus chargée.
Bon, j'étudie enfin SPL, la classe iterator et PDO et je fais un billet dessus. Merci pour votre aide, ça me permet de creuser un peu plus là où je n'ai pas encore osé aller.
Ecrire votre commentaire
05/09/2008 - Systeme