Back-End

17 mai, 2019

5 bons motivos para não usar arrays no PHP 7.4

Publicidade

A versão 7.4 do PHP que está por vir está tomando forma cada vez mais rapidamente, e até o momento apresenta novidades muito interessantes para a linguagem – que já foram confirmadas. Dentre elas:

A partir destas atualizações fica cada vez mais clara a intenção da comunidade em tornar o PHP cada vez mais simples de escrever e confiável na execução. O PHP está reduzindo sua sintaxe ao mesmo passo que reafirma a tipagem e performance.

Ao mesmo tempo, o Laravel continua no topo da lista de frameworks populares por diversas razões. Dentre elas, a facilidade de produzir software com velocidade e qualidade.

Com sua sintaxe agradável e componentes independentes, o framework (ou parte dele) acaba conquistando até mesmo desenvolvedores mais céticos (meu caso).

Animado com a revolução que algumas implementações trouxeram ao blog estático do PODEntender e sedento para atualizar para o PHP 7.4 assim que sair, eu vim aqui apresentar cinco motivos para você nunca mais usar arrays a partir do PHP 7.4!

1 – Arrays são uma péssima estrutura de dados

O termo array vem do latim e significa saco de bagulhos variados que você não quer se dar o trabalho de armazenar propriamente (brincadeira).

Mas veja, o tipo array é uma estrutura altamente permissiva que vai agregar qualquer tipo de dado indiscriminadamente. Num mesmo array você consegue armazenar inteiros, booleanos, strings e objetos.

Essa estrutura de dados permissiva te transforma em um(a) eterno(a) namorado(a) ciumento(a) do seu código, tendo que avaliar cada passo pelo qual ele passa. Não seja essa pessoa!

Confira:

$numeros = ...;

$total = array_sum($numeros);

Não sei quanto a você, mas eu não consigo confiar que $numeros possui apenas tipos numéricos. Me diga, sem executar o código, qual o resultado da soma se números fosse o seguinte:

$numeros = [0, 10, "1 maçã", false, "duas bananas"];

Não consegue, né?

Além disso, os índices dos arrays também são muito permissivos. Num array é possível armazenar um valor por chave numérica ou por string. Ou os dois!

$arrayLindo = [
    5 => 'cinco',
    'um',
    'dois' => '3'
];

E agora? Em qual posição do array está o valor um?

Portanto, a todo momento em que um array se apresenta pra mim, meu código fica automaticamente duas vezes mais defensivo.

Para realizar um array_sum(), por exemplo, eu costumo antes fazer um array_filter() que vai retirar todos os itens cujo retorno de is_numeric() seja false.

São verificações que eu não precisaria fazer se pudesse confiar nos tipos internos de um array. Iterações que todos nós poderíamos evitar.

Assim, chegamos ao segundo motivo.

2 – Arrays oferecem performance reduzida em diversos casos

Comecemos por um princípio básico: um parâmetro de função do tipo object, ou seja, uma instância de classe sempre é passado por referência. Um parâmetro de função do tipo array sempre é passado por cópia.

O que isso significa em termos práticos?

function arrayFunction(array $param) {
    $param['pos'] = 1;
}

function objectFunction(\stdClass $param) {
    $param->pos = 1;
}

$array = [];
arrayFunction($array);
var_dump($array); // vazio

$object = new \stdClass();
objectFunction($object);
var_dump($object); // pos = int(1)

Toda vez que você chamar a função arrayFunction() e passar um array, o PHP fará uma cópia do array inteiro para passar para a função. Este array se mantém naquele escopo.

Ao passo que, ao passar um objeto como parâmetro, não é feita uma cópia, mas é passada uma referência ao objeto. Isso significa menos memória consumida.

Com isso, existem diversas implementações de coleção otimizadas para diversos casos diferentes com as quais você pode utilizar menos memória, processamento, ou os dois.

Vamos ver um exemplo rápido usando o SplFixedArray?

O fixed array é um objeto muito interessante para quando você sabe o tamanho máximo da sua coleção, e é otimizado para lidar com os dados que espera receber.

Faz aí no seu computador! Vou deixar aqui o tempo que levou pra executar no meu:

<?php // array.php
$tamanho = 1000000;
$inicio = microtime(true);
$array = [];

for ($i=0; $i < $tamanho; $i++) {
  $array[] = null;
}

echo (microtime(true) - $inicio) . PHP_EOL; // 0.043247938156128
var_dump(memory_get_peak_usage()); // 33950192
<?php // SplFixedArray.php
$tamanho = 1000000;
$inicio = microtime(true);
$fixedArray = new SplFixedArray($tamanho);

for ($i=0; $i < $tamanho; $i++) {
  $fixedArray[$i] = null;
}

