mageekblog - Mot-clé - compositionLe blog personnel de Frédéric Hardy. Au menu, PHP, agilité, FreeBSD, cuisine et photographies.2021-12-02T08:20:54+01:00Frédéric Hardyurn:md5:26874ca5b8cd4cac8d08b0e68e64f63aDotclearsvneeg : la classe sparkline ou héritage vs. compositionurn:md5:2f5fecb22f38ca76741c53237accbf0d2010-04-26T18:15:00+02:002010-06-09T18:23:14+02:00mageekguysvneegcompositionheritagePHPPOOsparklinesvneegtrunk<p><a href="http://blog.mageekbox.net/?post/2010/04/22/L-%C3%A9lectro-enc%C3%A9phalogramme-de-PHP-existe-%21"><code>svneeg</code></a> ayant pour but de représenter sous forme graphique le nombre de commits quotidiens effectués sur un dépôt <a href="http://fr.wikipedia.org/wiki/Subversion_%28logiciel%29"><code>svn</code></a> en général et <a href="http://svn.php.net/viewvc/php/php-src/trunk/">le trunk de PHP</a> en particulier, je me suis dis que les <a href="http://fr.wikipedia.org/wiki/Repr%C3%A9sentations_graphiques_de_donn%C3%A9es_statistiques#Sparklines">sparklines</a> étaient le type de graphique le plus adapté.</p>
<p>En effet, leur créateur, à savoir <a href="http://fr.wikipedia.org/wiki/Edward_Tufte">M. Edward Tufte</a>, les décrit comme, je cite, <q>des graphiques intenses en données, de design simple, et ayant la taille d’un mot</q>.</p>
<p><img title="Exemples de sparklines, avr. 2010" style="margin: 0 auto; display: block;" alt="" src="http://blog.mageekbox.net/public/SparklineGallery.png" /></p>
<p>Ce format est donc très adapté à mon besoin puisque je souhaitais pouvoir intégrer le graphique n'importe où de manière simple.</p>
<p>De plus, l'information représentée par le graphe étant simple, la petitesse de sa représentation induite par ce format ne devient pas un frein à sa compréhension.</p>
<p>La solution trouvée et validée, il ne restait plus qu'à la mettre en œuvre.</p>
<p>Et pour une fois, contrairement à mon habitude de réinventer la roue, je me suis mis à la recherche d'un code PHP tiers qui me permette de générer mes <a href="http://fr.wikipedia.org/wiki/Repr%C3%A9sentations_graphiques_de_donn%C3%A9es_statistiques#Sparklines">sparklines</a>.</p>
<p>On me reproche en effet régulièrement de bafouer le mantra <a href="http://www.codinghorror.com/blog/2009/02/dont-reinvent-the-wheel-unless-you-plan-on-learning-more-about-wheels.html"><q>Don't reinvent the wheel !</q></a>, et je m'étais dis que ce <q>on</q> pouvait ne pas avoir tort.</p> <p>Et bien si.</p>
<p>Il avait tort, complètement et irrémédiablement tort.</p>
<p><a href="http://www.google.fr/search?q=php+sparkline">Google</a> m'a bien renvoyé quelques résultats, mais la plupart pointent directement ou indirectement sur la version 0.2 d'une <a href="http://sparkline.org/">classe écrite en PHP 4</a> qui remonte à 2005...</p>
<p>J'ai donc envisagé dans un second temps de faire effectuer le travail par le navigateur, via un script en <a href="http://fr.wikipedia.org/wiki/JavaScript">JavaScript</a> tel que <a href="http://omnipotent.net/jquery.sparkline/">celui-ci</a>, mais j'ai vite abandonné l'idée.</p>
<p>Je souhaitais en effet pouvoir distribuer mon graphique via une simple <abbr title="Uniform Resource Locator">URL</abbr>, à la manière d'une image, et cette solution ne le permettait pas.</p>
<p>Je me suis donc mis à concevoir une classe <a href="http://www.php.net">PHP</a> me permettant de faire de la génération de <a href="http://fr.wikipedia.org/wiki/Repr%C3%A9sentations_graphiques_de_donn%C3%A9es_statistiques#Sparklines">sparklines</a>.</p>
<p>Mon premier réflexe, en aficionados de la programmation orientée objet, a été de concevoir ma classe <code>sparkline</code> de la manière suivante :</p>
<blockquote><pre><code>...<br />namespace svneeg;<br /><br />class sparkline<br />{<br /> protected $width = 0;<br /> protected $height = 0;<br /> protected $foregroundColor = array(90, 90, 90);<br /> protected $backgroundColor = array(255, 255, 255);<br /> protected $gridColor = array(240, 240, 255);<br /> protected $axisColor = array(0, 0, 102);<br /><br /> public function __construct($width, $height)<br /> {<br /> ...<br /> }<br /><br /> public function setWidth($width)<br /> {<br /> ...<br /> }<br /><br /> public function setHeight($height)<br /> {<br /> ...<br /> }<br /><br /> public function setForegroundColor($r, $g, $b)<br /> {<br /> ...<br /> }<br /><br /> public function setBackgroundColor($r, $g, $b)<br /> {<br /> ...<br /> }<br /><br /> public function render($data)<br /> {<br /> ...<br /> }<br /><br /> public function save(array $data, $file)<br /> {<br /> ...<br /> }<br /><br /> ...<br />}<br />...<br /></code></pre></blockquote>
<p>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.</p>
<p>Cependant, à y réfléchir dans le détail, j'avais fait de la merde.</p>
<p>En effet, cette classe est très peu évolutive, comme le démontre ces quelques exemples :</p>
<ul>
<li>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.</li>
<li>Si nous souhaitons dessiner une courbe dans un autre style, il faut dériver la classe et ajouter le code correspondant.</li>
<li>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.</li>
<li>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.</li>
<li>Si nous voulons ajouter à la fois un nouveau format de sauvegarde et un nouvel élément graphique, <del>c'est le bordel</del> il faut dériver la classe et potentiellement faire de la duplication de code.</li>
</ul>
<p>Bref, la solution de l'héritage manque cruellement de souplesse pour faire évoluer cette classe, d'autant que <a href="http://www.php.net">PHP</a> ne supporte pas l'héritage multiple.</p>
<p>Mais pourquoi est-ce que je me retrouve dans cette situation ?</p>
<p>Le problème vient du fait que j'ai donné bien trop de responsabilités à ma classe.</p>
<p>Elle prend en effet en charge :</p>
<ol>
<li>La définition des dimensions de l'image finale,</li>
<li>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,</li>
<li>La génération de l'image finale,</li>
<li>La sauvegarde de l'image finale.</li>
</ol>
<p>Or, à la base, une <a href="http://fr.wikipedia.org/wiki/Repr%C3%A9sentations_graphiques_de_donn%C3%A9es_statistiques#Sparklines">sparkline</a> est une image disposant d'une hauteur et d'une largeur, et qui contient une courbe, éventuellement un ou plusieurs axes, un fond, etc.</p>
<p>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.</p>
<p>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.</p>
<p>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 <code>layer</code> qui prendra en charge à la fois la définition du style de l'élément ainsi que son rendu.</p>
<p>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 <code>writer</code> afin de pouvoir enregistrer l'image dans le format de son choix, voir même dans plusieurs formats.</p>
<p>Le code de la classe <code>sparkline</code> est donc devenu ceci :</p>
<blockquote><pre><code>...<br />namespace svneeg;<br /><br />class sparkline implements \iteratorAggregate, \countable<br />{<br /> ...<br /> protected $width = 0;<br /> protected $height = 0;<br /> protected $layers = array();<br /> protected $writers = array();<br /><br /> public function __construct($width, $height, array $data)<br /> {<br /> ...<br /> }<br /><br /> public function __destruct()<br /> {<br /> ...<br /> }<br /><br /> public function setWidth($width)<br /> {<br /> ...<br /> }<br /><br /> public function setHeight($height)<br /> {<br /> ...<br /> }<br /><br /> public function addLayer(\svneeg\sparkline\layer $layer)<br /> {<br /> ...<br /> }<br /><br /> public function addWriter(\svneeg\sparkline\writer $writer)<br /> {<br /> ...<br /> }<br /><br /> public function render()<br /> {<br /> ...<br /> foreach ($this->layers as $layerNumber => $layer)<br /> {<br /> $this->merge($layerNumber, $layer->draw($this));<br /> }<br /><br /> return $this;<br /> }<br /><br /> public function write()<br /> {<br /> ...<br /> foreach ($this->writers as $writerNumber => $writer)<br /> {<br /> $writer->write($data);<br /> }<br /><br /> return $this;<br /> }<br /><br /> ...<br />}<br />...<br /></code></pre></blockquote>
<p>Pour ajouter un élément au graphique, il n'est donc plus nécessaire de dériver la classe <code>sparkline</code>, il suffit de définir un nouveau type de calque à partir de la classe abstraite <code>layer</code>.</p>
<p>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 :</p>
<blockquote><pre><code> ...<br /> $sparkline<br /> ->addLayer(new layers\background\solid(0, 60, 0))<br /> ->addLayer(new layers\grid\vertical(0, 90, 0)) // dessiné au dessus du fond<br /> ->addLayer(new layers\lines\line()) // dessiné au dessus de la grille verticale<br /> ->addLayer(new layers\axis\y(0, 255, 0)) // dessiné au dessus de la courbe<br /> // pour dessiner l'axe des y en dessous de la courbe, il suffit d'ajouter le calque correspondant avant celui de la courbe.<br /> ;<br /> ...<br /></code></pre></blockquote>
<p>La même philosophie a été appliquée en ce qui concerne la sauvegarde de l'image :</p>
<blockquote><pre><code> ...<br /> $sparkline<br /> ->addWriter(new writers\png(config\directories\tmp . '/' . config\sparkline\name))<br /> ->addWriter(new writers\jpeg(config\directories\tmp . '/' . config\sparkline\name))<br /> ->addWriter(new writers\http\png())<br /> ;<br /> ...<br /></code></pre></blockquote>
<p>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.</p>
<p>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.</p>
<p>Et pour ceux qui sont intéressés par le code, justement, le code de la classe <code>sparkline</code> dans sa dernière version est disponible dans <a href="https://svn.mageekbox.net/repositories/svneeg/trunk/classes/sparkline.php">mon dépôt svn</a>.</p>
<p>Évidement, il ne fonctionne qu'avec au minimum la version 5.3 de <a href="http://www.php.net">PHP</a>, vu qu'il met en oeuvre les <a href="http://fr.php.net/namespaces">espaces de nommage</a>.</p>http://blog.mageekbox.net/?post/2010/04/26/Svneeg-%3A-la-gen%C3%A8se-de-la-classe-sparkline#comment-formhttp://blog.mageekbox.net/?feed/atom/comments/116