¿Por qué limitar la herencia con final?

Probablemente escuchó esta famosa declaración de GoF: "Prefiera la composición a la herencia de clase". Y luego, por regla general, se reflexionaron mucho sobre cómo la herencia determinada estáticamente no es tan flexible en comparación con una composición dinámica.


La flexibilidad es, por supuesto, una característica de diseño útil. Sin embargo, al elegir una arquitectura, estamos interesados ​​principalmente en la capacidad de mantenimiento, la capacidad de prueba, la legibilidad del código y la reutilización de módulos. Entonces, con estos criterios de buen diseño, la herencia también es un problema. “Y ahora, ¿no usas la herencia en absoluto?” - preguntas.


Veamos cómo una fuerte dependencia entre clases a través de la herencia puede hacer que la arquitectura de su sistema sea demasiado rígida y frágil. Y por qué usar una de las palabras clave más misteriosas y escurridizas del código: final . Las ideas formuladas se demuestran utilizando un simple ejemplo transversal. Al final del artículo, hay trucos y herramientas para un trabajo conveniente con las clases final .


El frágil problema de la clase base


El frágil problema de la clase base


Uno de los criterios principales para una buena arquitectura es el acoplamiento flexible , que caracteriza el grado de interconexión entre los módulos de software. No es por nada que el compromiso débil es parte de la lista de patrones GRASP que describen los principios básicos para compartir la responsabilidad entre las clases.


El compromiso débil tiene muchas ventajas.


  • Al aflojar las dependencias entre los módulos de software, facilita el mantenimiento y el soporte del sistema al crear una arquitectura más flexible.
  • Existe la posibilidad de un desarrollo paralelo de módulos acoplados libremente sin el riesgo de interrumpir su funcionamiento.
  • La lógica de la clase se vuelve más obvia, se hace más fácil usar la clase correctamente y para su propósito previsto, y es difícil de usar, incorrectamente.

Tradicionalmente, las dependencias en un sistema se entienden principalmente como conexiones entre el objeto usado (servicio) y el objeto que usa (cliente). Dicha relación modela la relación de agregación cuando el servicio es "parte del" cliente ( relación tiene ) y el cliente transfiere la responsabilidad del comportamiento al servicio incorporado en él. El principio de inversión de dependencia ( DIP ) desempeña un papel clave en el debilitamiento de la relación entre el cliente y el servicio, que propone convertir una dependencia directa entre módulos en una dependencia recíproca de módulos en una abstracción común.


Sin embargo, también puede mejorar significativamente la arquitectura de la aplicación aflojando las dependencias dentro del marco de una relación is-a . La relación de herencia predeterminada crea un acoplamiento estrecho , la más fuerte entre todas las formas posibles de dependencias y, por lo tanto, debe usarse con mucho cuidado.


Fuerte vínculo con la herencia


Fuerte vínculo con la herencia


La cantidad de código compartido entre las clases padre e hijo es muy grande. Este problema comienza a manifestarse especialmente cuando se abusa del concepto de herencia, utilizando la herencia exclusivamente para la reutilización de código horizontal y no para crear subclases especializadas. Porque la herencia es la forma más fácil de reutilizar el código. ¡Solo necesita escribir extends ParentClass y eso es todo! Después de todo, es mucho más simple que la agregación, la inyección de dependencia (DI), la asignación de interfaz.


La reducción de la vinculación de clase en la jerarquía de herencia se logra tradicionalmente mediante el uso de modificadores restrictivos del ámbito ( private , protected ). Incluso existe la opinión de que las propiedades de clase deben declararse exclusivamente con el modificador private . Y el modificador protected debe aplicarse con mucho cuidado y solo a los métodos, porque Fomenta las dependencias entre las clases padre e hijo.


Sin embargo, los problemas de herencia no solo están en las propiedades y métodos de ocultación, sino que son mucho más profundos. Una gran cantidad de literatura sobre arquitectura de aplicaciones, incluido el clásico libro GoF, está plagada de escepticismo sobre la herencia y sugiere buscar diseños más flexibles. ¿Pero es solo flexibilidad? Propongo a continuación para sistematizar los problemas de herencia, y luego pensar en cómo evitarlos.


