Surgiu há uns dias uma discussão interessante no trabalho. Um colega meu refactorizou uma componente que tinha programado fazia uns meses, e questionava-se quando ao melhor modo de se proteger contra aquilo que chamou a tipificação fraca na linguagem de programação Ruby (e com o que eu na altura concordei). Ele tomou a decisão de, segundo ele, verificar o tipo das variáveis passadas como argumento a cada função, protegendo-de dessa forma contra comportamentos inesperados, possíveis excepções que fossem lançadas, etc., e chamou ao que estava a fazer "programação defensiva". Quis discutir o resultado comigo, e aí começaram a surgir as diferenças na opinião de cada um. Eu achei o que ele estava a fazer contra-producente, visto a própria linguagem de programação (que dizem ser uma "linguagem orientada a objectos pura") já providenciar algum tipo de identificação desse tipo de excepções, ás quais ele podia aplicar algum tipo de tratamento, mas pior que isso, aplicar esse tipo de abordagem em cada método iria só "poluir" visualmente estes, desviando a atenção da lógica da rotina, e desviando-se também das práticas aconselhadas na programação em Ruby. Outro possível efeito secundário seria a ocorrência de algum tipo de "falha silenciosa".
def plus(a, b)
return nil unless a.kind_of?(Integer)
return nil unless b.kind_of?(Integer)
a + b
end
A discussão começou a aquecer, pelo que achei que era melhor rever os conceitos antes de continuar a "bater na cabeça do ceguinho".
Comecemos então pelo conceito acima referenciado de "tipificação fraca na linguagem de programação Ruby". Algo que, para que fique escrito em algum lado, eu repeti (erradamente) várias vezes em diversas ocasiões. Conceptualmente, o que diferencia a tipificação dita "fraca" da dita "forte" é a imposição (ou falta) de restrições na interoperabilidade entre valores de tipos diferentes. De acordo com esta perspectiva, Ruby é uma linguagem com tipificação forte (com algumas excepções). Então porquê a confusão estabelecida no início? Pois, é a falta de declaração do tipo de uma variável. Mas esse já é um conceito diferente. Trata-se agora de um outro conceito: o dinamismo da linguagem. Uma linguagem é considerada de tipificação estática quando o tipo do valor referenciado pelas variáveis é verificado em tempo de compilação, e dinâmica quando este é verificado em tempo real. Em Ruby, uma variável não é mais do que um "ponteiro", cujo valor pode ser qualquer coisa. Daí ser possível fazer coisas em Ruby como:
a = 2
a = "a"
No entanto, algo como isto:
a = 1
a = a + "2"
lançaria uma excepção. (Curiosamente, isto funcionaria em Javascript, o que só aumenta mais o meu desprezo pela especificação de uma linguagem tão útil como pessimamente estruturada).
Logo, concluindo, Ruby é uma linguagem "dinâmica e fortemente tipificada".
Passemos agora para a programação defensiva. O objectivo-base deste conceito é assegurar a continuação do funcionamento de uma componente de software protegendo-se contra mudanças eventuais futuras no código-fonte. Ou seja, eliminar a lei de Murphy equação. Segundo o artigo da Wikipedia sobre o assunto, propõe-se portanto a aumentar a qualidade do código-fonte e reduzir o número de erros, tornar o código-fonte compreensível, a fazer o software comportar-se de uma maneira previsível. Acho isso tudo óptimo. Aliás, acho cada um desses pontos um tema em si, e já um pouco para lá daquilo que a programação defensiva supostamente previne. Mas assim de repente, uns quantos pontos pessoais:
- eliminar a lei de Murphy; bonito, mas... isso é só para Deus e para o Chuck Norris. Existe uma razão para se falar tanto da lei de Murphy - ela é omnipresente;
- erros evitam-se de diversas formas, por exemplo testando o código, deste modo providenciando uma especificação de uso para quem vier pegar nesse código seguidamente;
- melhorar a legibilidade do código é óptimo, e eu sou um apologista disso, mas existem outras formas de complementar a compreensão do código escrito, tal como documentá-lo (ainda sou mais apologista deste ponto);
- fazer o software comportar-se de maneira previsível é um objectivo ambicioso. O que é definido como "previsível"? Que fazer nos casos excepcionais? Será que estes serão automaticamente compreendidos pelas rotinas que têm que interagir com esses valores "previsíveis"? Nestas alturas, é sempre bom vistoriar os requisitos de novo.
Então, voltando ao ponto inicial, é a situação do exemplo descrito no início do texto programação defensiva?
- Se as rotinas que utilizarem aqueles métodos nunca testarem os casos excepcionais, sejam eles valores nulos, excepções customizadas, etc., a lei de Murphy vai aparecer... para além do esforço acrescido de testar os casos excepcionais;
- Ele evitaria mais erros testando e criando uma especificação de uso (como eventualmente fez) para outros programadores, minimizando o impacto do erro humano;
- A legibilidade não foi melhorada, na minha opinião. Aquelas duas linhas que testam se o tipo do valor é um número inteiro toldam a minha vista um pouco (sim, estou a ser sarcasticamente exagerado, mas acho que compreendem onde quero chegar);
- A rotina comporta-se de forma previsível? Pois, não sei. Se o requisito da rotina é identificar os casos em que os valores não são inteiros, e depois tratá-los de forma adequada, tudo bem; caso contrário, o erro persiste. E se eu tiver que tratar sequências de caracteres de forma diferente de números reais? Será que posso usar a rotina com métodos reais? Será que produz um resultado esperado?
Resumindo e concluindo, acho que foi tudo uma boa ideia, mas aquele tratamento de erros não faz bem nem mal, simplesmente não acrescenta nada. Chamei-lhe "programação paranóica".
Sem comentários:
Enviar um comentário