Faites en sorte que vos classes soient toujours final
, si elles implémentent une interface et qu’aucune autre méthode publique n’est définie
Le mois dernier, j’ai eu quelques discussions sur l’utilisation du final
marqueur sur les classes PHP.
Le motif est récurrent :
- Je demande qu’une classe nouvellement introduite soit déclarée comme
final
- l’auteur du code est réticent à cette proposition, affirmant que cela
final
limite la flexibilité - Je dois expliquer que la flexibilité vient de bonnes abstractions, et non de l’héritage
Il est donc clair que les codeurs ont besoin d’une meilleure explication pour savoir quand utiliser final
et quand l’éviter.
Il existe de nombreux autresarticles sur le sujet, mais ceci est principalement considéré comme une “référence rapide” pour ceux qui me poseront les mêmes questions à l’avenir.
Quand utiliser “final”:
final
doit être utilisé dans la mesure du possible .
Pourquoi dois-je utiliser “final”?
Il existe de nombreuses raisons de noter une classe comme final
: je vais lister et décrire celles qui sont les plus pertinentes à mon avis.
1. Empêcher une chaîne d’héritage massive de malheur
Les développeurs ont la mauvaise habitude de résoudre les problèmes en fournissant des sous-classes spécifiques d’une solution existante (non adéquate). Vous l’avez probablement vu vous-même avec des exemples comme suit :
<?php class Db { /* ... */ } class Core extends Db { /* ... */ } class User extends Core { /* ... */ } class Admin extends User { /* ... */ } class Bot extends Admin { /* ... */ } class BotThatDoesSpecialThings extends Bot { /* ... */ } class PatchedBot extends BotThatDoesSpecialThings { /* ... */ }
C’est, sans aucun doute, comment vous ne devriez PAS concevoir votre code.
L’approche décrite ci-dessus est généralement adoptée par les développeurs qui confondent la POO avec « un moyen de résoudre des problèmes via l’héritage » (« programmation orientée héritage », peut-être ?).
2. Encourager la composition
En général, empêcher l’héritage de manière énergique (par défaut) a le bel avantage de faire réfléchir davantage les développeurs à la composition.
Il y aura moins de fonctionnalités de bourrage dans le code existant via l’héritage, ce qui, à mon avis, est un symptôme de précipitation combiné à une dérive des fonctionnalités .
Prenons l’exemple naïf suivant :
<?php class RegistrationService implements RegistrationServiceInterface { public function registerUser(/* ... */) { /* ... */ } } class EmailingRegistrationService extends RegistrationService { public function registerUser(/* ... */) { $user = parent::registerUser(/* ... */); $this->sendTheRegistrationMail($user); return $user; } // ... }
En créant le RegistrationService
final
, l’idée d’en EmailingRegistrationService
être une classe enfant est niée d’emblée, et des erreurs stupides telles que celle montrée précédemment sont facilement évitées :
<?php final class EmailingRegistrationService implements RegistrationServiceInterface { public function __construct(RegistrationServiceInterface $mainRegistrationService) { $this->mainRegistrationService = $mainRegistrationService; } public function registerUser(/* ... */) { $user = $this->mainRegistrationService->registerUser(/* ... */); $this->sendTheRegistrationMail($user); return $user; } // ... }
3. Forcez le développeur à penser à l’API publique de l’utilisateur
Les développeurs ont tendance à utiliser l’héritage pour ajouter des accesseurs et des API supplémentaires aux classes existantes :
<?php class RegistrationService implements RegistrationServiceInterface { protected $db; public function __construct(DbConnectionInterface $db) { $this->db = $db; } public function registerUser(/* ... */) { // ... $this->db->insert($userData); // ... } } class SwitchableDbRegistrationService extends RegistrationService { public function setDb(DbConnectionInterface $db) { $this->db = $db; } }
Cet exemple montre un ensemble de failles dans le processus de réflexion qui a conduit à SwitchableDbRegistrationService
:
- La
setDb
méthode est utilisée pour changer leDbConnectionInterface
au moment de l’exécution, ce qui semble cacher un problème différent en cours de résolution : peut-être avons-nous besoin d’un à laMasterSlaveConnection
place ? - La
setDb
méthode n’est pas couverte par leRegistrationServiceInterface
, nous ne pouvons donc l’utiliser que lorsque nous associons strictement notre code auSwitchableDbRegistrationService
, ce qui va à l’encontre de l’objectif du contrat lui-même dans certains contextes. - La
setDb
méthode modifie les dépendances au moment de l’exécution, ce qui peut ne pas être pris en charge par laRegistrationService
logique et peut également entraîner des bogues. - Peut-être que la
setDb
méthode a été introduite à cause d’un bogue dans l’implémentation d’origine : pourquoi le correctif a-t-il été fourni de cette manière ? Est-ce une solution réelle ou ne résout-elle qu’un symptôme ?
Il y a plus de problèmes avec l’ setDb
exemple, mais ce sont les plus pertinents pour notre objectif d’expliquer pourquoi final
cela aurait empêché ce genre de situation dès le départ.
4. Forcer le développeur à réduire l’API publique d’un objet
Étant donné que les classes avec beaucoup de méthodes publiques sont très susceptibles de casser le SRP , il est souvent vrai qu’un développeur voudra remplacer l’API spécifique de ces classes.
Commencer à faire chaque nouvelle implémentation final
oblige le développeur à penser à de nouvelles API dès le départ et à les garder aussi petites que possible.
5. Une final classe peut toujours être rendue extensible
Coder une nouvelle classe en tant final que signifie également que vous pouvez la rendre extensible à tout moment (si vraiment nécessaire).
Aucun inconvénient, mais vous devrez expliquer votre raisonnement pour un tel changement à vous-même et aux autres membres de votre équipe, et cette discussion peut conduire à de meilleures solutions avant que quoi que ce soit ne soit fusionné.
6. extends rompt l’encapsulation
À moins que l’auteur d’une classe ne l’ait spécifiquement conçue pour l’extension, vous devriez l’envisager final
même si ce n’est pas le cas.
L’extension d’un cours rompt l’encapsulation, et peut entraîner des conséquences imprévues et/ou des ruptures de BC : réfléchissez à deux fois avant d’utiliser le mot- extends
clé, ou mieux, faites vos cours final
et évitez aux autres d’avoir à y penser.
7. Vous n’avez pas besoin de cette flexibilité
Un argument que je dois toujours contrer est qu’il final
réduit la flexibilité d’utilisation d’une base de code.
Mon contre-argument est très simple : vous n’avez pas besoin de cette flexibilité.
Pourquoi en avez-vous besoin en premier lieu ? Pourquoi ne pouvez-vous pas écrire votre propre implémentation personnalisée d’un contrat ? Pourquoi ne pouvez-vous pas utiliser la composition ? Avez-vous bien réfléchi au problème ?
Si vous avez encore besoin de supprimer le mot- final
clé d’une implémentation, il peut y avoir une autre sorte d’odeur de code impliquée.
8. Vous êtes libre de changer le code
Une fois que vous avez créé une classe final
, vous pouvez la modifier autant que cela vous plaît.
Étant donné que l’encapsulation est garantie d’être maintenue, la seule chose dont vous devez vous soucier est l’API publique.
Maintenant, vous êtes libre de tout réécrire, autant de fois que vous le souhaitez.
Quand éviter final :
Les classes finales ne fonctionnent efficacement que sous les hypothèses suivantes :
- Il y a une abstraction (interface) que la classe finale implémente
- Toute l’API publique de la classe finale fait partie de cette interface
Si l’une de ces deux conditions préalables est manquante, vous arriverez probablement à un moment où vous rendrez la classe extensible, car votre code ne repose pas vraiment sur des abstractions.
Une exception peut être faite si une classe particulière représente un ensemble de contraintes ou de concepts qui sont totalement immuables, inflexibles et globaux à un système entier. Un bon exemple est une opération mathématique : $calculator->sum($a, $b)
il est peu probable qu’elle change avec le temps. Dans ces cas, il est prudent de supposer que nous pouvons utiliser le mot- final
clé sans une abstraction sur laquelle s’appuyer en premier.
Un autre cas où vous ne souhaitez pas utiliser le final
mot-clé concerne les classes existantes : cela ne peut être fait que si vous suivez semver et que vous remplacez la version majeure pour la base de code affectée.
Essaye le!
Après avoir lu cet article, envisagez de revenir à votre code et, si vous ne l’avez jamais fait, d’ajouter votre premier final
marqueur à une classe que vous envisagez d’implémenter.
Vous verrez le reste se mettre en place comme prévu.
Merci de votez pour cet article :