Por que limitar a herança com final?

Você provavelmente ouviu esta famosa declaração do GoF: "Prefira composição à herança de classe". E então, como regra geral, continuamos pensando em como a herança determinada estaticamente não é tão flexível em comparação com uma composição dinâmica.


É claro que a flexibilidade é um recurso de design útil. No entanto, ao escolher uma arquitetura, estamos interessados ​​principalmente em capacidade de manutenção, testabilidade, legibilidade de código e reutilização de módulos. Portanto, com esses critérios de bom design, a herança também é um problema. “E agora, não use herança?” - você pergunta.


Vejamos como uma forte dependência entre classes por herança pode tornar sua arquitetura de sistema excessivamente rígida e frágil. E por que usar uma das palavras-chave mais misteriosas e evasivas do código - final . As idéias formuladas são demonstradas usando um exemplo simples e transversal. No final do artigo, existem truques e ferramentas para um trabalho conveniente com final aulas final .


O problema da classe base


O frágil problema da classe base


Um dos principais critérios para uma boa arquitetura é o acoplamento flexível , que caracteriza o grau de interconexão entre os módulos de software. Não é à toa que o engajamento fraco faz parte da lista de padrões do GRASP que descreve os princípios básicos para compartilhar responsabilidades entre as classes.


O envolvimento fraco tem muitas vantagens.


  • Ao afrouxar as dependências entre os módulos de software, você facilita a manutenção e o suporte do sistema, criando uma arquitetura mais flexível.
  • Existe a possibilidade de desenvolvimento paralelo de módulos fracamente acoplados sem risco de interromper seu funcionamento.
  • A lógica da classe se torna mais óbvia, torna-se mais fácil usar a classe corretamente e para a finalidade a que se destina, e é difícil usá-la - errado.

Tradicionalmente, as dependências em um sistema são entendidas principalmente como conexões entre o objeto usado (serviço) e o objeto em uso (cliente). Esse relacionamento modela o relacionamento de agregação quando o serviço é "parte" do cliente ( relacionamento com um relacionamento ), e o cliente transfere a responsabilidade pelo comportamento para o serviço incorporado nele. Um papel fundamental no enfraquecimento do relacionamento entre o cliente e o serviço é desempenhado pelo princípio de inversão de dependência ( DIP ), que propõe a conversão de uma dependência direta entre módulos em uma dependência recíproca de módulos em uma abstração comum.


No entanto, você também pode melhorar significativamente a arquitetura do aplicativo, afrouxando as dependências na estrutura de um relacionamento é-um . O relacionamento de herança padrão cria um acoplamento rígido , o mais forte entre todas as formas possíveis de dependências e, portanto, deve ser usado com muito cuidado.


Forte vínculo com relação à herança


Forte vínculo com relação à herança


A quantidade de código compartilhado entre as classes pai e filho é muito grande. Esse problema começa a se manifestar especialmente fortemente quando o conceito de herança é abusado - usando a herança exclusivamente para reutilização de código horizontal, e não para criar subclasses especializadas. Porque a herança é a maneira mais fácil de reutilizar o código. Você só precisa escrever extends ParentClass e é isso! Afinal, é muito mais simples que agregação, injeção de dependência (DI), alocação de interface.


A redução do vínculo de classe na hierarquia de herança é tradicionalmente alcançada usando modificadores restritivos do escopo ( private , protected ). Existe até uma opinião de que as propriedades de classe devem ser declaradas exclusivamente com o modificador private . E o modificador protected deve ser aplicado com muito cuidado e apenas aos métodos, porque incentiva as dependências entre as classes pai e filho.


No entanto, os problemas de herança não estão apenas ocultando propriedades e métodos, eles são muito mais profundos. Muita literatura sobre arquitetura de aplicativos, incluindo o livro clássico do GoF, é repleta de ceticismo sobre herança e oferece olhar para projetos mais flexíveis. Mas é apenas flexibilidade? Proponho abaixo sistematizar os problemas de herança e depois pensar em como evitá-los.


