Découvrez le principe de responsabilité unique
Le principe SRP pour "Single Responsibility Principle" énonce qu’une classe ne devrait avoir qu’une et une seule raison de changer.
L’idée ici est de faire en sorte qu’une classe ne soit responsable que d’une seule fonction de votre application, et que cette responsabilité soit complètement encapsulée ("cachée") dans la classe.
L’objectif du principe SRP est de réduire la complexité de votre projet.
Bien sûr, quand vous démarrez un nouveau projet, cela peut sembler exagéré, mais dès que votre projet sera composé de dizaines de classes comportant chacune des dizaines de fonctions, il vous sera de plus en plus difficile de faire évoluer chacune des fonctions de votre application.
Vos classes seront tellement grosses que vous ne serez plus en capacité d’en retenir les détails d’implémentation (ce qu’elles sont censées faire). Vous aurez alors besoin de plus de temps pour trouver l’origine d’un bug, et vous aurez des difficultés à faire le lien entre deux classes.
Il sera complètement impossible à une personne qui arrive sur le projet de comprendre tout ce "***" (choisissez le terme adapté :D).
Croyez-moi, de nombreux vieux projets de logiciels finissent dans cet état où la seule solution pour les faire évoluer est de les refaire de zéro, entraînant beaucoup de perte de temps et d’argent.
Enfin, plus une classe fera de choses, plus vous aurez de modifications à faire dans de multiples fichiers pour faire évoluer votre code. Il sera difficile pour vous ou vos relecteurs de mesurer l’impact de toutes ces modifications, surtout que ces dernières risquent de casser le comportement d’autres fonctions de cette classe.
Mettez en pratique le principe SRP
La bonne nouvelle de ce principe, c’est qu’il est très simple à mettre en place : il suffit de diviser vos classes en de multiples classes ayant chacune une et une seule responsabilité ! :ange:
Prenons un exemple qui parlera à beaucoup d’entre nous, les fameuses classes nommées Tools
ou "Outils", qui sont finalement la version "objectifiée" de nos fichiers functions.php
d’antan...
<?php
// src/Tools.php
class Tools
{
public static function redirectAdmin($url)
{
header('Location: ' . $url);
exit;
}
public static function dateFormat($params, &$smarty)
{
return Tools::displayDate($params['date'], null, (isset($params['full']) ? $params['full'] : false));
}
public static function displayDate($date, $id_lang = null, $full = false, $separator = null)
{
if ($id_lang !== null) {
Tools::displayParameterAsDeprecated('id_lang');
}
if ($separator !== null) {
Tools::displayParameterAsDeprecated('separator');
}
if (!$date || !($time = strtotime($date))) {
return $date;
}
if ($date == '0000-00-00 00:00:00' || $date == '0000-00-00') {
return '';
}
if (!Validate::isDate($date) || !Validate::isBool($full)) {
throw new \Exception('Invalid date');
}
$context = Context::getContext();
$date_format = ($full ? $context->language->date_format_full : $context->language->date_format_lite);
return date($date_format, $time);
}
}
Ceci est un court extrait, mais il suffit pour appliquer le principe SRP sur cette classe. Si on liste les quelques responsabilités du code d’exemple :
formatage de dates avec les fonctions
displayDate
etdateFormat
;action de redirection HTTP (telle que l’on aurait pu le faire dans la partie précédente) ;
et une fonction qui sert à marquer un paramètre de fonction comme déprécié.
Ayant connaissance de tout cela, nous pouvons donc redécouper cette classe en 3 classes qui auront chacune un nom beaucoup plus adapté à sa fonction :
la classe
HttpHeaders
par exemple ne contiendrait que des fonctions qui permettraient de manipuler des en-têtes HTTP ;la classe
DateFormatter
aurait la responsabilité de formater des dates au format voulu ;enfin, la classe
Deprecator
pourrait contenir toutes les fonctions pour informer le développeur qu’il utilise un code qui n’est plus maintenu, et est voué à disparaître.
Voici le code mis à jour qui respecte le premier principe SOLID. Notre première classe,HttpHeaders
:
<?php
// src/HttpHeaders.php
class HttpHeaders
{
public static function redirect($url) {/*...*/}
}
Puis, DateFormatter
:
<?php
// src/Deprecator.php
class Deprecator
{
public static function displayParameterAsDeprecated($param) {/*...*/}
}
Enfin, la classe Deprecator
:
<?php
use Deprecator;
// src/DateFormatter.php
class DateFormatter
{
public static function dateFormat($params, &$smarty) {/*...*/}
public static function displayDate($date, $id_lang = null, $full = false, $separator = null)
{
if ($id_lang !== null) {
Deprecator::displayParameterAsDeprecated('id_lang');
}
if ($separator !== null) {
Deprecator::displayParameterAsDeprecated('separator');
}
if (!$date || !($time = strtotime($date))) {
return $date;
}
if ($date == '0000-00-00 00:00:00' || $date == '0000-00-00') {
return '';
}
if (!Validate::isDate($date) || !Validate::isBool($full)) {
throw new \Exception('Invalid date');
}
$context = Context::getContext();
$date_format = $full ?
$context->language->date_format_full :
$context->language->date_format_lite
;
return date($date_format, $time);
}
}
Le code doit vous sembler déjà plus facile à lire et à manipuler, non ? ;)
Nous avons aussi renommé la fonction redirectAdmin
en redirect
, car elle n’avait rien de spécifique à l’Admin !
Et pourtant, nous ne pouvons pas encore être satisfaits par la classe DateFormatter
, notamment par la fonction displayDate
qui a beaucoup trop de responsabilités, dont une bonne partie a été dépréciée, si l’on prend le temps de lire son code ! Voici une version améliorée de cette classe :
<?php
use Deprecator;
use DateValidator;
// src/DateFormatter.php
class DateFormatter
{
public static function dateFormat($params, &$smarty) {/*...*/}
public static function displayLite($date)
{
$time = DateValidator::validate($date);
$date_format = Context::getContext()->language->date_format_lite;
return date($date_format, $time);
}
public static function displayFull($date)
{
$time = DateValidator::validate($date);
$date_format = Context::getContext()->language->date_format_lite;
}
// fonction dépréciée, utilisez plutôt displayLite ou displayFull !
public static function displayDate($date, $id_lang = null, $full = false, $separator = null)
{
if ($id_lang !== null) {
Deprecator::displayParameterAsDeprecated('id_lang');
}
if ($separator !== null) {
Deprecator::displayParameterAsDeprecated('separator');
}
if (!$date || !($time = strtotime($date))) {
return $date;
}
if ($date == '0000-00-00 00:00:00' || $date == '0000-00-00') {
return '';
}
if (!Validate::isDate($date) || !Validate::isBool($full)) {
throw new \Exception('Invalid date');
}
$context = Context::getContext();
$date_format = $full ?
$context->language->date_format_full :
$context->language->date_format_lite
;
return date($date_format, $time);
}
}
class DateValidator
{
public static function validate($date)
{
if (!$date || !($time = strtotime($date))) {
return $date;
}
if ($date == '0000-00-00 00:00:00' || $date == '0000-00-00') {
return '';
}
if (!Validate::isDate($date) || !Validate::isBool($full)) {
throw new \Exception('Invalid date');
}
return $time;
}
}
Mais attendez... le principe SRP s’applique aux classes, pas aux fonctions, n’est-ce pas ? Il faudrait redécouper nos fonctions en multiples fonctions d’un ou deux arguments ? :-°
Le principe SRP est tout aussi applicable aux fonctions. Dès le moment où une fonction a trop de responsabilités, vous aurez des difficultés à la faire évoluer... notamment si elle est appelée par d’autres fonctions. Vous allez alors ajouter encore et encore des arguments et des conditions, et les problèmes de maintenance et de compréhension vont s’accumuler.
N’hésitez donc pas à réduire la complexité d’une fonction en la redécoupant en multiples fonctions beaucoup plus simples, et qui ont une et une seule responsabilité. :)
Exercez-vous !
J’ai identifié une classe nommée Uploader
, qui a plusieurs responsabilités :
le téléchargement de fichiers, bien sûr ;
le redimensionnement d’images ;
la vérification du type MIME des fichiers ;
la récupération de l’extension des fichiers.
Téléchargez l'archive de l'exercice et commencez par créer les différentes classes. Remplacez d’abord le code de chaque fonction qui n’est pas à sa place dans la classe Uploader
par un appel à la fonction de la nouvelle classe.
Par exemple :
<?php
use FileInformation;
class Uploader
{
public function getExtension()
{
$fileInformation = new FileInformation();
return $fileInformation->getExtension($this->name);
}
}
À ce stade, le code devrait toujours fonctionner sans problème : après tout, vous avez seulement déplacé le code.
Ensuite, recherchez dans le script de test de l'application les appels aux fonctions de la classeUploader
et remplacez-les (lorsque c’est nécessaire) par des appels à vos nouvelles classes : votre application devrait toujours fonctionner.
Enfin, vérifiez le comportement en exécutant le script app.php
: si le code ne fonctionne plus, c’est que vous avez oublié de remplacer certains appels de fonction. :euh:
En résumé
Une classe, une fonction ne doit avoir qu'une et une seule responsabilité ;
Le code devient plus facile à lire et à maintenir, et beaucoup plus facile à tester ;
Procédez par étape, en vérifiant à chaque fois que votre projet fonctionne toujours ;
Lorsque l'on "refactorise son code", c'est très souvent pour appliquer ce principe sans même s'en rendre compte ! :p
Une fois cet exercice fini, rejoignez-moi dans le second chapitre à la découverte du deuxième principe SOLID !