Back-End

6 fev, 2017

O caminho das trevas

Publicidade

Ao longo dos últimos meses, eu me interessei por duas novas linguagens: Swift e Kotlin. Elas duas têm uma série de semelhanças. Na verdade, as semelhanças são tão rígidas que eu me pergunto se essa não é uma nova tendência na nossa linguagem churn. Se assim for, é um caminho das trevas.

Ambas as linguagens integraram algumas características funcionais. Por exemplo, ambas têm lambdas. Isso é uma coisa boa, em geral. Quanto mais aprendemos sobre programação funcional, melhor. Essas linguagens estão muito distantes de uma linguagem de programação verdadeiramente funcional, mas cada passo nessa direção é um bom passo.

Meu problema é que ambas as linguagens dobraram para baixo na tipagem estática forte. Ambas parecem ter a intenção de fechar todo o single type hole em suas linguagens pais. No caso da Swift, a linguagem pai é o bizarro híbrido sem tipagem de C e Smalltalk chamado Objective-C; então, talvez, a ênfase na tipagem seja compreensível. No caso de Kotlin, o pai é o Java já bastante fortemente tipado.

Agora, eu não quero que você pense que eu sou contra as linguagens estaticamente tipadas. Eu não sou. Existem vantagens definitivas para as linguagens dinâmicas e estáticas, e eu felizmente uso ambos os tipos. Eu tenho uma pequena preferência por tipagem dinâmica e, por isso, eu uso um pouco de Clojure. Por outro lado, eu provavelmente escrevo mais Java do que Clojure. Assim, você pode me considerar um bi-typical. Eu ando em ambos os lados da rua – por assim dizer.

Não é o fato de que Swift e Kotlin sejam estatisticamente tipadas que me tem preocupado. Pelo contrário, é a profundidade dessa tipaem estática.

Eu não chamaria Java de uma linguagem fortemente opinativa quando se trata de tipagem estática. Você pode criar estruturas em Java que seguem bem as regras da tipagem; mas você também pode violar muitas das regras de tipagem sempre que quiser ou precisar. A linguagem se queixa um pouco quando você faz isso e lança algumas barreiras, mas não tantas que chegue a ser obstrucionista.

Swift e Kotlin, por outro lado, são completamente inflexíveis quando se trata de suas regras de tipagem. Por exemplo, em Swift, se você declarar uma função para lançar uma exception, então por Deus cada chamada para essa função, todo o caminho até a pilha, deve ser adornada com um bloqueio do-try, ou um try!, ou try?. Não há nenhuma maneira, nessa linguagem, de silenciosamente lançar uma exceção em direção ao nível superior sem pavimentar um super-caminho para ele através de toda a árvore de chamada (você pode assistir a Justin e eu lutando com isso em nossos vídeos Mobile Application Case Study).

Agora, talvez você pense que isso é uma coisa boa. Talvez você ache que tem havido um monte de erros em sistemas que resultaram de exceções não-encurraladas. Talvez você pense que as exceções que não são acompanhadas, passo a passo, até a pilha de chamada são arriscadas e sujeitas a erros. E, claro, você estaria certo sobre isso. As exceções não declaradas e não gerenciadas são muito arriscadas.

A questão é: de quem é o trabalho de gerenciar esse risco? É trabalho da linguagem? Ou é trabalho do programador?

No Kotlin, você não pode derivar de uma classe ou substituir uma função, a menos que você adorne essa classe ou função como open. Você também não pode substituir uma função a menos que a função de substituição seja adornada com override. Se você negligenciar adornar uma classe com open, a linguagem não permitirá que você derive dela.

Agora, talvez você ache que isso é uma coisa boa. Talvez você acredite que hierarquias de herança e derivação que são permitidas crescer sem limite são uma fonte de erro e risco. Talvez você ache que podemos eliminar classes inteiras de erros forçando os programadores a declararem explicitamente que suas classes estão open. E você pode estar certo. Derivação e herança são coisas arriscadas. Muito pode dar errado quando você substitui uma função em uma classe derivada.

A questão é: de quem é o trabalho gerenciar esse risco? É trabalho da linguagem? Ou é trabalho do programador.

Tanto Swift como Kotlin incorporaram o conceito de tipos nullable. O fato de que uma variável pode conter um null torna-se parte da tipagem dessa variável. Uma variável do tipo String não pode conter um null; ela só pode conter uma String reificada. Por outro lado, uma variável do tipo String? tem um tipo nullable e pode conter um null.

As regras da linguagem insistem que quando você usa uma variável nullable, você deve primeiro verificar essa variável para null. Então, se s é uma String?, então var l = s.length() não compilará. Em vez disso, você tem que dizer var l = s.length() ?: 0 ou var l = if (s!=null) s.length() else 0.

