Et bien si.
Il avait tort, complètement et irrémédiablement tort.
Google m'a bien renvoyé quelques résultats, mais la plupart pointent directement ou indirectement sur la version 0.2 d'une classe écrite en PHP 4 qui remonte à 2005...
J'ai donc envisagé dans un second temps de faire effectuer le travail par le navigateur, via un script en JavaScript tel que celui-ci, mais j'ai vite abandonné l'idée.
Je souhaitais en effet pouvoir distribuer mon graphique via une simple URL, à la manière d'une image, et cette solution ne le permettait pas.
Je me suis donc mis à concevoir une classe PHP me permettant de faire de la génération de sparklines.
Mon premier réflexe, en aficionados de la programmation orientée objet, a été de concevoir ma classe sparkline
de la manière suivante :
...
namespace svneeg;
class sparkline
{
protected $width = 0;
protected $height = 0;
protected $foregroundColor = array(90, 90, 90);
protected $backgroundColor = array(255, 255, 255);
protected $gridColor = array(240, 240, 255);
protected $axisColor = array(0, 0, 102);
public function __construct($width, $height)
{
...
}
public function setWidth($width)
{
...
}
public function setHeight($height)
{
...
}
public function setForegroundColor($r, $g, $b)
{
...
}
public function setBackgroundColor($r, $g, $b)
{
...
}
public function render($data)
{
...
}
public function save(array $data, $file)
{
...
}
...
}
...
Je m'étais dit, sans trop y réfléchir, qu'il me suffirait de dériver cette classe en fonction de mes besoins pour obtenir de nouveaux types de graphiques plus ou moins complexes tel que ceux présentés au début de ce billet.
Cependant, à y réfléchir dans le détail, j'avais fait de la merde.
En effet, cette classe est très peu évolutive, comme le démontre ces quelques exemples :
- Si nous souhaitons sauvegarder le graphique dans autre chose qu'un fichier ou sous un autre format il faut dériver la classe et ajouter le code correspondant.
- Si nous souhaitons dessiner une courbe dans un autre style, il faut dériver la classe et ajouter le code correspondant.
- Si nous souhaitons ajouter au graphique un élément, comme une légende ou un axe d'ordonnées ou d'abscisse, il faut dériver la classe et ajouter le code correspondant.
- Si nous souhaitons ajouter au graphique un élément, il n'est pas possible de le dessiner à la profondeur de son choix, par exemple entre le fond de l'image et la courbe.
- Si nous voulons ajouter à la fois un nouveau format de sauvegarde et un nouvel élément graphique,
c'est le bordelil faut dériver la classe et potentiellement faire de la duplication de code.
Bref, la solution de l'héritage manque cruellement de souplesse pour faire évoluer cette classe, d'autant que PHP ne supporte pas l'héritage multiple.
Mais pourquoi est-ce que je me retrouve dans cette situation ?
Le problème vient du fait que j'ai donné bien trop de responsabilités à ma classe.
Elle prend en effet en charge :
- La définition des dimensions de l'image finale,
- La définition du style des éléments graphiques qui compose l'image finale tel que la courbe, les axes, la grille et le fond,
- La génération de l'image finale,
- La sauvegarde de l'image finale.
Or, à la base, une sparkline est une image disposant d'une hauteur et d'une largeur, et qui contient une courbe, éventuellement un ou plusieurs axes, un fond, etc.
Ce n'est donc pas de son ressort de prendre en charge la définition du style des éléments graphiques, leur rendu, ou bien encore sa propre sauvegarde.
Il est donc nécessaire de déléguer ces tâches à des classes annexes, et donc de faire de la composition, en terme de programmation orientée objet.
Le dessin de chaque élément graphique doit donc être délégué à une instance d'une classe dédiée dérivée d'une classe layer
qui prendra en charge à la fois la définition du style de l'élément ainsi que son rendu.
De même, la sauvegarde du graphique doit être assurée par une ou plusieurs instances d'une classe dédiée dérivée d'une classe writer
afin de pouvoir enregistrer l'image dans le format de son choix, voir même dans plusieurs formats.
Le code de la classe sparkline
est donc devenu ceci :
...
namespace svneeg;
class sparkline implements \iteratorAggregate, \countable
{
...
protected $width = 0;
protected $height = 0;
protected $layers = array();
protected $writers = array();
public function __construct($width, $height, array $data)
{
...
}
public function __destruct()
{
...
}
public function setWidth($width)
{
...
}
public function setHeight($height)
{
...
}
public function addLayer(\svneeg\sparkline\layer $layer)
{
...
}
public function addWriter(\svneeg\sparkline\writer $writer)
{
...
}
public function render()
{
...
foreach ($this->layers as $layerNumber => $layer)
{
$this->merge($layerNumber, $layer->draw($this));
}
return $this;
}
public function write()
{
...
foreach ($this->writers as $writerNumber => $writer)
{
$writer->write($data);
}
return $this;
}
...
}
...
Pour ajouter un élément au graphique, il n'est donc plus nécessaire de dériver la classe sparkline
, il suffit de définir un nouveau type de calque à partir de la classe abstraite layer
.
De plus, avec cette nouvelle version, il est possible de dessiner un élément graphique à la profondeur de son choix, en fonction de l'ordre dans lequel les calques sont ajoutés au graphique :
...
$sparkline
->addLayer(new layers\background\solid(0, 60, 0))
->addLayer(new layers\grid\vertical(0, 90, 0)) // dessiné au dessus du fond
->addLayer(new layers\lines\line()) // dessiné au dessus de la grille verticale
->addLayer(new layers\axis\y(0, 255, 0)) // dessiné au dessus de la courbe
// pour dessiner l'axe des y en dessous de la courbe, il suffit d'ajouter le calque correspondant avant celui de la courbe.
;
...
La même philosophie a été appliquée en ce qui concerne la sauvegarde de l'image :
...
$sparkline
->addWriter(new writers\png(config\directories\tmp . '/' . config\sparkline\name))
->addWriter(new writers\jpeg(config\directories\tmp . '/' . config\sparkline\name))
->addWriter(new writers\http\png())
;
...
La composition est donc une solution élégante qui permet d'avoir un code souple, évolutif et surtout simple car le périmètre fonctionnel de chaque classe est strictement limité à une tâche précise.
En conséquence, il y a beaucoup moins de risques d'erreurs lors du développement et l'évolutivité et la maintenabilité du code est grandement augmentée par rapport à une solution qui nécéssite le recours à l'héritage.
Et pour ceux qui sont intéressés par le code, justement, le code de la classe sparkline
dans sa dernière version est disponible dans mon dépôt svn.
Évidement, il ne fonctionne qu'avec au minimum la version 5.3 de PHP, vu qu'il met en oeuvre les espaces de nommage.
8 réactions
1 De metagoto - 28/04/2010, 02:34
Sympathique.
J'ai parcouru le code rapidos sur le repo et je décèle un problème qui vient briser toute ton argumentation: les fonctionnalités du "driver" sont éparpillées (et codées en dur) dans plusieurs composants. En conséquence, ajouter un driver basé sur ImageMagick, par exemple, va nécessiter un refactoring complet (c'est un euphémisme) de presque toutes les classes.
Je chipote hein, je suis là pour ça
2 De mageekguy - 28/04/2010, 07:14
@metagoto : Le , qui va d'ailleurs être renommé en a pour seul et unique but de générer un calque et gérer sa destruction et celle des ressources associées (couleur, etc).
En effet, pour l'instant, un calque est dédié à une API graphique spécifique (gd, imagemagik, autre), pour la simple et bonne raison que faire un driver générique n'est à mon avis pas une bonne idée (ca revient à faire un driver pour tout les SGBD existant, et nous savons très bien tous les deux ce que cela donne).
Les API offrent des fonctionnalités trop disparates et diverses pour faire cela.
J'ai donc adopté la logique PDO, en créant une classe qui se charge de créer et détruire le calque en fonction de l'API graphique choisie pour ce calque tout comme PDO uniformise la connexion à la base.
C'est effectivement discutable et bizarre, mais au moins, la duplication de code est limitée.
Et c'est pour cela que la méthode draw() d'un calque renvoit une châine de caractères et non une ressource.
Et puis, comme d'habitude, rien ne dit que cela ne va pas changer.
3 De metagoto - 28/04/2010, 08:53
Un layer dédié à une api graphique, ça me va. Comme tu le dis, une interface unifiée serait d'une lourdeur peu recommandable (même dans le cadre d'un Google Summer of Code™). Mais dans ce cas, il faut revoir les implémentations des writers et même de la classe host (sparkline). En fait il faudrait revoir une bonne partie de l'architecture si on veut, je te cite, "une solution élégante qui permet d'avoir un code souple, évolutif et surtout simple car le périmètre fonctionnel de chaque classe est strictement limité à une tâche précise".
Je me permettrai donc de tempérer l'enthousiasme dont tu fais part dans la conclusion de ton billet
"Et puis, comme d'habitude, rien ne dit que cela ne va pas changer."
lol. Ce sont de sages paroles
4 De mageekguy - 28/04/2010, 10:07
@metagoto : Je ne sais pas quelle version tu as regardé, mais pas mal de choses ont déjà changé à ce niveau.
Je t'invite donc à regarder la dernière version.
5 De ashgenesis - 30/04/2010, 09:53
J'ai trouvé sympa l'idée et j'ai voulu m'amuser à faire la même chose sur mon projet actuel ;). Cependant, il n'y a pas encore de moyen d'authentication pour les dépôts en nécessitant.
J'ai donc rajouté 2 méthodes avant le svn_log dans le recorder.php pour m'authentifier en enregistrant les paramètres dans le fichier de config au niveau de la déclaration des informations du dépots svn cela donne donc
svn_auth_set_parameter(SVN_AUTH_PARAM_DEFAULT_USERNAME, config\svn\username);
svn_auth_set_parameter(SVN_AUTH_PARAM_DEFAULT_PASSWORD, config\svn\password);
namespace svneeg\config\svn
{
const repository = 'http://mondepotsvn/svn/trunk/';
const defaultStartRevision = 1;
const delta = 90; // jours
}
6 De mageekguy - 30/04/2010, 11:27
@ashgenesis : T'es vraiment un gars bien, toi, tu le sais j'espère ?
7 De ashgenesis - 30/04/2010, 12:33
Ben j'en avais besoin j'ai juste rajouté quelques lignes et je fais partager C'est toujours utile :D
8 De mageekguy - 30/04/2010, 13:03
@ashgenesis : Ton ajout est dans le trunk, tu peux mettre à jour ;).