Pourquoi limiter l'héritage au final?

Vous avez probablement entendu cette fameuse déclaration du GoF: "Préférez la composition à l'héritage de classe." Et puis, en règle générale, de longues réflexions ont porté sur la façon dont l'héritage déterminé statiquement n'est pas si flexible par rapport à une composition dynamique.


La flexibilité est bien sûr une caractéristique de conception utile. Cependant, lors du choix d'une architecture, nous nous intéressons principalement à la maintenabilité, la testabilité, la lisibilité du code, la réutilisation des modules. Donc, avec ces critères de bonne conception, l'héritage est également un problème. "Et maintenant, n'utilisez pas du tout l'héritage?" - demandez-vous.


Voyons comment une forte dépendance entre les classes par héritage peut rendre votre architecture système trop rigide et fragile. Et pourquoi utiliser l'un des mots-clés les plus mystérieux et insaisissables du code - final . Les idées formulées sont illustrées à l'aide d'un simple exemple transversal. À la fin de l'article, il y a des astuces et des outils pour un travail pratique avec les classes final .


Le problème de la classe de base fragile


Le problème de la classe de base fragile


L'un des principaux critères d'une bonne architecture est le couplage lâche , qui caractérise le degré d'interconnexion entre les modules logiciels. Ce n'est pas pour rien que la faiblesse de l'engagement fait partie de la liste des modèles GRASP qui décrivent les principes de base du partage des responsabilités entre les classes.


Un engagement faible présente de nombreux avantages.


  • En relâchant les dĂ©pendances entre les modules logiciels, vous facilitez la maintenance et le support du système en crĂ©ant une architecture plus flexible.
  • Il existe la possibilitĂ© de dĂ©velopper en parallèle des modules Ă  couplage lâche sans risque de perturber leur fonctionnement.
  • La logique de la classe devient plus Ă©vidente, il devient plus facile d'utiliser la classe correctement et pour son usage prĂ©vu, et il est difficile de l'utiliser - faux.

Traditionnellement, les dépendances dans un système sont principalement comprises comme des connexions entre l'objet utilisé (service) et l'objet utilisateur (client). Une telle relation modélise la relation d'agrégation lorsque le service fait «partie» du client ( a une relation ) et que le client transfère la responsabilité du comportement au service intégré. Un rôle clé dans l'affaiblissement de la relation entre le client et le service est joué par le principe d'inversion de dépendance ( DIP ), qui propose de convertir une dépendance directe entre modules en une dépendance réciproque des modules sur une abstraction commune.


Cependant, vous pouvez également améliorer considérablement l'architecture de l'application en desserrant les dépendances dans le cadre d'une relation is-a . La relation d'héritage par défaut crée un couplage étroit , la plus forte parmi toutes les formes de dépendances possibles, et doit donc être utilisée très soigneusement.


Lien solide concernant l'héritage


Lien solide concernant l'héritage


La quantité de code partagée entre les classes parent et enfant est très importante. Ce problème commence à se manifester particulièrement fortement lorsque le concept d'héritage est abusé - en utilisant l'héritage exclusivement pour la réutilisation de code horizontal, et non pour créer des sous-classes spécialisées. Parce que l'héritage est le moyen le plus simple de réutiliser du code. Vous avez juste besoin d'écrire des extends ParentClass et c'est tout! Après tout, c'est beaucoup plus simple que l'agrégation, l' injection de dépendance (DI), l'allocation d'interface.


La réduction du lien de classe dans la hiérarchie d'héritage est traditionnellement obtenue en utilisant des modificateurs restrictifs de la portée ( private , protected ). Il existe même une opinion selon laquelle les propriétés de classe doivent être déclarées exclusivement avec le modificateur private . Et le modificateur protected doit être appliqué très soigneusement et uniquement aux méthodes, car il encourage les dépendances entre les classes parent et enfant.


Cependant, les problèmes d'héritage ne sont pas seulement liés au masquage des propriétés et des méthodes, ils sont beaucoup plus profonds. De nombreux ouvrages sur l'architecture des applications, y compris le livre classique du GoF, sont sceptiques quant à l'héritage et suggèrent de rechercher des conceptions plus flexibles. Mais n'est-ce que de la flexibilité? Je propose ci-dessous de systématiser les problèmes de succession, et ensuite de réfléchir à la manière de les éviter.


