Nous allons commencer une série d’articles reprenant les gros principes de base en orienté objet. Ce sont 5 points à ne pas rater! Si vous débutez, cela va clairement vous aider à bien structurer votre code. Si vous êtes déjà dans le bain, cela peut vous servir de piqûre de rappel.
Nous aurons donc 5 articles pour chaque lettre de l’acronyme SOLID.
- S : Single responsibility
- O: Open/Closed
- L: Liskov substitution
- I: Interface segregation
- D: Dependency inversion
Commençons aujourd’hui par le premier principe: La responsabilité unique.
Single Responsibility
Le premier principe SOLID est à la fois simple et compliqué. Simple à comprendre, mais difficile à bien appliquer.
« S » comme « Single Responsibility » consiste à dire qu’une classe n’a qu’une, et une seule, raison de changer. On peut aussi dire qu’une classe ne remplit qu’un rôle ou qu’elle n’a qu’une seule responsabilité. Qu’est-ce ce qu’un rôle? Il en existe probablement des milliers, mais en voici une petite liste pour vous faire une idée :
- Affichage
- Logique de vérification de donnée(s)
- Envoi d’un e-mail
- Communication avec la base de données
- Système de cache
- Représentation d’une entité
- Routage en fonction de l’URL
- Logique de paiement
- Etc.
Chacun son rôle, chacun son chemin
Si on a une classe « Utilisateur » qui vérifie qu’un e-mail est valide, cela veut dire qu’elle a 2 rôles : Représentation et vérification. Cette classe a donc 2 raisons d’être modifiée:
- Ajout ou modification d’un attribut de l’entité (ex : Ajout du nom de famille)
- Modification de la vérification d’e-mail, en passant d’une expression régulière à une méthode un peu plus avancée.
Donc, si on utilise une entité, on embarque la vérification de l’e-mail: pas si grave. Par contre, si on veut vérifier un e-mail, on doit instancier un « Utilisateur ». Les 2 rôles sont couplés, nous avons donc une cohésion forte.
On le sait : plus les responsabilités sont couplées, plus les méthodes poussent comme des champignons, plus elles ont de chances d’être modifiées, plus elles sont susceptibles d’avoir des bugs, plus elles sont fragiles, plus elles sont rigides… Bref, tout ça n’annonce rien de bon.
Des tests unitaires pour les méthodes privées ?
Vouloir faire des tests unitaires de méthodes privées n’est pas un bon présage non plus. Si vous voulez les tester grâce à la « Reflection » ou que vous voulez les « mocker » (pas de chance c’est impossible), c’est que souvent, elles cachent une logique qui devrait probablement être externalisée dans une autre classe. Voici l’exemple d’une classe qui normalise une adresse et qui y ajoute des coordonnées grâce à l’api de « Google Maps ».
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
class JolieAdresse{ } class AdresseNormalizer{ public function __construct($rue, $numero, $codePostal, $ville, $pays) { $this->rue = $rue; $this->numero = $numero; $this->codePostal = $codePostal; $this->ville = $ville; $this->pays = $pays; } public function getJolieAdresse() { $json = $this->getGoogleJson(); $jolieAdresse = new JolieAdresse(); $jolieAdresse->latitude = $json['results'][0]['geometry']['location']['lat']; $jolieAdresse->longitude = $json['results'][0]['geometry']['location']['lng']; $jolieAdresse->numero = $json['results'][0]['address_components'][0]['long_name']; $jolieAdresse->rue = $json['results'][0]['address_components'][1]['long_name']; $jolieAdresse->ville = $json['results'][0]['address_components'][2]['long_name']; $jolieAdresse->pays = $json['results'][0]['address_components'][4]['long_name']; $jolieAdresse->codePostal = $json['results'][0]['address_components'][5]['long_name']; return $jolieAdresse; } private function getGoogleJson() { $adresse = $this->rue.' '.$this->numero.', '.$this->codePostal.' '. $this->ville.', ' . $this->pays; $googleUrl = 'http://maps.googleapis.com/maps/api/geocode/json?address=' . urlencode($adresse) . '&sensor=false'; return json_decode(file_get_contents($googleUrl),true); } } $normalizer = new AdresseNormalizer('Avn Louise', '46', '1000', 'Bxl', 'Be'); $jolieAdresse = $normalizer->getJolieAdresse(); |
Voilà un bout de code qui fonctionne bien et qui n’expose que ce qui doit l’être. Cependant, on peut y constater que l’objet « AdresseNormalizer » va récupérer les informations via un webservice et les convertit en un objet « JolieAdresse » : Deux responsabilités. C’est encore plus embêtant si ma classe « JolieAdresse » est une classe spécifique à mon projet, je ne pourrai jamais réutiliser ma super classe « AdresseNormalizer ». Tristesse!
Cela se corse lorsque l’on veut tester unitairement la conversion du retour de « Google Maps » en objet. Se baser sur une ressource externe pour vos tests unitaires (que ce soit celui de Google ou un autre prestataire) peut renvoyer des résultats aléatoires. Il faut donc « mocker » l’appel, mais c’est impossible ici car tout est dans une méthode privée. La seule solution est de diviser la classe en deux :
- Une classe « GoogleMap » qui va chercher les informations d’une adresse donnée, pour les renvoyer au format « json ».
- Pourquoi pas une méthode statique « createFromGoogleMap » dans la classe « JolieAdresse » qui reçoit un objet de type « GoogleMap »
Notez que, grâce à ce refactoring, la classe « GoogleMap » peut être réutilisée dans un autre projet, sans aucun problème. Le maître mot, selon moi, pour le principe de « Single Responsibility » est la réutilisabilité. Chaque classe ou ensemble de classes, pourrait être vu comme un plugin.
N’hésitez pas à refactorer votre code avant qu’il ne soit trop tard. En effet, une classe qui a 2 responsabilités est parfois synonyme de « porte ouverte » : On se dit « si elle peut gérer l’html, on peut y ajouter la gestion du cache » et « si elle gère le cache, on peut y ajouter l’enregistrement dans Memcached ou en base de données? », etc. Les responsabilités s’accumulent et vous voilà coincé avec une classe qui fait le café et qui a peut-être un constructeur avec beaucoup trop d’arguments.
Choisir Bonux
Je terminerai par un diction de Maité qui est aussi valable pour tous les principes que nous allons voir: c’est une question de bon sens. Il est parfois (mais il faut pouvoir le justifier) préférable de vivre avec une classe qui a deux responsabilités, plutôt que deux classes qui n’en n’ont qu’une. L’exemple de la classe « Modem » est assez connu:
On voit clairement que cette classe a deux responsabilités : La gestion de la connexion d’une part et la gestion des messages de l’autre. Cependant, faut-il les séparer ? Vous n’allez pas aimer la réponse, mais cela dépend du contexte. Je vous propose de voir ce qu’en dit Uncle Bob, dans son livre dont voici l’extrait (page 112).
C’est un peu un cas à part, surtout qu’il parle de compilation qui ne nous intéresse que très peu en PHP. Cependant, il est intéressant de voir que même la compilation peut influencer la stratégie à avoir pour coder vos classes… Vous auriez fait quoi vous? Une ou deux classes?
Pas si simple cette recommandation dans la vie de tous les jours… Mais tellement importante! C’est la bonne vieille question du « Qui fait quoi? ».
Merci pour l’article! 🙂