Projetando funções mais elegantes em PHP

Em: 19/12/2007 Tags: , Referencie do seu blog (Trackback)

Uma das facilidades óbvias que o PHP nos dá para definir funções é a possibilidade de se definir valores padrão para os parâmetros. Acredito, porém, que esse é o “lado negro” da força. Se uma função, por exemplo, deve receber uma grande quantidade de parâmetros, e ao menos 4 ou 5 são opcionais, é bem provável que você cairá no seguinte dilema: qual dos parâmetros aqui é mais importante?

Vamos a um exemplo nada típico para PHP. Imagine que você quer criar uma biblioteca gráfica. Sua pequena biblioteca deverá ser capaz de desenhar círculos, e para isso você decidiu implementar a função draw_circ(). O que sabemos sobre um círculo? Todo círculo tem, por exemplo, um raio. Isso já é alguma coisa. Então você escreve sua definição:

function draw_circ($radius) { ... }

Um círculo tem raio… ok… mas com essa definição todos os meus círculos serão idênticos. Precisamos de mais parâmetros nessa função. Podemos adicionar uma cor de fundo, uma cor para a “borda” e uma largura para a “borda”. Mas será que, sempre que utilizar minha nova função, desejarei especificar essas 4 características? Não, certamente não. Então podemos declarar esses 3 novos parâmetros como opcionais, pois daremos valores padrão a eles.

function draw_circ($radius, $bg_color='white', $border_color='black', $border_width='1px') { ... }

Mas acabamos de criar um problema do ponto de vista da beleza do código. Como poderemos criar um círculo de raio 3, com cor de fundo branca, cor da borda preta, mas com largura da borda de 3px? Ou seja, como criar um círculo com todos os parâmetros definidos com valores padrão, mas apenas com o quarto parâmetro assumindo um valor diferente? Não é difícil, é verdade:

draw_circ(3, null, null, '3px');

A beleza do código é comprometida com esses dois parâmetros null. Pois neste caso nossa função recebe apenas 4 parâmetros. Mas na vida real muitas vezes nossas funções recebem 8, 9, ou mais. Com que solução contamos, em PHP, para deixar esse código mais apresentável?

Minha abordagem favorita (que você talvez não goste) é emprestada de Ruby. Ela consiste na utilização de um array associativo, que atua como um agregador de todos os parâmetros opcionais. Com essa nova abordagem podemos definir nossa função com apenas 2 parâmetros:

function draw_circ($radius, $options=array()) { ... }

Continuamos tendo o raio como o único parâmetro obrigatório. Os demais passam a ser opcionais, já que o nosso array associativo de opções é, ele próprio, opcional.

Mesmo com essa nova definição é possível escrever a implementação dessa função de forma equivocada. Esta seria uma má implementação para o tratamento das opções passadas para $options:

function draw_circ($radius, $options=array()) {
    $options['bg_color'] = isset($options['bg_color']) ? $options['bg_color'] : 'white';
    $options['border_color'] = isset($options['border_color']) ? $options['border_color'] : 'black';
    $options['border_width'] = isset($options['border_width']) ? $options['border_width'] : '1px';
    // ...
}

Por que ela é equivocada?

  • É verbosa demais
  • Trata os parâmetros de forma individual
  • Deixa a legibilidade do código a desejar

O que essa implementação faz é o seguinte: para cada chave válida em $options, verifica se há um valor definido para essa chave. Se há, então o atribui novamente na mesma chave de $options. Senão, atribui o valor padrão àquela chave.

A função array_merge() chega, então, para salvar o dia e garantir a legibilidade desse código. De acordo com a documentação oficial, essa função faz o seguinte:

array_merge() funde os elementos de um ou mais arrays para que os valores de um sejam adicionados no fim do array anterior. Ela retorna o array resultante.

Até aí nada de mais. Essa função parece legal, mas serve para nós? O texto continua:

Se os arrays de entrada possuem as mesmas chaves string, então o segundo valor definido para aquela chave irá sobrescrever o primeiro.

Opa! É disso que precisamos.

Veja como fica a nova implementação:

function draw_circ($radius, $options=array()) {
    $defaults = array(
        'bg_color'     => 'white',
        'border_color' => 'black',
        'border_width' => '1px'
    );
    $options = array_merge($defaults, $options);
    // ...
}

draw_circ(5, array('bg_color' => 'orange'));

A idéia é muito simples: $defaults é o nosso array associativo que possui os valores padrão para cada uma das opções possíveis. É importante que todos os valores padrão sejam declarados nesse array. Após sua definição utilizamos array_merge() para sobrescrever cada um dos valores definidos em $defaults pelos respectivos valores definidos em $options. Uma chave de $defaults não definida por $options permanecerá com seu valor original (ou seja, seu valor padrão ou default). Como array_merge() não modifica o array original, precisamos atribuir seu resultado à antiga variável $options, agora com valores sobrescritos.

Com essa implementação ganhamos muito em legibilidade e em beleza do código. O grande problema fica para a notação de arrays literais em PHP. Enquanto a definição da função draw_circ() fica muito legal uma vez que utilizamos array_merge(), a chamada a essa função precisa ter a construção array(). Esse é o preço que se paga. Você pode escolher entre esta implementação ou continuar com as dezenas de variáveis com valores padrão e, de vez em quando, ver suas chamadas de função recebendo valores null intercalados com valores verdadeiros.

Bonus

Ficou curioso para saber como draw_circ poderia ser implementada em Ruby? Vamos ao código:

def draw_circ(radius, options={})
  defaults = {
    :bg_color     => 'white',
    :border_color => 'black',
    :border_width => '1px'
  }
  options = defaults.merge(options)
  options.each {|key,value| puts "#{key} => #{value}" }
end

draw_circ(5, :bg_color => 'orange')

A última linha da implementação não faz parte da função, foi colocada apenas para mostrar que a saída é aquela que esperamos:

bg_color => orange
border_color => black
border_width => 1px

Artigos relacionados:

3 respostas para “Projetando funções mais elegantes em PHP”

  1. Raphael deAlmeida disse:

    Bem interessante seu artigo, já usava o array options, mas o array_merge ajuda bastante :)

  2. Caio disse:

    Raphael, só após entrar no seu blog descobri que o projeto PHPSpec existe ;)

    Valeu, adicionei teu blog aos meus feeds.

  3. Felipe disse:

    Muito bom o post. Muito útil.. vlw

Escreva um comentário (utilize o formato Markdown)