Prenons comme «lapin expérimental» la classe de bloc de commentaires la plus simple avec un tableau de commentaires à l'intérieur. Des classes similaires avec une collection à l'intérieur se trouvent en grand nombre dans n'importe quel projet.


 class CommentBlock { /** @var Comment[]   */ private $comments = []; } 

Les exemples ci-dessous sont délibérément simplifiés dans le style KISS afin de montrer les idées formulées dans le code. Vous pouvez trouver des exemples de code dans l'article et une annotation détaillée sur leur utilisation dans ce référentiel .


Problèmes d'héritage


Commençons par le problème le plus évident, qui est principalement donné dans la littérature sur l'architecture.


L'héritage viole le principe de la dissimulation


Étant donné que la sous-classe a accès aux détails d'implémentation de la classe parente, il est souvent dit que l'héritage viole l'encapsulation.

GoF, Design Patterns

Bien que le livre classique, The Gangs of Four, traite des violations d'encapsulation, il serait plus exact de dire que «l'héritage viole le principe de la dissimulation» . Après tout, l' encapsulation est une combinaison de données et de méthodes conçues pour leur traitement. Mais le principe de dissimulation ne fait que garantir la restriction de l'accès de certains composants du système aux détails de la mise en œuvre d'autres.


Le respect du principe de dissimulation dans l'architecture permet d'assurer l'engagement des modules grâce à une interface stable. Et si la classe autorise l'héritage, elle fournit automatiquement les types d'interfaces stables suivants:


  • interface publique utilisĂ©e par tous les clients de cette classe;
  • interface protĂ©gĂ©e utilisĂ©e par toutes les classes enfants.

C'est-à-dire la classe enfant dispose d'un arsenal de capacités bien plus important que l'API publique. Par exemple, il peut affecter l'état interne de la classe parente caché dans ses propriétés protected .


Souvent, une classe enfant n'a pas besoin d'accéder à tous les éléments de la classe parente qui sont accessibles par héritage, cependant, vous ne pouvez pas fournir sélectivement l'accès aux membres protected des classes pour certaines des sous-classes. La classe enfant commence à dépendre de l' interface protégée de la classe parente.


La principale raison de l'héritage de classe, le plus souvent, est d'étendre les fonctionnalités de la classe parente en réutilisant son implémentation. Si initialement l'accès à l'implémentation de la classe parente via l' interface protégée ne laissait pas présager de problèmes, alors que le système se développe, le programmeur commence à utiliser cet accès dans les méthodes de la classe enfant et à renforcer le lien dans la hiérarchie.


La classe parente est désormais obligée de maintenir la stabilité non seulement de l' interface publique , mais également de l' interface protégée , car toute modification de celle-ci entraînera des problèmes dans le travail des classes enfants. Cependant, il est impossible de refuser d'utiliser protected membres de classe protected . Si l' interface protégée correspond complètement à l' interface publique externe, c'est-à-dire la classe parente n'utilisera que private membres public et private , alors l'héritage n'a généralement aucun sens.


En fait, le mot clé protected n'offre aucune protection aux membres de la classe. Pour avoir accès à ces membres, il suffit d’hériter de la classe, et dans le cadre de la classe enfant, vous avez toutes les chances de violer le principe de la dissimulation. Il devient très facile d'utiliser une classe de manière incorrecte, ce qui est l'un des premiers signes d'une mauvaise architecture.


Utiliser une classe via une interface protégée


Violation du principe de dissimulation via une interface protégée


Plus important encore, les éléments encapsulés (constantes, propriétés, méthodes) deviennent non seulement lisibles et invoqués dans la classe enfant, mais peuvent également être remplacés. Une telle opportunité comporte un danger caché - en raison de tels changements, le comportement des objets de la classe enfant peut devenir incompatible avec les objets de la classe parent. Dans ce cas, la substitution des objets de la classe enfant aux points du code où le comportement des objets de la classe parent était censé entraîner des conséquences imprévues.


Par exemple, nous ajoutons la fonctionnalité de la classe CommentBlock :


 class CommentBlock { /** @var Comment[]   */ protected $comments = []; /**       `$comments` */ public function getComment(string $key): ?Comment { return $this->comments[$key] ?? null; } } 

