Inferno "zero" e como sair dele

Valores nulos, quando usados ​​sem pensar, podem tornar sua vida insuportável e você pode nem entender o que exatamente lhes causa tanta dor. Deixe-me explicar.


Valores padrão


Todos nós vimos um método que leva muitos argumentos, mas mais da metade deles é opcional. O resultado é algo como isto:

public function insertDiscount(   string $name,   int $amountInCents,   bool $isActive = true,   string $description = '',   int $productIdConstraint = null,   DateTimeImmutable $startDateConstraint = null,   DateTimeImmutable $endDateConstraint = null,   int $paymentMethodConstraint = null ): int 

No exemplo acima, queremos criar um desconto que se aplique em todos os lugares por padrão, mas ele pode ficar inativo ao criar, aplicar apenas a produtos específicos, agir apenas em determinados momentos ou quando o usuário seleciona um método de pagamento específico.

Se você deseja criar um desconto para um determinado método de pagamento, precisará chamar o método da seguinte maneira:

 insertDiscount('Discount name', 100, true, '', null, null, null, 5); 

Esse código funcionará, mas é completamente incompreensível para a pessoa que o lê. Torna-se extremamente difícil analisá-lo, portanto, não podemos suportar facilmente o aplicativo.

Vamos pegar esse exemplo de argumento por argumento.

O que é um desconto válido?


Já descobrimos que o desconto ilimitado se aplica a todos os lugares. Assim, um desconto válido contém tudo, exceto as restrições que podemos adicionar posteriormente. O argumento isActive tem um valor padrão true. Portanto, o método pode ser chamado da seguinte maneira:

 insertDiscount('Discount name', 100); 

Apenas lendo o código, não sei se o desconto estará ativo imediatamente. Para descobrir, eu precisaria verificar se a assinatura do método tem valores padrão.

Agora imagine que você precisa ler 200 linhas de código. Tem certeza de que deseja verificar todas as assinaturas do método chamado para obter informações ocultas? Prefiro ler o código sem precisar procurar nada.

O mesmo vale para o argumento responsável pela descrição. Por padrão, é uma sequência vazia - isso pode causar muitos problemas no local em que você espera ver uma descrição. Por exemplo, ele pode ser impresso no cheque, mas, como está vazio, o usuário simplesmente vê uma linha vazia ao lado da linha com o valor. O sistema não deve permitir que isso aconteça.

Eu reescreveria esse método assim:

 public function insertDiscount(  string $name,  string $description,  int $amountInCents,  bool $isActive ): int 

Eu removi completamente as restrições, pois decidimos adicioná-las posteriormente usando métodos separados. Como todos os parâmetros agora são necessários, eles podem ser organizados em qualquer ordem. Coloquei a descrição logo após o nome, porque o código fica melhor quando estão perto.

 insertDiscount(  'Discount name',  'Discount description',  100,  Discount::STATUS_ACTIVE ); 

Também usei constantes para o status da atividade de desconto. Agora você não precisa olhar para a assinatura do método para descobrir o que significa verdadeiro para esse argumento: torna-se óbvio que estamos criando um desconto ativo. No futuro, podemos melhorar ainda mais esse método (spoiler: usando objetos de valor).

Adicionando restrições


Agora você pode adicionar várias restrições. Para evitar zero, zero, zero inferno, criaremos métodos separados.

 public function addProductConstraint(  Discount $discount,  int $productId ): Discount; public function addDateConstraint(  Discount $discount,  DateTimeImmutable $startDate,  DateTimeImmutable $endDate ): Discount; public function addPaymentMethodConstraint(  Discount $discount,  int $paymentMethod ): Discount; 

Assim, se queremos criar um novo desconto com certas restrições, faremos assim:

 $discountId = insertDiscount(  'Discount name',  'Discount description',  100,  Discount::STATUS_ACTIVE ); addPaymentMethodConstraint(  $discountId,  PaymentMethod::CREDIT_CARD ); 

Agora compare isso com a chamada original. Você verá como ficou mais conveniente ler.

Nulo nas propriedades do objeto


A resolução de zeros nas propriedades do objeto também causa problemas. Não consigo transmitir com que frequência vejo essas coisas:

 $currencyCode = strtolower(  $record->currencyCode ); 

Buum! "Não é possível passar nulo ao strtolower." Isso aconteceu porque o desenvolvedor esqueceu que currencyCode pode ser nulo. Como muitos desenvolvedores ainda não usam o IDE ou suprimem avisos neles, isso pode passar despercebido por muitos anos. O erro continuará rolando em algum log não lido e os clientes reportarão problemas periódicos que aparentemente não estão relacionados a isso, para que ninguém se preocupe em examinar esta linha de código.