Talvez você pense que isso é uma coisa boa. Talvez você tenha visto NPES suficientes em sua vida. Talvez você saiba, sem sombra de dúvida, que nulls sem controle são a causa de bilhões e bilhões de dólares de falhas de software (na verdade, a documentação da Kotlin chama o NPE de “Billion Dollar Bug” – “Erro de um bilhão de dólares”). E, claro, você está certo. É muito arriscado ter nulls rampaging em torno do sistema fora de controle.

A questão é: de quem é o trabalho é gerenciar os nulls? Da linguagem? Ou do programador?

Essas linguagens são como o pequeno garoto holandês colando os dedos no dique. Cada vez que há um novo tipo de erro, adicionamos um recurso de linguagem para evitar esse tipo de erro. E assim essas línguas acumulam cada vez mais e mais dedos em buracos em diques. O problema é que, eventualmente, você fica sem dedos das mãos e dos pés.

Mas antes de você ficar sem os dedos das mãos e dos pés, você criou linguagens que contêm dezenas de palavras-chave, centenas de restrições, uma sintaxe tortuosa e um manual de referência que se lê como um livro de legislação. Na verdade, para se tornar um especialista nessas linguagens, você deve se tornar um advogado de linguagem (um termo que foi inventado durante a era C ++).

Esse é o caminho errado!

Pergunte a si mesmo por que estamos tentando conectar defeitos com recursos de linguagem. A resposta deve ser óbvia. Nós estamos tentando conectar esses defeitos porque eles ocorrem com demasiada frequência.

Agora, pergunte a si mesmo por que esses defeitos acontecem com tanta frequência. Se sua resposta for que nossas linguagens não os impedem, então eu sugiro fortemente que você pare seu trabalho e nunca mais pense em ser um programador novamente; porque os defeitos nunca são culpa das nossas linguagens. Os defeitos são culpa dos programadores. São os programadores que criam defeitos – não as linguagens.

E o que é que os programadores devem fazer para evitar defeitos? Eu vou te dar uma suposição. Aqui estão algumas sugestões. É um verbo. Começa com um “T”. Sim. Você entendeu. TESTE!

Você testa para que o seu sistema não emita nulls inesperados. Você testa para que seu sistema manipule nulls em suas entradas. Você testa para que cada exceção que você possa jogar seja pega em algum lugar.

Por que essas linguagens estão adotando todas essas características? Porque os programadores não estão testando seu código. E porque os programadores não estão testando seu código, agora temos linguagens que nos obrigam a colocar a palavra open na frente de cada classe que queremos derivar. Agora temos linguagens que nos obrigam a adornar cada função, ao longo de todo o caminho até a árvore de chamada, com try!. Agora temos linguagens que são tão fechadas e tão especificadas, que você tem que projetar todo o sistema na frente antes que você possa codificar qualquer um.

Considere: Como posso saber se uma classe está open ou não? Como eu sei se, em algum lugar abaixo da árvore de chamada, alguém pode lançar uma exceção? Quanto código eu terei que mudar quando finalmente descobrir que alguém realmente precisa retornar um null até a árvore de chamada?

Todas essas restrições, que essas linguagens estão impondo, pressupõem que o programador tem perfeito conhecimento do sistema, antes de o sistema ser escrito. Eles presumem que você sabe que classes terão que ser open e quais não vão. Eles presumem que você sabe quais caminhos de chamada vão lançar exceções, e quais não. Elas presumem que você sabe quais funções produzirão null e quais não.

E por causa de toda essa presunção, elas o punem quando você está errado. Elas forçam você a voltar e mudar quantidades maciças de código, adicionando try!, ?: ou open ao longo de todo o caminho até a pilha.

E como você evita ser punido? Existem duas maneiras. Um que funciona e uma que não. A que não funciona é projetar tudo antes da codificação. A única que realmente evita a punição é anular todas as precauções.

E assim você declarará todas as suas classes e todas as suas funções como open. Você nunca usará exceções. E você vai se acostumar a usar muitos e muitos caracteres ! para substituir as verificações null e permitir NPES rampage através de seus sistemas.

Por que a central nuclear de Chernobyl pegou fogo, derreteu, destruiu uma pequena cidade e deixou uma grande área inabitável? Eles ignoraram todas as precauções. Portanto, não dependemos de precauções para evitar catástrofes. Em vez disso, é melhor você se acostumar a escrever muitos e muitos testes, não importando qual linguagem você estiver usando!

***

Uncle Bob faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://blog.cleancoder.com/uncle-bob/2017/01/11/TheDarkPath.html.