et nous en hériterons la classe CustomCommentBlock , dans laquelle nous profitons de toutes les possibilités de briser la dissimulation.


 class CustomCommentBlock extends CommentBlock { /** *    * *    (information hiding) *     `CommentBlock::$comments`, *     */ public function setComments(array $comments): void { $this->comments = $comments; } /** *    ,   `Comment::getKey()` * *       */ public function getComment(string $key): ?Comment { foreach ($this->comments as $comment) { if ($comment->getKey() === $key) { return $comment; } } return null; } } 

Les cas courants de violation de la dissimulation sont les suivants:


  • Les mĂ©thodes de la classe enfant rĂ©vèlent l'Ă©tat de la classe parent et permettent d'accĂ©der aux membres cachĂ©s de la classe parent. Un tel scĂ©nario n'Ă©tait probablement pas prĂ©vu lors de la conception de la classe parente, ce qui signifie que la logique de ses mĂ©thodes sera probablement violĂ©e.
    Dans l'exemple, la classe enfant fournit la méthode setter CustomCommentBlock::setComments() pour modifier la propriété CommentBlock::$comments protégée masquée dans la classe parente.
  • remplacer le comportement de la mĂ©thode de la classe parent dans la classe enfant. Parfois, les dĂ©veloppeurs perçoivent cette fonctionnalitĂ© comme un moyen de rĂ©soudre les problèmes de la classe parent en crĂ©ant des classes enfants avec un comportement modifiĂ©.
    Dans l'exemple, la méthode CommentBlock::getComment() de la classe parente s'appuie sur les clés du tableau associatif CommentBlock::$comments . Et dans la classe enfant - aux clés des commentaires eux-mêmes, accessibles via la méthode Comment::getKey() .

Le problème banane-singe-jungle


Le problème avec les langages orientés objet est qu'ils traînent dans tout leur environnement implicite. Vous vouliez juste une banane, mais en conséquence vous avez obtenu un gorille tenant cette banane, et toute la jungle en plus.

Joe Armstrong, créateur d'Erlang

Les dépendances sont toujours présentes dans l'architecture du système. Cependant, l'héritage comporte un certain nombre de facteurs de complication.


Vous avez probablement rencontré une situation où, à mesure que le produit logiciel se développe, les hiérarchies de classes se sont considérablement développées. Par exemple


 class Block { /* ... */ } class CommentBlock extends Block { /* ... */ } class PopularCommentBlock extends CommentBlock { /* ... */ } class CachedPopularCommentBlock extends PopularCommentBlock { /* ... */ } /* .... */ 

Vous héritez et héritez, mais vous ne pouvez pas décider quels membres hériter. Vous héritez de tout et de l'ensemble, héritant des membres de toutes les classes dans l'arborescence de la hiérarchie. En outre, vous obtenez une forte dépendance sur l'implémentation de la classe parente, sur la classe parente de la classe parente, etc. Et ces dépendances ne peuvent en aucun cas être affaiblies (contrairement à l'agrégation complète avec DIP ).


Sans parler du fait qu'une classe feuille dans une hiérarchie aussi profonde violera presque certainement le principe de responsabilité unique ( SRP ), en sait et en fait trop. Vous avez commencé le développement avec une classe Block simple, puis y avez ajouté des fonctions pour sélectionner des commentaires, puis des fonctionnalités de tri par popularité, une mise en cache attachée ... En conséquence, vous avez obtenu une classe avec beaucoup de responsabilités et, de plus, mal connectée ( faible cohésion )


Vous vouliez juste obtenir une banane (créer un objet feuille dans la hiérarchie) et vous ne vous souciez pas de la façon dont elle est arrivée au supermarché le plus proche (comment le comportement est mis en œuvre, dont le résultat est cet objet). Cependant, avec l'héritage, vous êtes obligé de porter la réalisation de toute la hiérarchie, à partir de la jungle elle-même. Vous devez garder à l'esprit les caractéristiques de la jungle et les nuances de leur mise en œuvre, tout en vous concentrant sur la banane.