echo (microtime(true) - $inicio) . PHP_EOL; // 0.041646957397461
var_dump(memory_get_peak_usage()); // 16394864

O SplFixedArray neste exemplo roda em menos tempo (a diferença é inexpressiva, sejamos justos) e utiliza metade da memória para realizar a mesma ação. Metade!

O tipo array traz consigo diversas responsabilidades: iterar, contar, armazenar e acessar por chave.

A analogia do pato se encaixa perfeitamente! Ele anda, voa e nada, mas não faz nenhum dos três direito. O mesmo acontece com o nosso array.

O SplFixedArray é especializado em criar coleções de tamanho fixo com 16 bytes por posição e a classe faz isso muito bem!

E olha só: o SplFixedArray tem toda API do Iterator bonitinha implementada, que é uma API consistente e que segue o mesmo padrão por todos que a implementam, diferente de certos tipos de dados por aí.

3 – Existem abstrações muito mais legíveis e diretas

E aqui eu falo abertamente do pacote Collection do Laravel. Apesar de não ser o pedaço de código mais performático do mundo, ele apresenta uma API muito agradável e é extensível. Portanto, a parte de performance você pode consertar, se necessário.

Vamos dar uma olhada na diferença.

Eis aqui uma coleção contendo nomes de pessoas, e queremos coletar somente pessoas cujo nome tenha mais de 12 caracteres e transformar cada nome em uma entidade Pessoa do nosso domínio.

$nomes = ['Nome Curto', 'Um nome um pouco maior', 'Outro Curto'];

class Pessoa
{
    public function __construct(string $name)
    {...}
}

Este requisito se traduz em duas operações: filter e map. Nosso filter precisa remover pessoas cujo strlen($nome) seja menor ou igual a 12, enquanto o map precisa transformar uma string em instância de Pessoa.

Usando a API nativa, ficaria assim:

$nomes = ['Nome Curto', 'Um nome um pouco maior', 'Outro Curto'];

// Utilizando short closures: php 7.4
$nomes = array_filter($nomes, fn(string $nome): boolean => strlen($nome) <= 12);
$nomes = array_map(fn(string $nome): Pessoa => new Pessoa($nome), $nomes);

Além da confusão da posição dos argumentos que faz qualquer programador sem autocomplete pensar duas vezes sobre sua profissão, nós precisamos atribuir o valor de $nomes três vezes.

Usei array_filter() e array_map(), mas eu sei que quando se trata de arrays a gente gosta mesmo é de usar foreach().

Mas veja só como o Collection do Laravel trata esse mesmo problema:

// Isto é um exemplo! Não use collect()!! Me pergunta no twitter que eu te explico
$nomes = collect(['Nome Curto', 'Um nome um pouco maior', 'Outro Curto'])
    ->filter(fn(string $nome): boolean => strlen($nome) <= 12)
    ->map(fn(string $nome): Pessoa => new Pessoa($nome));

Com a API Collection fazemos uma única atribuição. A API é consistente – o código fica claro de início e, de quebra, você pode optar por implementações diferentes do seu mecanismo de lista que façam mais sentido e possam ser mais eficientes no seu caso de uso.

São exatamente os benefícios da API Collection que contrastam com o motivo quatro, a seguir:

4 – A gente nunca sabe que diabos está dentro de um array

No começo do texto eu já comentei que um array é um saco de bagulhos, certo?

Já a implementação com Collection nos permite tornar a nossa coleção especializada em determinado tipo, sem muita dor de cabeça. Uma classe PessoaCollection, por exemplo, nos permite esperar que seus elementos sejam do tipo Pessoa em vez de precisarmos testar com is_string, instanceof ou afins cada um dos seus elementos.

É justamente disso que o Object Calisthenics fala no exercício de First class collections.

  • Uma classe que contenha uma lista em suas propriedades não deverá possuir outras propriedades

Em outras palavras: pare de usar array pra tudo e crie uma classe pra representar sua coleção de tipo específico!

No caso de Collection você pode ainda estender a classe e sobrescrever os métodos pertinentes para garantir os tipos internos da coleção, já que o PHP não traz consigo Generics.

No estatística do PODEntender eu sequer implementei esta checagem, porque julguei desnecessária. Ao sobrescrever a classe Collection por um tipo específico, eu simplesmente adoto, por convenção, que os tipos internos serão aqueles.

class PessoaCollection extends Collection
{}

Eu diria, ao ver uma classe dessa, que PessoaCollection é um conjunto de vários objetos do tipo Pessoa. Portanto, poderia tornar essa classe otimizada para a sua necessidade de implementar uma estratégia específica de collection usando o SplObjectStorage:

class PessoaCollection extends SplObjectStorageCollection
{}

class SplObjectStorageCollection extends Collection
{
    private SplObjectStorage $storage;

