Conférence donnée au PHP Tour Nantes 2012 : Réutilisabilité du code au sein d'un contexte multi-technos basé sur une application concrète des principes de conception SOLID
Oxalide Workshop #4 - Docker, des tours dans le petit bassin
Réutilisabilité du code PHP
1. Réutilisabilité du code
IT&L@bs
CO PMM
Version 1.01, le 29 novembre 2012 Nicolas Le Nardou
Architecte / Expert Technique PHP
nicolas.lenardou@orange.com
Réutilisabilité du code Page 1
2. Introduction : contexte projet
> Centre de services d’un grand groupe de presse
> 20-30 sites en PHP
- eZ Publish, Symfony 2, WordPress, from scratch, …
> Forte audience : environ 10M de pages vues / jour
> Périmètres fonctionnels très proches
> Pression sur les coûts de développement
Réutilisabilité du code Page 2
3. Introduction : contexte projet
> A son écriture, le partage du code entre plusieurs sites est :
- Soit déjà acté
- Soit déjà en cours de discussion
> Le partage avec les autres sites du CDS est toujours de l’ordre du
possible
Réutilisabilité du code Page 3
4. Introduction : problématique
> Comment se construire un référentiel de code commun à tous nos
sites ?
- Quel que soit le socle technique
- Sans renoncer aux apports de ces différents socles (pas de politique du
plus petit dénominateur commun)
> Objectifs :
- Mutualiser la maintenance du code
- Réduire les temps de développement
Réutilisabilité du code Page 4
5. sommaire
> 1 – Rappel des principes de conception SOLID
> 2 – Etude de cas concrets
> 3 – Point sur les tests unitaires
Réutilisabilité du code Page 5
9. SOLID : single responsibility
> Principe de responsabilité unique :
> « Une classe ne fait qu’une et une seule chose »
> Envie de rajouter une fonctionnalité ? Il est temps de créer une
nouvelle classe.
> Une classe au fonctionnement clairement défini et borné sera plus
facilement réutilisable
> Une classe aux multiples responsabilités sera fatalement dupliquée
pour être adaptée au nouveau besoin
Réutilisabilité du code Page 9
10. SOLID : open / closed
> « Une classe doit être fermée à la modification et ouverte à
l’extension »
> Une évolution ne devrait pas vous faire casser du code, juste en
ajouter !
> Une classe doit prévoir de pouvoir être étendue sans être réécrite.
Réutilisabilité du code Page 10
11. SOLID : Liskov substitution
> « On doit pouvoir substituer à un objet d’une classe X, tout objet
d’une sous classe de X »
> Corolaire : Une classe utilisant un objet de classe X ne doit pas
avoir connaissance des sous classes de X (sous peine de violer le
principe open/closed)
Réutilisabilité du code Page 11
12. SOLID : Interface segregation
> « Un objet ne devra pas dépendre d’un autre objet mais de son
interface »
> Il faut expliciter la dépendance réelle au travers d’une interface
Réutilisabilité du code Page 12
13. SOLID : Dependency Injection
> « Un objet ne doit pas instancier un autre objet, il doit le recevoir de
l’extérieur »
> Inversion de contrôle
> Pas d’utilisation du mot clé new dans une classe
> Injection par constructeur ou mutateur
Réutilisabilité du code Page 13
15. cas concret #1
> Besoin : Injecter dans nos pages des tags javascript (tracking, pub,
…)
> Implémentation : Une classe TagServer qui calcule la valeur d’un
tag en fonction d’un contexte en entrée (url, contenu, …) et d’un jeu
de règles
- Moteur de règles
- Jeu de règles en configuration
> Contrainte : A déployer sur :
1. Un site eZ Publish
2. Un site Symfony 2.x
Réutilisabilité du code Page 15
16. cas concret #1
> Les mauvaises solutions :
- Faire 2 développements distincts
- Dupliquer la classe et la modifier
- Nombreux paramètres dans le constructeur
- Ou toute autre abomination …
Réutilisabilité du code Page 16
17. cas concret #1 : implémentation eZ Publish
class TagServer
{
private
$rules;
public function __construct()
{
$this->rules = array();
$ini = eZINI::instance('tagserver.ini');
$ini->assign('Tags', 'Rules', $this->rules);
}
// ...
}
Réutilisabilité du code Page 17
18. cas concret #1 : problèmes
public function __construct()
{
$this->rules = array();
$ini = eZINI::instance('tagserver.ini');
$ini->assign('Tags', 'Rules', $this->rules);
}
> Couplage fort : TagServer dépend de eZINI
La classe n’est réutilisable que sur un autre site eZ Publish
Réutilisabilité du code Page 18
19. cas concret #1 : problèmes
> Solution : injecter l’objet eZINI dans le constructeur
Injection de dépendances (SOLID)
> On pourra ainsi substituer à une occurrence d’eZINI, un objet d’une
sous classe d’eZINI
Réutilisabilité du code Page 19
20. cas concret #1 : eZINI injecté
class TagServer
{
private
$rules;
public function __construct(eZINI $ini)
{
$this->rules = array();
$ini->assign('Tags', 'Rules', $this->rules);
}
// ...
}
Réutilisabilité du code Page 20
21. cas concret #1 : eZINI injecté
> La construction du serveur :
$ini = eZINI::instance('tagserver.ini');
$server = new TagServer($ini);
Réutilisabilité du code Page 21
22. cas concret #1 : eZINI injecté
> Couplage désormais faible
> Mais problème de sémantique : conceptuellement nous n’avons pas
besoin d’un eZINI, nous avons plutôt besoin de la configuration.
Il nous faut une interface « Configuration »
Séparation d’interfaces (SOLID)
Réutilisabilité du code Page 22
23. cas concret #1 : interface Configuration
interface Configuration
{
const SEPARATOR = '/';
/**
* Read configuration if exists. Returns default value
* otherwise.
*
* @param string $variableName fully qualified variable name
* @param mixed $defaultValue
*/
public function read($variableName, $defaultValue);
}
Réutilisabilité du code Page 23
24. cas concret #1 : interface Configuration
class TagServer
{
private
$rules;
public function __construct(Configuration $config)
{
$this->rules = $configuration->read(
'tagserver/Tags/Rules',
array()
);
}
}
Réutilisabilité du code Page 24
25. cas concret #1 : interface Configuration
> La dépendance avec le framework d’eZ Publish est rompue …
> … mais notre code ne fonctionne plus pour eZ Publish
> Il nous faut une implémentation de Configuration reposant sur eZINI
Substitution de Liskov (SOLID)
Réutilisabilité du code Page 25
26. cas concret #1 : eZConfiguration
class eZConfiguration implements Configuration
{
public function read($variableName, $defaultValue)
{
list($file, $group, $variable) =
explode(self::SEPARATOR, $variableName);
$ini = eZINI::instance($file . '.ini');
$ini->assign($group, $variable, $defaultValue);
return $defaultValue;
}
}
Réutilisabilité du code Page 26
27. cas concret #1 : eZConfiguration
> Appel
$configuration = new eZConfiguration();
$server = new TagServer($configuration);
> Fonctionne à nouveau pour eZ Publish
- Sans modification de la classe TagServer
Open / Closed (SOLID)
Réutilisabilité du code Page 27
28. cas concret #1 : site Symfony
> Etape suivante : réutiliser notre classe TagServer sur un site
reposant sur Symfony
Réutilisabilité du code Page 28
29. cas concret #1 : site Symfony
> Bien sûr, la classe eZConfiguration ne fonctionnera pas
> Il nous faut une classe YamlConfiguration
Réutilisabilité du code Page 29
30. cas concret #1 : site Symfony
class YamlConfiguration implements Configuration
{
public function read($variableName, $defaultValue)
{
list($file, $group, $variable) =
explode(self::SEPARATOR, $variableName);
$loader = Yaml::parse($file);
if(array_key_exists($loader[$group][$variable]))
{
return $loader[$group][$variable];
}
return $defaultValue;
}
}
Réutilisabilité du code Page 30
31. cas concret #1 : site Symfony
> Construction du serveur :
$configuration = new YamlConfiguration();
$server = new TagServer($configuration);
> Et …. c’est tout !
Réutilisabilité du code Page 31
32. cas concret #1 : bilan
> Coût du déploiement de notre classe TagServer sur un autre
framework PHP
≈
Coût de développement d’une classe d’adaptation pour accéder à
la configuration
> Aucune modification de notre classe TagServer n’a été nécessaire
> Les classes de la couche d’adaptation sont elles-mêmes
réutilisables
constitution d’une boîte à outils très rapidement
Réutilisabilité du code Page 32
34. cas concret #2
> Nous voulons ajouter des logs à notre classe TagServer
> Contraintes :
- Possibilité de les activer / désactiver
- Possibilité de se reposer sur le système de log du socle technique utilisé
Réutilisabilité du code Page 34
35. cas concret #2
class TagServer
{
private
$logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
}
Réutilisabilité du code Page 35
36. cas concret #2 : empilement de paramètres
class TagServer
{
private
$rules,
$logger;
public function __construct(Configuration $configuration, Logger $logger)
{
$this->logger = $logger;
$this->rules = $configuration->read(
'tagserver/Tags/Rules',
array()
);
}
}
Réutilisabilité du code Page 36
37. cas concret #2 : dépendance faible
> Contrairement à la configuration, le logger est une dépendance
faible
> Un logger n’est pas requis pour le fonctionnement de notre classe
Injection par mutateur
Réutilisabilité du code Page 37
38. cas concret #2 : injection par mutateur
class TagServer
{
private
$logger;
public function __construct(Configuration $configuration)
{
$this->logger = null;
/* ... */
}
public function setLogger(Logger $logger)
{
$this->logger = $logger;
return $this;
}
}
Réutilisabilité du code Page 38
39. cas concret #2 : injection par mutateur
private function writeLog($message)
{
if($this->logger !== null)
{
$this->logger->write($message);
}
}
> Et l’appel :
$server = new TagServer(new eZConfiguration());
$server->setLogger(new eZLogger());
Réutilisabilité du code Page 39
40. cas concret #2 : overhead
> Les cas présentés sont simples et petits
> A dimension d’un projet réel, les overheads de code pour construire
les objets peuvent devenir pénibles à gérer.
> Par exemple, il a fort à parier que le logger soit nécessaire sur de
nombreuses classes.
Réutilisabilité du code Page 40
41. cas concret #2 : conteneur d’injection
> Solution : recours à un conteneur d’injection
> Pimple (Sensio Labs)
> DI Component de Symfony (Sensio Labs)
> Objet en charge de l’instanciation des autres objets
Réutilisabilité du code Page 41
42. cas concret #2 : conteneur commun
abstract class Container extends Pimple
{
public function __construct()
{
$this['tagServer'] = function ($container){
$server = new TagServer($container['configuration']);
$server->setLogger($container['logger']);
return $server;
};
}
}
Réutilisabilité du code Page 42
43. cas concret #2 : conteneur d’injection
Conteneur commun
à tous les socles techniques
Conteneurs spécifiques
Réutilisabilité du code Page 43
44. cas concret #2 : conteneur spécifique (eZ Publish)
class eZContainer extends Container
{
public function __construct()
{
parent::__construct();
$this['configuration'] = function ($container){
return new eZConfiguration();
};
$this['logger'] = $this->share(function ($container){
return new eZLogger();
});
}
}
Réutilisabilité du code Page 44
45. cas concret #2 : conteneur d’injection
> Et la construction de notre classe :
$container = new eZContainer();
$server = $container['tagServer'];
Réutilisabilité du code Page 45
46. cas concret #2 : conteneur d’injection
> Quelques remarques :
- Le conteneur peut s’appuyer sur de la configuration (ex: Symfony)
- Risque de dépendance au conteneur + global state
- Dépendances masquées : quid des outils d’analyse ?
Réutilisabilité du code Page 46
48. testabilité : souvenez-vous
class TagServer
{
private
$rules;
public function __construct()
{
$this->rules = array();
$ini = eZINI::instance('tagserver.ini');
$ini->assign('Tags', 'Rules', $this->rules);
}
// ...
}
Réutilisabilité du code Page 48
49. testabilité : problématique
> Une instance eZ Publish est nécessaire
Problème de performances des tests
> Un fichier eZINI est également nécessaire
Eparpillement du code de test
Maintenabilité affaiblie
> Et si eZINI était un service à bouchonner ? (comme la db, un
webservice ou le filesystem)
Réutilisabilité du code Page 49
51. testabilité : ArrayConfiguration
class ArrayConfiguration implements Configuration
{
private $values;
public function __construct(array $values)
{
$this->values = $values;
}
public function read($variableName, $defaultValue)
{
if(array_key_exists($variableName, $this->values))
{
return $this->values[$variableName];
}
return $defaultValue;
}
}
Réutilisabilité du code Page 51
52. testabilité : le test unitaire
class TagServerTest extends PHPUnit_Framework_TestCase
{
private
$tagServer;
public function setUp()
{
$configuration = new ArrayConfiguration(array(
'tagserver/Tags/Rules' => array(/* ... */)
));
$this->tagServer = new TagServer($configuration);
}
}
Réutilisabilité du code Page 52
53. testabilité : bilan
> C’est testable !
> C’est performant !
> Le test est facile à maintenir !
> Possibilité de tester aussi les cas à la marge :
- Configuration manquante
- Configuration erronée
- Configuration non consistante
- …
Réutilisabilité du code Page 53