Evitando o null-driven-development

Quando a programação em C estava em alta, havia uma série de alocações e liberações de memória. Depois disso, a alocação/liberação passou para C++, e a partir daí tínhamos código como o seguinte:

Person *person = new Person();
delete person;

Algumas vezes, queríamos criar um objeto mas não tínhamos todas as informações dele. Era comum usarmos o ponteiro e só atribuir ele quando tivessemos a informação:

Person *person = null;
//do something in-between
person = new Person(name);

Isso causava um efeito estranho que, eventualmente, o objeto seria “nulo”, ou “não existente”. Isso era uma novidade até o momento, já que nas linguagens mais antigas (VB, QuickBasic, Pascal, etc) ou não havia esse conceito de “nulo” ou não era comum usar.

Quando as linguagens orientadas a objeto dominaram o mercado, esse “null-pattern” acabou também entrando no mercado. Em Java (e Scala), por exemplo, qualquer operação que envolva um null lança um “Null-pointer exception” (que muitos programadores simplesmente capturam com um try-catch, mandam imprimir no console o stacktrace, e continuam o programa, que normalmente para de funcionar). Em Ruby, as coisas são mais complexas…

Ruby é a primeira linguagem que eu conheço que meio que “institucionalizou” o uso de nulos.

Em Ruby, o nulo (ou nil) é um objeto Singleton, da classe NilClass. Por isso, ele pode receber métodos-o que é meio absurdo, já que nil representa a inexistência de algo. Mas o verdadeiro dano está no método try:

object = nil
object.try(:upcase) # Retorna nil
object = "foo"
object.try(:upcase) # Retorna "FOO"

Quando programamos, por exemplo, em Rails, é comum também termos views como:

<div>
  Nome: <%= @user.nome %><br />
  Idade: <%= @user.age %><br />
  CPF: <%= @user.cpf %><br />
  RG: <%= @user.rg %><br />
</div>

Sabendo que CPF ou RG podem estar nulos, isso também é absurdo. Mas por que isso é absurdo? Considere que você entrou num cadastro de alunos e você tem a seguinte informação:

Nome: John Alexander
Idade: 20
CPF:
RG:

O que os campos CPF e RG estão querendo dizer? O aluno possui CPF, mas ninguém cadastrou nada? Ou o aluno deliberadamente não tem essa informação (digamos, ele é estrangeiro)? O problema aqui é que “nulo” é a AUSÊNCIA de informação. Em Ruby, nil.to_i retorna o número 0, por exemplo. Mas o que é “NADA convertido para INTEIROS”? É como perguntar “quantas pernas tem essa cadeira que não existe?”-é absurdo.

Mas como corrigir isso? Em primeiro lugar, não há como corrigir quando o framework já te oferece um valor ou nulo-como o ActiveRecord, por exemplo. Mas, se eu fosse optar por algum comportamento em ORMs, usando o que o ActiveRecord já oferece, eu optaria por um código bem simples:

def get(name_of_attribute)
  self[name_of_attribute] or raise NilReceived, 'found a nil when I wasn't expecting'
end

def get_or(name_of_attribute, default)
  self[name_of_attribute] or default
end

Fica muito fácil resolver o problema agora na view de alunos:

<div>
  Nome: <%= @user.get(:nome) %><br />
  Idade: <%= @user.get(:age) %><br />
  CPF: <%= @user.get_or(:cpf, "Não possui") %><br />
  RG: <%= @user.get_or(:rg, "Não possui") %><br />
</div>

Nesse caso, se um usuário estiver com registro incompleto, ele lançará uma exception-e isso, muitas vezes, é desejável. Você não deveria poder cadastrar um aluno sem nome ou idade, então por que ele está no cadastro assim? Será um bug nos cadastros? Você pode, digamos, emitir um relatório aonde os alunos estão com cadastro incompleto, e propagar esse problema lá pra frente?

Essa é uma pergunta importante: digamos que você precise calcular a média de idade das pessoas que frequentam o seu Pub. Só que os cadastros estão incompletos, há muitas pessoas que não tem sua idade cadastrada, e para facilitar você simplesmente faz o seguinte código:

weeks_and_people = Person.grouped_by_weekday
weeks_and_people.inject({}) do |result, (week, people)|
  sum = people.sum { |person| person.age.to_i }
  result.merge(week => sum / people.size.to_f)
end

Agora, digamos que METADE dos seus clientes não tem a idade cadastrada. E que a idade média (real) dos seus clientes é de 26 anos. Quando você fizer a conta, a média de idade de seus clientes será de 13 anos.

Só que você, programador, não vai descobrir isso a tempo-você projetou o sistema, colocou em produção, e foi culpa dos donos do Pub que não cadastraram corretamente os clientes. Mas aí, quando o estabelecimento receber uma fiscalização por permitir menores de idade no Pub, por que seu relatório “inofensivo” foi usado numa transação de alta importância e ninguém o conferiu (e não, as pessoas não conferem! Experiência própria de trabalhar numa empresa em que a tela principal ficou dois dias quebrada, e ninguém percebeu).

Há outra coisa que vem de linguagens funcionais que podemos aproveitar muito: lazy e memoization. Se você quer criar uma estrutura que pode ter elementos não definidos, a única coisa que você precisa fazer é deixá-los com um valor padrão, porém não computar esse valor. Digamos que você tem um cadastro de pessoas, e essas pessoas tem um chefe. Basta deixar o chefe “preparado”, porém com valores padrão, e depois só preencher esses campos:

class User
  def boss
    @boss ||= Boss.new(name: 'Sem chefe definido')
  end
end

Essa abordagem é interessante porque ao deferir a computação para depois, podemos criar verdadeiras estruturas infinitas sem precisarmos nos preocupar com nulos:

# Com lazy e memoization:
"O usuário #{user.name} possui chefe #{user.boss.name} e o ramal do chefe é #{user.boss.telephone.number}"

# Sem lazy e memoization:

"O usuário #{user.name} possui chefe #{user.boss.try(:name) || 'Sem chefe definido'} e o ramal do chefe é #{user.boss.try(:telephone).try(:number)}"

A idéia é: evitem nulos. Evitem ao máximo chamar operações nos nulos. Existe um motivo pelo qual SQL tem um tratamento tão peculiar de campos nulos: é uma coisa que não deveria existir, e que cada vez mais estamos inventando usos desnecessários.

Advertisements
This entry was posted in Ruby and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s