    // Sobrescrever métodos pertinentes
}

Particularmente penso que sobrescrever os métodos da classe Collection é desgastante. Faria mais sentido se Collection fosse uma interface em alguns contextos, mas a forma como foi construída requer que não.

Paciência, cada projeto entende o que é melhor para o seu contexto.

Todo esse esforço, porém, tem uma saída positiva. Para que possamos ter um código cada vez mais testável, é importante seguir o princípio da responsabilidade única. E é assim que chegamos ao motivo número 5:

5 – A sua regra de negócio não precisa saber como funciona um array!

Será que a coleção precisa crescer? Qual é o tamanho esperado? Eu preciso colocar tudo em memória ou vou usar como stream? Eu acesso por chave numérica ou string?

Normalmente não reparamos nisso, mas coleções, por si só, possuem um domínio muito específico e importante o suficiente para uma boa modelagem. Quando ignoramos isso acabamos tomando o caminho curto que, como vimos, pode ser pior.

Ao tratar nossas coleções como os domínios de respeito que são, passamos também a poder depender de interfaces quando se trata de sua utilização. Ou melhor, podemos depender de contratos!

As collections que vimos acima resolvem muita coisa para nós, desde seus contratos. Funções como filter() e map() estão sempre presentes, além de outras muito interessantes como groupBy() e por aí vai.

Não há bons motivos para que executemos todas essas lógicas em nossas classes de negócio (domínio, services, repositories).

Além disso, há um ganho em extrair a lógica de coleção que é quase óbvio, mas que vale a pena ser citado:

  • Quanto menor o número de responsabilidades, mais simples e diretos serão os testes

Para finalizar este artigo, eu gostaria de trazer um último exemplo mostrando como a utilização de collections especializadas tem se pagado no PODEntender. Se você abrir este teste aqui, será direcionado para o seguinte snippet:

public function testExecuteFetchesExactAmount(): void
{
    $audioEpisodeCollection = $this->createDefaultAudioEpisodeCollection()
        ->sortByDesc(function (AudioEpisode $episode) {
            return $episode->createdAt();
        });

    $this->postRepository->withAudio()->willReturn($audioEpisodeCollection);

    $result = $this->fetchLatestEpisodesService->execute(2, null);

    $this->assertEquals(2, $result->count());

    $lastEpisode = $result->first();
    $episodeBeforeLast = $result->last();

    $this->assertEquals($audioEpisodeCollection->take(2)->first()->guid(), $lastEpisode->guid());
    $this->assertEquals($audioEpisodeCollection->take(2)->last()->guid(), $episodeBeforeLast->guid());
}

Um método de teste com 11 linhas de código (que poderiam ser reduzidas para 5 sem muita perda de legibilidade).

Como o nosso postRepository trabalha com Collections, fazer o mock para seu output é relativamente simples e direto.

As chamadas ao método assertEquals() também são muito simples e diretas, pois eu posso confiar no contrato da collection e simplesmente dizer ao teste o que eu espero que aconteça de forma semântica!

Ainda assim você pode estar exclamando:

  • “Ah, mas eu vou ficar criando classe e forçar o PHP a carregar mais classes à toa? Isso vai aumentar o tempo de execução por uma questão estética!”

Não, não e não. Não é à toa, não vai aumentar o tempo de execução e não é uma questão puramente estética.

Não é à toa: segregar responsabilidades e tornar sua classe testável são princípios mais que consolidados na prática de desenvolvimento de software.

Não vai aumentar o tempo de execução. Ao menos não necessariamente. O PHP 7.4 possui um mecanismo de preloading que vai permitir carregar pacotes antes da execução do script e vai manter isso em cache para execuções futuras. Alguns benchmarks preliminares mostraram aumento de performance de cerca de 50% no Zend Framework 2.

Não é uma questão puramente estética: especializar seus componentes em busca de alta coesão e baixo acoplamento são princípios de design que todo software manutenível deve buscar.

Conclusão

A versão 7.4 do PHP oferece diversas otimizações de performance e também de linguagem. Existe um esforço conjunto da comunidade para tornar seu código mais legível e performático, mas, principalmente tipado!

A utilização de arrays quando é mal feita, tende a condenar o seu código e a quem mais tiver coragem de tocar nele. Seja em performance, legibilidade ou até mesmo em decisões de design.

É claro que você não precisa esperar o PHP 7.4 sair pra parar de usar arrays. O quanto antes você começar, melhor.

Fique à vontade para me perguntar no Twitter a qualquer momento, caso tenha ficado alguma dúvida.

Um beijo e até a próxima!

***

Este artigo foi publicado originalmente em: https://phpsp.org.br/artigos/5-bons-motivos-para-nao-usar-arrays-no-php-74/