Vamos tomar como "coelho experimental" a classe mais simples de bloco de comentários com uma matriz de comentários dentro. Classes semelhantes com uma coleção interna são encontradas em grandes números em qualquer projeto.


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

Os exemplos abaixo são deliberadamente simplificados no estilo KISS , a fim de mostrar as idéias formuladas no código. Você pode encontrar exemplos de código no artigo e uma anotação detalhada sobre seu uso neste repositório .


Problemas de herança


Vamos começar com o problema mais óbvio, que é dado principalmente na literatura sobre arquitetura.


A herança viola o princípio da ocultação


Como a subclasse tem acesso aos detalhes de implementação da classe pai, costuma-se dizer que a herança viola o encapsulamento.

GoF, Padrões de Design

Embora o livro clássico, The Gangs of Four, lide com violações de encapsulamento, seria mais preciso dizer que "a herança viola o princípio da ocultação" . Afinal, o encapsulamento é uma combinação de dados com métodos projetados para seu processamento. Mas o princípio da ocultação apenas fornece uma restrição de acesso de alguns componentes do sistema aos detalhes da implementação de outros.


Seguir o princípio da ocultação na arquitetura permite garantir o envolvimento dos módulos através de uma interface estável. E se a classe permitir herança, ela fornecerá automaticamente os seguintes tipos de interfaces estáveis:


  • interface pública usada por todos os clientes desta classe;
  • interface protegida usada por todas as classes filho.

I.e. a classe filho tem um arsenal de recursos muito maior do que a API pública fornece. Por exemplo, isso pode afetar o estado interno da classe pai oculto em suas propriedades protected .


Freqüentemente, uma classe filho não precisa acessar todos os elementos da classe pai disponíveis na estrutura de herança; no entanto, você não pode fornecer seletivamente acesso a membros da classe protected para algumas das subclasses. A classe filho começa a depender da interface protegida da classe pai.


O principal motivo da herança de classe, na maioria das vezes, é expandir a funcionalidade da classe pai reutilizando sua implementação. Se o acesso inicial à implementação da classe pai via interface protegida não pressagiar problemas, à medida que o sistema se desenvolve, o programador começa a usar esse acesso nos métodos da classe filho e a fortalecer o vínculo na hierarquia.


A classe pai agora é forçada a manter a estabilidade não apenas da interface pública , mas também da interface protegida , pois quaisquer alterações nela levarão a problemas no trabalho das classes filho. No entanto, é impossível recusar o uso de membros da classe protected . Se a interface protegida corresponder completamente à interface pública externa, ou seja, a classe pai usará apenas membros public e private , então a herança geralmente não faz sentido.


De fato, a palavra-chave protected na verdade não fornece proteção para os membros da classe. Para obter acesso a esses membros, basta herdar da classe e, dentro da estrutura da classe filho, você tem toda a oportunidade de violar o princípio da ocultação. Torna-se muito fácil usar uma classe incorretamente, que é um dos primeiros sinais de arquitetura pobre.


Usando uma classe através de uma interface protegida


Violação do princípio da ocultação através de uma interface protegida


Mais importante, os elementos encapsulados (constantes, propriedades, métodos) tornam-se não apenas legíveis e invocados na classe filho, mas também podem ser substituídos. Essa oportunidade está repleta de um perigo oculto - devido a essas mudanças, o comportamento dos objetos da classe filho pode se tornar incompatível com os objetos da classe pai. Nesse caso, substituir os objetos da classe filho nos pontos do código em que o comportamento dos objetos da classe pai deveria levar a conseqüências imprevistas.


Por exemplo, adicionamos a funcionalidade da classe CommentBlock :


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

e herdaremos dela a classe CustomCommentBlock , na qual tiramos proveito de todas as possibilidades de quebrar a ocultação.


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

