Les images peuvent être soumises à des droits dauteur

PHP : Quand déclarer les classes finales ?

(Last Updated On: 28 décembre 2022)

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 finalmarqueur sur les classes PHP.

Le motif est récurrent :

  1. Je demande qu’une classe nouvellement introduite soit déclarée commefinal
  2. l’auteur du code est réticent à cette proposition, affirmant que cela finallimite la flexibilité
  3. 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 finalet 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”:

finaldoit ê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 setDbméthode est utilisée pour changer le DbConnectionInterfaceau 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 à la MasterSlaveConnectionplace ?
  • La setDbméthode n’est pas couverte par le RegistrationServiceInterface, nous ne pouvons donc l’utiliser que lorsque nous associons strictement notre code au SwitchableDbRegistrationService, ce qui va à l’encontre de l’objectif du contrat lui-même dans certains contextes.
  • La setDbméthode modifie les dépendances au moment de l’exécution, ce qui peut ne pas être pris en charge par la RegistrationServicelogique et peut également entraîner des bogues.
  • Peut-être que la setDbmé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’ setDbexemple, mais ce sont les plus pertinents pour notre objectif d’expliquer pourquoi finalcela 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 finaloblige 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- extendsclé, ou mieux, faites vos cours finalet é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 finalré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- finalclé 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 :

  1. Il y a une abstraction (interface) que la classe finale implémente
  2. 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- finalclé sans une abstraction sur laquelle s’appuyer en premier.

Un autre cas où vous ne souhaitez pas utiliser le finalmot-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 finalmarqueur à 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 :
Pas malMoyenBienAcès bienExcélent (No Ratings Yet)
Loading...

Laisser un commentaire

Translate »