Par conséquent, l'état de votre classe est réparti sur de nombreuses classes parentes. Vous ne pouvez résoudre ce problème qu'en limitant l'impact de l'environnement externe (la jungle) sur votre classe grâce à l'encapsulation et au masquage. Cependant, il est impossible d'y parvenir avec héritage, car l'héritage viole le principe de la dissimulation.


Comment maintenant tester les classes enfants quelque part au fond de l'arborescence hiérarchique, car leur implémentation est dispersée dans les classes parentes? Pour les tests, vous aurez besoin de toutes les classes parentes, et vous ne pouvez en aucun cas les remplacer, car vous avez un engagement non pas dans le comportement, mais dans la mise en œuvre. Étant donné que votre classe ne peut pas être facilement isolée et testée, vous hériterez de nombreux problèmes de maintenabilité, d'extensibilité et de réutilisation.


Récursivité ouverte par défaut


Cependant, la classe enfant ne dépend pas seulement de l' interface protégée du parent. Il partage également partiellement avec lui une réalisation physique, en dépend et peut l'influencer. Cela viole non seulement le principe de la dissimulation, mais rend également le comportement de la classe des enfants particulièrement déroutant et imprévisible.


Les langages orientés objet fournissent une récursivité ouverte par défaut. En PHP, la récursivité ouverte est implémentée en utilisant la pseudo-variable $this . Un appel de méthode via $this est appelé self-call dans la littérature.


L'auto-appel conduit à des appels de méthode dans la classe actuelle, ou il peut être redirigé dynamiquement vers le haut ou vers le bas de la hiérarchie d'héritage en fonction d'une liaison tardive. En fonction de cela, l' auto-appel est divisé en:


  • down-call - un appel Ă  une mĂ©thode dont l'implĂ©mentation est remplacĂ©e dans une classe enfant, plus bas dans la hiĂ©rarchie.
  • up-call - un appel Ă  une mĂ©thode dont l'implĂ©mentation est hĂ©ritĂ©e de la classe parente, plus haut dans la hiĂ©rarchie. Vous pouvez explicitement faire des appels ascendants en PHP via la construction parent::method() .

L'utilisation fréquente des appels descendants et des appels entrants dans l'implémentation de méthodes accroche encore plus étroitement les classes, ce qui rend l'architecture difficile et fragile.


Prenons un exemple. Nous implémentons la méthode getComments() dans la classe parent CommentBlock , qui retourne un tableau de commentaires.


 class CommentBlock { /* ... */ /** *        `getComment()`. * *        `CustomCommentBlock`, * ..   `CommentBlock::getComment()`  * `CustomCommentBlock::getComment()` . */ public function getComments(): array { $comments = []; foreach ($this->comments as $key => $comment) { $comments[] = $this->getComment($key); } return $comments; } } 

Cette méthode s'appuie sur la logique de CommentBlock::getComment() et CommentBlock::getComment() sur les commentaires par les clés du tableau associatif $comments . Dans le contexte de la classe CustomCommentBlock de la méthode CommentBlock::getComments() , l' appel vers le bas de la méthode CustomCommentBlock::getComment() sera exécuté. Cependant, la méthode CustomCommentBlock::getComment() a un comportement différent de celui attendu dans la classe parente. En tant que paramètre, cette méthode attend la propriété key du commentaire lui-même.


Par conséquent, CommentBlock::getComments() hérité automatiquement de la classe parente, s'est révélé incompatible avec le comportement de CustomCommentBlock::getComment() . L'appel de getComments() dans le contexte de CustomCommentBlock retournera très probablement un tableau de valeurs null .


En raison de l'engagement fort, lorsque vous apportez des modifications à une classe, vous ne pouvez pas vous concentrer uniquement sur son comportement. Vous êtes obligé de prendre en compte la logique interne de toutes les classes, de bas en haut de la hiérarchie. La liste et l'ordre de l' appel vers le bas dans la classe parente doivent être connus et documentés, ce qui viole substantiellement le principe de la dissimulation. Les détails d'implémentation font partie du contrat de classe parent.


ContrĂ´le des effets secondaires


Dans l'exemple précédent, le problème s'est manifesté en raison de la différence de logique des méthodes getComment() dans les classes parent et enfant. Cependant, il ne suffit pas de contrôler la similitude du comportement des méthodes dans la hiérarchie des classes. Des problèmes peuvent vous attendre si ces méthodes ont des effets secondaires.