Tomemos como "conejo experimental" la clase más simple de bloque de comentarios con una serie de comentarios en su interior. Clases similares con una colección dentro se encuentran en grandes cantidades en cualquier proyecto.


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

Los ejemplos a continuación se simplifican deliberadamente en el estilo KISS para mostrar las ideas formuladas en el código. Puede encontrar ejemplos de código del artículo y una anotación detallada sobre su uso en este repositorio .


Problemas de herencia


Comencemos con el problema más obvio, que se da principalmente en la literatura sobre arquitectura.


La herencia viola el principio de ocultamiento.


Dado que la subclase tiene acceso a los detalles de implementación de la clase principal, a menudo se dice que la herencia viola la encapsulación.

GoF, Patrones de diseño

Aunque el libro clásico, The Gangs of Four, trata sobre violaciones de encapsulación, sería más exacto decir que "la herencia viola el principio de ocultamiento" . Después de todo, la encapsulación es una combinación de datos con métodos diseñados para su procesamiento. Pero el principio de ocultación solo garantiza la restricción del acceso de algunos componentes del sistema a los detalles de la implementación de otros.


Seguir el principio de ocultamiento en la arquitectura permite garantizar la participación de los módulos a través de una interfaz estable. Y si la clase permite la herencia, proporciona automáticamente los siguientes tipos de interfaces estables:


  • interfaz pública utilizada por todos los clientes de esta clase;
  • interfaz protegida utilizada por todas las clases secundarias.

Es decir la clase secundaria tiene un arsenal de capacidades mucho mayor que el que ofrece la API pública. Por ejemplo, puede afectar el estado interno de la clase primaria oculta en sus propiedades protected .


A menudo, una clase secundaria no necesita acceso a todos los elementos de la clase primaria a los que se puede acceder mediante la herencia, sin embargo, no puede proporcionar acceso selectivo a miembros protected de clases para algunas de las subclases. La clase secundaria comienza a depender de la interfaz protegida de la clase primaria.


La razón principal de la herencia de clase, con mayor frecuencia, es expandir la funcionalidad de la clase padre reutilizando su implementación. Si el acceso inicial a la implementación de la clase principal a través de la interfaz protegida no presagiaba problemas, entonces, a medida que el sistema se desarrolla, el programador comienza a usar este acceso en los métodos de la clase secundaria y fortalece el vínculo en la jerarquía.


La clase principal ahora se ve obligada a mantener la estabilidad no solo de la interfaz pública , sino también de la interfaz protegida , ya que cualquier cambio en ella conducirá a problemas en el trabajo de las clases secundarias. Sin embargo, es imposible negarse a usar miembros de la clase protected . Si la interfaz protegida coincidirá completamente con la interfaz pública externa, es decir la clase padre solo usará miembros public y private , luego la herencia generalmente no tiene sentido.


De hecho, la palabra clave protected realidad no proporciona protección para los miembros de la clase. Para obtener acceso a dichos miembros, es suficiente heredar de la clase, y dentro del marco de la clase secundaria, tiene todas las oportunidades de violar el principio de ocultación. Se vuelve muy fácil usar una clase incorrectamente, que es uno de los primeros signos de una arquitectura pobre.


Usar una clase a través de una interfaz protegida


Violación del principio de ocultación a través de una interfaz protegida


Más importante aún, los elementos encapsulados (constantes, propiedades, métodos) se vuelven no solo legibles e invocados en la clase secundaria, sino que también pueden anularse. Tal oportunidad está cargada de un peligro oculto: debido a tales cambios, el comportamiento de los objetos de la clase secundaria puede volverse incompatible con los objetos de la clase primaria. En este caso, la sustitución de los objetos de la clase secundaria en aquellos puntos del código donde se suponía que el comportamiento de los objetos de la clase primaria tendría consecuencias imprevistas.


Por ejemplo, agregamos la funcionalidad de la clase CommentBlock :


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

y CustomCommentBlock de ella la clase CustomCommentBlock , en la que aprovechamos todas las posibilidades para romper la ocultación.


 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; } } 