Os casos comuns de violações de ocultação são os seguintes:


  • Os métodos da classe filho revelam o estado da classe pai e fornecem acesso aos membros ocultos da classe pai. Esse cenário provavelmente não estava previsto ao projetar a classe pai, o que significa que a lógica de seus métodos provavelmente será violada.
    No exemplo, a classe filha fornece o método setter CustomCommentBlock::setComments() para modificar a propriedade CommentBlock::$comments protegida oculta na classe pai.
  • substituindo o comportamento do método da classe pai na classe filho. Às vezes, os desenvolvedores percebem esse recurso como uma maneira de resolver os problemas da classe pai criando classes filho com comportamento modificado.
    No exemplo, o método CommentBlock::getComment() na classe pai depende das chaves na matriz associativa CommentBlock::$comments . E na classe filho - para as chaves dos próprios comentários, acessíveis através do método Comment::getKey() .

O problema da banana-macaco-selva


O problema das linguagens orientadas a objetos é que elas arrastam todo o ambiente implícito. Você só queria uma banana, mas como resultado, havia um gorila segurando essa banana, além de toda a selva.

Joe Armstrong, criador de Erlang

As dependências estão sempre presentes na arquitetura do sistema. No entanto, a herança carrega uma série de fatores complicadores.


Você provavelmente se deparou com uma situação em que, à medida que o produto de software se desenvolve, as hierarquias de classes cresceram significativamente. Por exemplo


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

Você herda e herda, mas não pode decidir quais membros herdar. Você herda tudo e o todo, herdando membros de todas as classes na árvore da hierarquia. Além disso, você obtém uma forte dependência da implementação da classe pai, da classe pai da classe pai e assim por diante. E essas dependências não podem ser enfraquecidas de forma alguma (em oposição à agregação completa com o DIP ).


Sem mencionar que uma classe folha em uma hierarquia tão profunda quase certamente violará o princípio da responsabilidade única ( SRP ), saberá e fará muito. Você iniciou o desenvolvimento com uma classe Block simples, adicionou funções a ele para selecionar comentários, recursos para classificar por popularidade, cache anexado ... Como resultado, você obteve uma classe com muitas responsabilidades e, além disso, pouco conectada ( baixa coesão )


Você só queria obter uma banana (criar um objeto de folha na hierarquia) e não se importa como ela chegou ao supermercado mais próximo (como o comportamento é implementado, cujo resultado é esse objeto). No entanto, com a herança, você é forçado a realizar a realização de toda a hierarquia, a partir da própria selva. Você deve ter em mente os recursos da selva e as nuances de sua implementação, enquanto gostaria de se concentrar na banana.


Como resultado, o estado da sua classe está espalhado por muitas classes principais. Você pode resolver esse problema apenas limitando o impacto do ambiente externo (a selva) em sua classe por meio de encapsulamento e ocultação. No entanto, é impossível conseguir isso com herança, porque herança viola o princípio da ocultação.


Como agora testar classes filho em algum lugar profundo na árvore da hierarquia, porque sua implementação está espalhada pelas classes pai? Para o teste, você precisará de todas as classes pai e não poderá substituí-las de nenhuma maneira, porque você tem um envolvimento não no comportamento, mas na implementação. Como sua classe não pode ser facilmente isolada e testada, você herdará muitos problemas - com capacidade de manutenção, extensibilidade e reutilização.


Recursão aberta padrão


No entanto, a classe filho não depende apenas da interface protegida do pai. Ele também compartilha parcialmente com ele uma realização física, depende e pode influenciá-la. Isso não apenas viola o princípio da ocultação, mas também torna o comportamento da classe infantil particularmente confuso e imprevisível.


Linguagens orientadas a objetos fornecem recursão aberta por padrão. No PHP, a recursão aberta é implementada usando a pseudo-variável $this . Uma chamada de método através de $this é chamada de auto-chamada na literatura.