La fonction avec effets secondaires (fonction avec effets secondaires ) modifie certains états du système, en plus de l'effet principal - renvoyant le résultat au point d'appel. Exemples d'effets secondaires:


  • changer les variables externes Ă  la mĂ©thode (par exemple, les propriĂ©tĂ©s des objets);
  • changer les variables statiques locales Ă  la mĂ©thode;
  • interaction avec des services externes.

Ces effets secondaires font donc également partie de la mise en œuvre , qui ne peuvent pas non plus être efficacement cachés dans le processus d'héritage.


Imaginez que CommentBlock devions inclure la méthode viewComment() dans la classe CommentBlock pour obtenir une représentation textuelle de l'un des commentaires.


 class CommentBlock { /** @var Comment[]   */ protected $comments = []; /**         */ public function viewComment(string $key): string { return $this->comments[$key]->view(); } } 

Ajoutez un effet secondaire à la classe enfant et spécifiez son objectif. Nous implémentons la classe CountingCommentBlock , qui complète CommentBlock possibilité de compter les vues des commentaires individuels dans le cache. Laissez la classe accepter l' injection d'un cache compatible PSR-16 dans le constructeur ( injection de constructeur ) via l'interface CounterInterface (qui, cependant, a finalement été exclue du PSR-16). Nous utiliserons la méthode increment() pour incrémenter atomiquement la valeur du compteur dans le cache.


 class CountingCommentBlock extends CommentBlock { /** @var CounterInterface  */ private $cache; public function __construct(CounterInterface $cache) { $this->cache = $cache; } /**        */ public function viewComment(string $key): string { $this->cache->increment($key); return parent::viewComment($key); } } 

Tout fonctionne bien. Cependant, à un moment donné, il est décidé d'ajouter la fonction viewComments() pour former une représentation textuelle de tous les commentaires dans le bloc. Cette méthode est ajoutée à la classe de base CommentBlock et, en un coup d'œil, l'héritage de l'implémentation de cette méthode par toutes les classes enfants semble très pratique et évite d'écrire du code supplémentaire dans les classes enfants.


 class CommentBlock { /* ... */ /**           */ public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $comment->view(); } return $view; } } 

Cependant, la classe parent ne sait rien des fonctionnalités d'implémentation des classes enfants. viewComments() (responsibility) CountingCommentBlock – .


:


 $commentBlock = new CountingCommentBlock(new SomeCache()); /* ... */ $commentBlock->viewComments(); 

. , .


« » . , viewComments() ( ).



, . , , , . – « » (" Fragile base class "). «- » – (fragility).


, ? . , CommentBlock , .


 class CommentBlock { /** @var Comment[]   */ protected $comments = []; /**         */ public function viewComment(string $key): string { return $this->comments[$key]->view(); } /**           */ public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $comment->view(); } return $view; } } 

CountingCommentBlock .


 class CountingCommentBlock extends CommentBlock { /** @var CounterInterface  */ private $cache; public function __construct(CounterInterface $cache) { $this->cache = $cache; } /**        */ public function viewComment(string $key): string { $this->cache->increment($key); return parent::viewComment($key); } /**        */ public function viewComments(): string { foreach ($this->comments as $key => $comment) { $this->cache->increment($key); } return parent::viewComments(); } } 

CommentBlock::viewComments() :


 $view .= $comment->view(); 

, viewComment() , – . . viewComment() viewComments() . , CommentBlock::viewComment() CommentBlock::viewComments() :


 class CommentBlock { /* ... */ public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); //  `$comment->view()` } return $view; } } 

CommentBlock , , . CommentBlock – , «». .


, . CountingCommentBlock . :


 $commentBlock = new CountingCommentBlock(new SomeCache()); /* ... */ $commentBlock->viewComments(); 

:


 CountingCommentBlock::viewComments() -> CommentBlock::viewComments() -> (n ) CountingCommentBlock::viewComment() 

: CountingCommentBlock::viewComments() CountingCommentBlock::viewComment() . C'est-à-dire – . CountingCommentBlock , , !


, protected . , . . , .