É claro que podemos adicionar cheques nulos sempre que acessamos o currencyCode. Mas então acabaremos em um tipo diferente de inferno:

 if ($record->currencyCode === null) {  throw new \RuntimeException('Currency code cannot be null'); } if ($record->amount === null) {  throw new \RuntimeException('Amount cannot be null'); } if ($record->amount > 0) {  throw new \RuntimeException('Amount must be a positive value'); } 

Mas, como você já entendeu, essa não é a melhor solução. Além de bagunçar seu método, agora você deve repetir esse teste em qualquer lugar. E toda vez que você adicionar outra propriedade nula, não se esqueça de fazer outra verificação desse tipo! Felizmente, existe uma solução simples: objetos de valor.

Objetos de valor


Objetos de valor são coisas poderosas, mas simples. O problema que estávamos tentando resolver era que é necessário validar constantemente todas as nossas propriedades. Mas fazemos isso porque não sabemos se é possível confiar nas propriedades do objeto, se são válidas. E se pudéssemos?

Para confiar nos valores, eles precisam de dois atributos: eles devem ser validados e não devem ter sido alterados desde a validação. Dê uma olhada nesta classe:

 final class Amount {  private $amountInCents;  private $currencyCode;  public function __construct(int $amountInCents, string $currencyCode): self  {    Assert::that($amountInCents)->greaterThan(0);    $this->amountInCents = $amountInCents;    $this->currencyCode = $currencyCode;  }  public function getAmountInCents(): int  {    return $this->amountInCents;  }  public function getCurrencyCode(): string  {    return $this->currencyCode;  } } 

Estou usando o pacote beberlei / assert. Emite uma exceção sempre que a verificação falha. É o mesmo que a exceção para null no código-fonte, a menos que tenhamos movido a verificação para este construtor.

Como usamos declarações de tipo, garantimos que o tipo também está correto. Portanto, não podemos passar int para seguir em frente. Se você estiver usando uma versão mais antiga do PHP que não suporta declarações de tipo, poderá usar este pacote para verificar os tipos com -> integer () e -> string ().

Depois de criar o objeto, os valores não podem ser alterados, porque temos apenas getters, mas não setters. Isso é chamado de imunidade. Adicionar final não permite estender essa classe para adicionar setters ou métodos mágicos. Se você vir Montante $ amount nos parâmetros do método, poderá ter 100% de certeza de que todas as suas propriedades foram validadas e que o objeto é seguro. Se os valores não fossem válidos, não poderíamos criar um objeto.

Agora, com a ajuda de objetos de valor, podemos melhorar ainda mais o nosso exemplo:

 $discount = new Discount(  'Discount name',  'Discount description',  new Amount(100, 'CAD'),  Discount::STATUS_ACTIVE ) insertDiscount($discount); 

Observe que primeiro criamos um desconto e, por dentro, usamos Quantia como argumento. Isso garante que o método insertDiscount receba um objeto de desconto válido, além de facilitar a compreensão de todo esse bloco de código.

Zero Horror Story


Vejamos um caso interessante em que nulo pode ser prejudicial em um aplicativo. A idéia é extrair a coleção do banco de dados e filtrá-la.

 $collection = $this->findBy(['key' => 'value']); $result = $this->filter($collection, $someFilterMethod); if ($result === null) {   $result = $collection; } 

Se o resultado for nulo, use a coleção original como resultado? Isso é problemático, pois o método de filtragem retorna nulo se não encontrou valores adequados. Assim, se tudo for filtrado, ignoraremos o filtro e retornaremos todos os valores. Isso quebra completamente a lógica.

Por que a coleção original é usada como resultado? Nós nunca saberemos. Suspeito que o desenvolvedor tenha uma certa suposição sobre o que nulo significa nesse contexto, mas acabou errado.

Este é o problema com valores nulos. Na maioria dos casos, não está claro o que eles significam e, portanto, só podemos adivinhar como responder a eles. É muito fácil cometer um erro. A exceção, por outro lado, é muito clara:

 try {  $result = $this->filter($collection, $someFilterMethod); } catch (CollectionCannotBeEmpty $e) {  // ... } 

Este código é único. É improvável que um desenvolvedor o interprete mal.

Vale a pena o esforço?


Tudo isso parece um esforço extra para escrever código que faça a mesma coisa. É sim. Mas, ao mesmo tempo, você gastará muito menos tempo lendo e compreendendo o código, portanto os esforços serão bastante recompensados. Cada hora extra que gasto escrevendo código economiza dias em que não preciso alterar o código ou adicionar novos recursos. Pense nisso como um investimento garantido de alto rendimento.

Então chegou o fim do meu discurso nulo. Espero que isso ajude você a escrever um código mais compreensível e sustentável.

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


All Articles