A auto-chamada leva a chamadas de método na classe atual ou pode ser redirecionada dinamicamente para cima ou para baixo na hierarquia de herança com base na ligação tardia. Dependendo disso, a chamada interna é dividida em:


  • down-call - uma chamada para um método cuja implementação é substituída em uma classe filho, mais baixa na hierarquia.
  • up-call - uma chamada para um método cuja implementação é herdada da classe pai, mais alta na hierarquia. Você pode fazer uma chamada explícita no PHP por meio da construção parent::method() .

O uso frequente de chamada e chamada na implementação de métodos prende as classes ainda mais de perto, tornando a arquitetura resistente e quebradiça.


Vamos dar um exemplo. Implementamos o método getComments() na classe CommentBlock pai, que retorna uma matriz de comentários.


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

Esse método baseia-se na lógica do CommentBlock::getComment() e CommentBlock::getComment() os comentários pelas chaves da matriz associativa $comments . No contexto da classe CustomCommentBlock do método CommentBlock::getComments() , a chamada de CustomCommentBlock::getComment() método CustomCommentBlock::getComment() será executada. No entanto, o método CustomCommentBlock::getComment() tem um comportamento diferente do esperado na classe pai. Como parâmetro, esse método espera a propriedade de key do próprio comentário.


Como resultado, CommentBlock::getComments() herdado automaticamente da classe pai, mostrou-se incompatível no comportamento com CustomCommentBlock::getComment() . Chamar getComments() no contexto de CustomCommentBlock provavelmente retornará uma matriz de valores null .


Devido ao forte envolvimento, ao fazer alterações em uma classe, você não pode se concentrar apenas em seu comportamento. Você é forçado a levar em conta a lógica interna de todas as classes, para baixo e para cima na hierarquia. A lista e a ordem de chamada na classe pai devem ser conhecidas e documentadas, o que viola substancialmente o princípio da ocultação. Os detalhes da implementação tornam-se parte do contrato da classe pai.


Controle de efeitos colaterais


No exemplo anterior, o problema se manifestou devido à diferença na lógica dos métodos getComment() nas classes pai e filho. No entanto, controlar a semelhança do comportamento dos métodos na hierarquia de classes não é suficiente. Problemas podem esperar por você se esses métodos tiverem efeitos colaterais.


A função com efeitos colaterais (função com efeitos colaterais ) altera alguns estados do sistema, além do efeito principal - retornando o resultado ao ponto de chamada. Exemplos de efeitos colaterais:


  • alterar variáveis ​​externas ao método (por exemplo, propriedades do objeto);
  • alterando variáveis ​​estáticas locais para o método;
  • interação com serviços externos.

Portanto, esses efeitos colaterais também são a parte da implementação , que também não pode ser efetivamente ocultada no processo de herança.


Imagine que precisávamos incluir o método viewComment() na classe CommentBlock para obter uma representação textual de um dos comentários.


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

Adicione um efeito colateral à classe filho e especifique seu objetivo. Implementamos a classe CountingCommentBlock , que complementa o CommentBlock capacidade de contar visualizações de comentários individuais no cache. Permita que a classe aceite a injeção de um cache compatível com PSR-16 no construtor ( injeção de construtor ) por meio da interface CounterInterface (que, no entanto, acabou sendo excluída do PSR-16). Usaremos o método increment() para incrementar atomicamente o valor do contador no 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); } } 

Tudo funciona bem. No entanto, em algum momento, decidiu-se adicionar a função viewComments() para formar uma representação textual de todos os comentários no bloco. Esse método é adicionado à classe base CommentBlock e, de relance, herdar a implementação desse método por todas as classes filho parece muito conveniente e evita a gravação de código adicional nas classes filho.


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

No entanto, a classe pai não sabe nada sobre os recursos de implementação das classes filho. 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() . I.e. – . 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 , , . I.e. , . .


, , 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 , (). I.e. , – 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 ? Não. - , 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 ), , «» .


, . I.e. :


 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 .


Editando o modelo embutido `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 .


Conclusão


, : final ! IDE, .


, , . – SOLID . , , .


, . :


  • ;
  • final , , , ;
  • .

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


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


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


final . ( SOLID ) ( fragile ) . , .

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


All Articles