. , . , . , , « - ».


, . , « ». « » $this , private , .


, . , , . PHP ( public , protected , private ) final .


final


PHP . , , .. , , , . , final .


PHP 5 final , , . , .

#1.
...
#2
...

: , .

PHP, « final »


PHP . . final , .


, final – PHP, . , PHP : typehints , ..


final , , . C'est-Ă -dire , . .


, , private , . , .


, , , API. , «» , . «» - . «» , .


API « ». , .


, final .


final


« »


– . , public protected .


, , . PHP ( -) . – , , , .


, () . « » ( Template method ).


, :


  • . , () . . final . – .
  • . . , . () .

, abstract , , final , . , , . , .. final .


. , , . , . . .., , - , , , . :


  • abstract ;
  • final , , .

. « » abstract final .


CommentBlock .


 abstract class CommentBlock { /**   */ protected $comments = []; /**         */ abstract public function viewComment(string $key): string; /**           */ final public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); } return $view; } } 

SimpleCommentBlock :


 final class SimpleCommentBlock extends CommentBlock { public function viewComment(string $key): string { return $this->comments[$key]->view(); } } 

, , :


 final class CountingCommentBlock extends CommentBlock { /**  */ private $cache; public function __construct(CounterInterface $cache) { $this->cache = $cache; } /**        */ public function viewComment(string $key): string { $this->cache->increment($key); return $this->comments[$key]->view(); } } 

«», . final final .


. , . , « » down-call , , . .


, , . , .



- , . extends , implements .


- . CommentBlock :


 interface CommentBlock { /**         */ public function viewComment(string $key): string; /**           */ public function viewComments(): string; } 

:


 final class SimpleCommentBlock implements CommentBlock { /**   */ private $comments = []; public function viewComment(string $key): string { return $this->comments[$key]->view(); } public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); } return $view; } } 

, :


 final class CountingCommentBlock implements CommentBlock { /**   */ private $comments = []; /**  */ private $cache; public function __construct(CounterInterface $cache) { $this->cache = $cache; } /**        */ public function viewComment(string $key): string { $this->cache->increment($key); return $this->comments[$key]->view(); } public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); } return $view; } } 

implements - ( association ) .


-, . - . final , : , ..


. . , public . implements , () , , ( ISP ). , .


/ ( OCP )? final implements – PHP .


, , , . , implements , . .


final , . . – , implements final .


« », . implements , final .


, « », , . – SimpleCommentBlock CountingCommentBlock viewComments() .



, viewComments() , , , , . , . ( decorator pattern ). , « », – final , implements .


, CommentBlock , .


 interface CommentBlock { /**      */ public function getCommentKeys(): array; /**         */ public function viewComment(string $key): string; /**           */ public function viewComments(): string; } 

, getCommentKeys() . . , protected , CommentBlock .


SimpleCommentBlock - «» . , , implements final .


 final class SimpleCommentBlock implements CommentBlock { /**   */ private $comments = []; public function getCommentKeys(): array { return array_keys($this->comments); } public function viewComment(string $key): string { return $this->comments[$key]->view(); } public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); } return $view; } } 

CountingCommentBlock - «» – OCP . CountingCommentBlock : CommentBlock .


 final class CountingCommentBlock implements CommentBlock { /**  CommentBlock */ private $commentBlock; /**  */ private $cache; public function __construct(CommentBlock $commentBlock, CounterInterface $cache) { $this->commentBlock = $commentBlock; $this->cache = $cache; } public function getCommentKeys(): array { return $this->commentBlock->getCommentKeys(); } public function viewComment(string $key): string { $this->cache->increment($key); return $this->commentBlock->viewComment($key); } public function viewComments() : string { $commentKeys = $this->getCommentKeys(); foreach ($commentKeys as $commentKey) { $this->cache->increment($commentKey); } return $this->commentBlock->viewComments(); } } 

- CountingCommentBlock . , viewComment() . ( forwarding methods ).


, « ». getCommentKeys() . , «» , , .


, . , - «» . , – ( , SOLID) ( , , ).


. SimpleCommentBlock CountingCommentBlock CommentBlock , . -, .


, , final , CommentBlock .


SimpleCommentBlock CountingCommentBlock - « », . « » , . , « ». – .