Los casos comunes de violaciones de ocultamiento son los siguientes:


  • Los métodos de la clase secundaria revelan el estado de la clase primaria y proporcionan acceso a los miembros ocultos de la clase primaria. Tal escenario probablemente no se previó al diseñar la clase padre, lo que significa que la lógica de sus métodos probablemente se violará.
    En el ejemplo, la clase secundaria proporciona el CustomCommentBlock::setComments() establecimiento CustomCommentBlock::setComments() para modificar la CommentBlock::$comments protegida CommentBlock::$comments oculta en la clase primaria.
  • anulando el comportamiento del método de la clase primaria en la clase secundaria. A veces, los desarrolladores perciben esta característica como una forma de resolver los problemas de la clase primaria creando clases secundarias con comportamiento modificado.
    En el ejemplo, el CommentBlock::getComment() en la clase padre se basa en las claves de la matriz asociativa CommentBlock::$comments . Y en la clase secundaria, a las claves de los comentarios mismos, accesibles a través del método Comment::getKey() .

El problema de la jungla, el mono y el plátano


El problema con los lenguajes orientados a objetos es que arrastran todo su entorno implícito. Solo querías un plátano, pero como resultado tienes un gorila sosteniendo este plátano, y toda la jungla además.

Joe Armstrong, creador de Erlang

Las dependencias siempre están presentes en la arquitectura del sistema. Sin embargo, la herencia conlleva una serie de factores complicados.


Probablemente haya encontrado una situación en la que, a medida que se desarrolla el producto de software, las jerarquías de clase han crecido significativamente. Por ejemplo


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

Hereda y hereda, pero no puede decidir qué miembros heredará. Usted hereda todo y todo, heredando miembros de todas las clases en todo el árbol de jerarquía. Además, obtiene una fuerte dependencia de la implementación de la clase principal, de la clase principal de la clase principal, etc. Y estas dependencias no se pueden debilitar de ninguna manera (a diferencia de la agregación completa con DIP ).


Sin mencionar el hecho de que una clase hoja en una jerarquía tan profunda casi seguramente violará el principio de responsabilidad única ( SRP ), saber y hacer demasiado. Comenzó el desarrollo con una clase simple de Block , luego le agregó funciones para seleccionar comentarios, luego características para ordenar por popularidad, almacenamiento en caché adjunto ... Como resultado, obtuvo una clase con muchas responsabilidades y, además, poco conectada ( baja cohesión )


Solo quería obtener un plátano (crear un objeto hoja en la jerarquía) y no le importa cómo llegó al supermercado más cercano (cómo se implementa el comportamiento, cuyo resultado es este objeto). Sin embargo, con la herencia, se ve obligado a llevar a cabo la realización de toda la jerarquía, comenzando desde la jungla misma. Debe tener en cuenta las características de la jungla y los matices de su implementación, mientras desea centrarse en el plátano.


Como resultado, el estado de su clase se extiende sobre muchas clases para padres. Puede resolver este problema solo limitando el impacto del entorno externo (la jungla) en su clase a través de la encapsulación y la ocultación. Sin embargo, es imposible lograr esto con la herencia, porque La herencia viola el principio de ocultamiento.


¿Cómo probar ahora las clases secundarias en algún lugar profundo del árbol de jerarquía, porque su implementación está dispersa entre las clases principales? Para las pruebas, necesitará todas las clases principales y no puede anularlas de ninguna manera, porque Tienes un compromiso no en el comportamiento, sino en la implementación. Como su clase no puede aislarse y probarse fácilmente, heredará muchos problemas, con capacidad de mantenimiento, extensibilidad y reutilización.


Recurrencia abierta predeterminada


Sin embargo, la clase secundaria no solo depende de la interfaz protegida del padre. También comparte parcialmente con él una realización física, depende de ella y puede influir en ella. Esto no solo viola el principio de ocultamiento, sino que también hace que el comportamiento de la clase infantil sea particularmente confuso e impredecible.


Los lenguajes orientados a objetos proporcionan recursividad abierta por defecto. En PHP, la recursión abierta se implementa utilizando la pseudo-variable $this . Una llamada al método a través de $this se llama auto-llamada en la literatura.