, final , . , «--» – . DIP , .


: SimpleCommentBlock – ; CountingCommentBlock – SimpleCommentBlock , (). C'est-à-dire , – SRP . , , ( cohesion ) .


« » – (, , ), , . , .


.


viewRandomComment() SimpleCommentBlock - CountingCommentBlock . , – viewRandomComment() . CountingCommentBlock::viewRandomComment() .


, viewComments() SimpleCommentBlock . CountingCommentBlock SimpleCommentBlock , .


 final class SimpleCommentBlock implements CommentBlock { /* ... */ /**         */ public function viewRandomComment(): string { $key = array_rand($this->comments); return $this->comments[$key]->view(); } /**      */ public function viewComments() : string { $view = ''; foreach ($this->comments as $key => $comment) { /*    `$this->viewComment()`      */ $view .= $this->comments[$key]->view(); } return $view; } } 


: , , , , , .. .


PHP , , final , . , , , . , . . – , – .


, . PHPDoc , . : .


, , (.. , public protected ). PSR-19: PHPDoc tags , , . PHPDoc , « -».


JavaDoc @implSpec , . , PHPDoc API , .. public . @implSpec , API . , protected .


 class CommentBlock { /* ... */ /** *         *       *  (,    ) * * @implSpec      `$key` *    `$this->comments`      `view()` * * @param string $key   * @return string    */ public function viewComment(string $key): string { return $this->comments[$key]->view(); } } 

PHPDoc @implSpec . $key . – view() .


, @implSpec :


  • ( , , , ..).
  • parent::method() .

, «» . , self-call ( $this ) , , :


  • $this ;
  • ;
  • .

 class CommentBlock { /* ... */ /** *           * * @implSpec    `$this->comments` *        `$this->viewComment()`. *       . * * @return string     */ final public function viewComments(): string { $view = ''; foreach ($this->comments as $key => $comment) { $view .= $this->viewComment($key); } return $view; } } 

, viewComments() final . viewComment() , . :


  • viewComment() viewComments() ;
  • viewComments() viewComment() .

, . , , . « », CommentBlock CountingCommentBlock , . viewComment() viewComments() , , .


, , . PHPDoc API : , , .


, , , , , , «». – . PHPDoc , .


, , . – final , . . – « ».


:


  • . ;
  • final . .

, , final . final , . , . , ?


final . , , . , , final , .


final


« », , , , , . , protected . , final – , , , public , .


, , . , , , . – final .


, final ? Non. - , Opensource , , . IDE . , .


, , final . final , . .


final , . public ? – , ?


, . , final , , protected .


final , code review ( code review ? ;). , .


  • final ? ? ?
  • final ? ? ?

. , , code review . .


. final PHP5 . , , ( , ) .


, , . « » , . , C# virtual , – override . PHP final extandable , « final », extend .



, – . . . .


. , . , . final , , -. , , implements .


, , final .


  1. final :


     final class SimpleCommentBlock { /* ... */ public function getCommentKeys(): array { /* ... */ } public function viewComment(string $key): string { /* ... */ } public function viewComments(): string { /* ... */ } } 

  2. . , final . , . .


    , public . , , .


    , .


     interface CommentBlock { public function getCommentKeys(): array; public function viewComment(string $key): string; public function viewComments(): string; } 

  3. .


     final class SimpleCommentBlock implements CommentBlock { /* ... */ } 

  4. -, . - CommentBlock . - .


     final class CountingCommentBlock implements CommentBlock { /* ... */ private $commentBlock; public function __construct(CommentBlock $commentBlock /* ,... */) { $this->commentBlock = $commentBlock; } /* ... */ } 


, . , final , , - . , final .


final , . , «». , . final .


final


final – ( PHPUnit, Mockery ) «» ( test doubles ). , .


, :


 final class SimpleCommentBlockTest extends TestCase { public function testCreatingTestDouble(): void { $mock = $this->createMock(SimpleCommentBlock::class); } } 

:


 Class "SimpleCommentBlock" is declared "final" and cannot be mocked. 

, « » PHPUnit , :


 class Mock_SimpleCommentBlock_591bc3f3 extends SimpleCommentBlock { /* ... */ } 

«» . -, , .. . , -, , PHPUnit . , .


final . : . .



– «» . , ( Post , Comment ) - ( value object ) ( stable ) . . classical TDD ( mockist TDD )


, ( volatile ) . , , «» – , . , «» , . ( DIP ), , «» .


, . C'est-Ă -dire :


 interface CommentBlock { /* ... */ } 

:


 final class SimpleCommentBlock implements CommentBlock { /* ... */ } 

«» :


 final class CommentBlockTest extends TestCase { public function testCreatingTestDouble(): void { $mock = $this->createMock(CommentBlock::class); } } 

«» , . :


  • «» ;
  • ;
  • «» , .. DIP .


, «» – , -. , ( , ). , « » « ».


, final .


– -. -. , -, . Mockery .


 class SimpleCommentBlockTest extends TestCase { public function testCreatingProxyDouble() { /*    */ $simpleCommentBlock = new SimpleCommentBlock(); /*  - */ $proxy = Mockery::mock($simpleCommentBlock); /*    */ $proxy->shouldReceive('viewComment') ->andReturn('text'); /*     */ $this->assertEquals('text', $proxy->viewComment('1')); /* `$proxy`     `SimpleCommentBlock`  */ $this->assertNotInstanceOf(SimpleCommentBlock::class, $proxy); } } 

– - . , instanceof . ( type declarations ), - .


« » – PHP, , . Bypass Finals , final . composer final :


 public function testUsingBypassFinals(): void { /*   `final` */ BypassFinals::enable(); $mock = $this->createMock(SimpleCommentBlock::class); } 

final


, PHP . « » , final . , IDE final .


PHPStorm


PHPStorm . File | Settings | Editor | File and Code Templates Files PHP Class . final .


Modification du modèle intégré `PHP Class`


File | New | PHP Class :


 final class SimpleCommentBlock { } 

, . . – .


PHPStorm Refactor | Extract | Interface . , . ( Replace class reference with interface where possible ) PHPDoc ( Move PHPDoc ).


:


 interface CommentBlock { /** PHPDoc */ public function viewComment(string $key): string; } 

:


 final class SimpleCommentBlock implements CommentBlock { public function viewComment(string $key): string { /* ... */ } } 

File | New | PHP Class -, . :


 final class CountingCommentBlock implements CommentBlock { /** @var CommentBlock */ private $commentBlock; } 

Code | Generate | Constructor . .


 final class CountingCommentBlock implements CommentBlock { /* ... */ public function __construct(CommentBlock $commentBlock) { $this->commentBlock = $commentBlock; } } 

– . Code | Generate | Implement Methods . , . PHPStorm «» , IntelliJ IDEA ReSharper .


 final class CountingCommentBlock implements CommentBlock { /* ... */ /** * @inheritDoc */ public function viewComment(string $key): string { // TODO: Implement viewComment() method. } } 

PHPDoc . .


PHPStan


, . , , , final . ( ).


PHPStan . « » . PHPStan . .


FinalRule localheinz/phpstan-rules . PHPStan\Rules\Rule processNode() .


. , FinalRule « » allowAbstractClasses . , , classesNotRequiredToBeAbstractOrFinal .


, composer :


 composer require --dev phpstan/phpstan composer require --dev localheinz/phpstan-rules 

FinalRule phpstan.neon :


 services: - class: Localheinz\PHPStan\Rules\Classes\FinalRule arguments: allowAbstractClasses: true classesNotRequiredToBeAbstractOrFinal: [] tags: - phpstan.rules.rule 

( max ):


 vendor/bin/phpstan -lmax analyse src 

:


  ------ ------------------------------------------------------------------------ Line CommentBlock.php ------ ------------------------------------------------------------------------ 10 Class CommentBlock is neither abstract nor final. ------ ------------------------------------------------------------------------ 

JSON Continuous Integration .


Conclusion


, : final ! IDE, .


, , . – SOLID . , , .


, . :


  • ;
  • final , , , ;
  • .

, - - . . . , , , , . , . ?


, – , . final . ( cognitive load ) – . – .


: final . . – . . . , . , .


final . ( SOLID ) ( fragile ) . , .

Source: https://habr.com/ru/post/fr482154/


All Articles