La auto-llamada conduce a llamadas a métodos en la clase actual, o se puede redirigir dinámicamente hacia arriba o hacia abajo en la jerarquía de herencia en función del enlace tardío. Dependiendo de esto, la auto-llamada se divide en:


  • down-call : una llamada a un método cuya implementación se anula en una clase secundaria, de menor jerarquía.
  • up-call : una llamada a un método cuya implementación se hereda de la clase primaria, más arriba en la jerarquía. Puede hacer explícitamente una llamada ascendente en PHP a través de la construcción parent::method() .

El uso frecuente de down-call y up-call en la implementación de métodos engancha las clases aún más de cerca, haciendo que la arquitectura sea resistente y frágil.


Tomemos un ejemplo. Implementamos el método getComments() en la clase CommentBlock , que devuelve una serie de comentarios.


 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; } } 

Este método se basa en la lógica de CommentBlock::getComment() e CommentBlock::getComment() sobre los comentarios mediante las claves de la matriz asociativa $comments . En el contexto de la clase CustomCommentBlock del método CustomCommentBlock::getComment() , se ejecutará la CustomCommentBlock::getComment() método CustomCommentBlock::getComment() . Sin embargo, el CustomCommentBlock::getComment() tiene un comportamiento diferente al esperado en la clase primaria. Como parámetro, este método espera la propiedad key del comentario en sí.


Como resultado, CommentBlock::getComments() heredado automáticamente de la clase principal, resultó ser incompatible en el comportamiento con CustomCommentBlock::getComment() . Llamar a getComments() en el contexto de CustomCommentBlock probablemente devolverá una matriz de valores null .


Debido al fuerte compromiso, al hacer cambios en una clase, no puede enfocarse solo en su comportamiento. Usted está obligado a tener en cuenta la lógica interna de todas las clases, arriba y abajo de la jerarquía. La lista y el orden de la llamada descendente en la clase principal deben ser conocidos y documentados, lo que viola sustancialmente el principio de ocultación. Los detalles de implementación se convierten en parte del contrato de la clase principal.


Control de efectos secundarios.


En el ejemplo anterior, el problema se manifestó debido a la diferencia en la lógica de los métodos getComment() en las clases padre e hijo. Sin embargo, no es suficiente controlar la similitud del comportamiento de los métodos en la jerarquía de clases. Los problemas pueden esperar si estos métodos tienen efectos secundarios.


La función con efectos secundarios (función con efectos secundarios ) cambia algún estado del sistema, además del efecto principal, devolviendo el resultado al punto de llamada. Ejemplos de efectos secundarios:


  • cambio de variables externas al método (por ejemplo, propiedades de objeto);
  • cambio de variables estáticas locales al método;
  • interacción con servicios externos.

Entonces, estos efectos secundarios también son esa parte de la implementación , que tampoco puede ocultarse de manera efectiva en el proceso de herencia.


Imagine que necesitamos incluir el método viewComment() en la clase CommentBlock para obtener una representación textual de uno de los comentarios.


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

Agregue un efecto secundario a la clase secundaria y especifique su propósito. Implementamos la clase CountingCommentBlock , que complementa CommentBlock capacidad de contar vistas de comentarios individuales en el caché. Deje que la clase acepte la inyección de un caché compatible con PSR-16 en el constructor ( inyección de constructor ) a través de la interfaz CounterInterface (que, sin embargo, finalmente se excluyó de PSR-16). Usaremos el método increment() para incrementar atómicamente el valor del contador en el caché.


 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); } } 

Todo funciona bien Sin embargo, en algún momento, se decide agregar la función viewComments() para formar una representación textual de todos los comentarios en el bloque. Este método se agrega a la clase base CommentBlock y, de un vistazo, heredar la implementación de este método por todas las clases secundarias parece muy conveniente y evita escribir código adicional en las clases secundarias.


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

Sin embargo, la clase principal no sabe nada sobre las características de implementación de las clases secundarias. 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() . Es decir – . 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 , , . Es decir , . .


, , 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 – ; CountingCommentBlockSimpleCommentBlock , (). Es decir , – 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 ? No - , 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 ), , «» .


, . Es decir :


 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 .


`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 .


Conclusión


, : final ! IDE, .


, , . – SOLID . , , .


, . :


  • ;
  • final , , , ;
  • .

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


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


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


final . ( SOLID ) ( fragile ) . , .

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


All Articles