Paradigmas de Programação para arquitetos

Errata: Antes de mais nada, eu acabei me encontrando mais uma vez com o livro "Arquitetura Limpa" do Uncle Bob, um livro que li por volta de 2018-2019 e que, como todo bom livro de programação, tem muitas informações boas para serem extraídas, se tiver um pensamento crítico. Atualmente, fiz um resumo dos capítulos 4, 5 e 6 e decidi compartilhá-los aqui.

A maioria dos programadores, infelizmente, não pensa muito criticamente sobre paradigmas de programação. Na verdade, eles os defendem. Se olharmos de um ângulo social e antropológico, isso faz até certo sentido. Mas aqui, eu gostaria de analisar os ângulos históricos e arquiteturais. Com isso, espero que essa lógica de defender um paradigma perca um pouco o sentido para quem vier a ler este artigo.

Como a matemática e a física moldaram a programação estruturada.

Era março de 1952. Poucos anos se passaram depois do fim da Segunda Guerra e, se vocês lembram um pouco da história da computação, Alan Turing criou o ENIAC para decifrar as cifras do front alemão. No Centro Matemático de Amsterdã, um jovem estudante de matemática chamado Edsger Dijkstra estava começando a trabalhar como o primeiro programador dos Países Baixos. Ele inclusive só decidiu essa profissão porque julgou ter mais desafio intelectual do que física (sim, física. Newton, Einstein e Oppenheimer claramente são fichinhas).

Como falei, eram anos após o final da Segunda Guerra, então Dijkstra começou sua carreira na época dos tubos a vácuo, lentos, frágeis e com programação direta no binário ou assembly cru. Tínhamos a receita para um fluxo de edição, compilação e teste que demorava horas e dias.

É aí que entra a matemática. Dijkstra percebeu que programação é complexa e que programadores não são muito competentes (quem diria). Por causa disso, ele percebeu um problema recorrente: um programa parecia funcionar, mas um detalhe que passou despercebido impede-o de funcionar, o que normalmente é percebido depois de horas ou dias de compilação e testes. Isso fez com que ele implementasse o teorema matemático de prova.

De forma direta, o teorema matemático de prova é uma proposta para construir uma hierarquia Euclidiana de postulados, teoremas, corolários e lemas para os programadores usarem como os matemáticos.

Mas o que é isso?

Não sou matemático, mas ele basicamente falou que os programadores poderiam construir os programas a partir de estruturas comprovadas e as amarrariam com código que provaria que eles estão corretos.

Sim, ele acabou de iniciar toda a lógica de funções, métodos, bibliotecas e testes que conhecemos hoje.

Mas quando ele estava criando esse postulado, ele percebeu que um item amplamente usado nos códigos na época era prejudicial a essa proposta, e esse item é o comando GOTO.

Isso porque os comandos GOTO normalmente quebravam qualquer fluxo que poderia ser seguido e enviariam para outro lugar, impossibilitando a separação em módulos e os tais testes de prova. Na verdade, ele viu que os usos bons de GOTO eram os que correspondiam a estruturas de controle simples como if/then/else e do/while.

Se os módulos tivessem somente esses controles simples, foi comprovado que poderíamos dividir nossos programas em módulos testáveis.

Após lançar um paper defendendo essa ideia, a comunidade de programação entrou em crise. Metade dos programadores defendia Dijkstra enquanto outros defendiam GOTO. Como falei acima, de forma social e antropológica, isso faz sentido. Mas o que devia ser importante para nós programadores é o resultado e, no final, o tempo mostrou que Dijkstra estava correto. Hoje, praticamente não temos GOTO nas linguagens de programação, e quando temos, é de forma bem restrita.

A física entrou mais tarde, quando os programadores descobriram que não podiam usar testes matemáticos para testar software. Isso porque os testes na matemática sempre tratam a verdade de forma absoluta, ou seja, preveem a funcionalidade das funções em todos os âmbitos.

E se você já programou um pouco na vida, sabe que isso é impossível. A própria frase "na minha máquina funciona" é a antítese desse termo.

E os programadores resolveram isso retirando o teste matemático e adicionando o teste científico no lugar.

A ciência, diferente da matemática, busca sempre contradizer a verdade em seus testes, ou seja, busca a falha.

E essa base é usada pelos programadores até hoje nos testes de funções.

Quando escrevemos nossos testes unitários, temos sempre o pensamento de: A minha função nesse cenário e proposta específica funciona?

Programação Orientada a Objetos

A primeira coisa que o livro pergunta é: o que é orientação a objetos? E logo chegamos a três palavras: Encapsulamento, Herança e Polimorfismo. Vamos desmembrá-los.

Encapsulamento

O objetivo do encapsulamento é, na verdade, privar dados e funções de um escopo específico. Em OO, esse escopo é uma classe.

Porém, já em C, tínhamos essa funcionalidade perfeitamente:

Os usuários dessa lib, vamos chamá-la de point.h, não têm acesso aos membros da struct Point. Eles só têm acesso aos métodos que estão no cabeçalho da lib.

Encapsulamento perfeito e funcional sem linguagem OO. Agora vamos ao C++:

Por causa do compilador do C++, os clientes do arquivo header point.h sabem das variáveis-membro x e y! Tivemos que incluir palavras como public, protected e private depois, mas isso foi mais um hack para atender o compilador. Depois, outras linguagens como Java e C# aboliram totalmente o cabeçalho, e se mudássemos o nome de propriedades privadas das bibliotecas, teríamos que recompilar os clientes, quebrando o encapsulamento.

Herança

Se analisarmos o programa main, veremos que a estrutura de dados NamedPoint é a mesma de Point. Ela pode se disfarçar porque é um superconjunto de Point. Isso, pelo que sei, era usado pelos programadores antes do OO e inclusive é o que C++ usa por baixo dos panos para fazer herança.

Ou seja, havia um truque para fazer herança e o que OO fez foi trazer a herança de forma mais conveniente e viabilizar heranças múltiplas.

Polimorfismo

O polimorfismo sempre existiu antes das linguagens OO. Vamos voltar ao C:

A função getchar() lê os dados do STDIN, e putchar() escreve no STDOUT, mas que dispositivos são esses?

Essas funções são exemplos polimórficos porque seus comportamentos mudam dependendo do dispositivo, mas a assinatura não, assim como o estilo de interfaces em Java, C# e Go.

No caso desses dispositivos, o próprio sistema operacional Unix exige que cada driver de I/O ofereça cinco funções padrão: open, close, read, write e seek.

Se criarmos uma classe que tenha essas 5 funções e escreva em um arquivo, ou um array, por exemplo, ela pode ser implementada como STDIN e STDOUT.

E, como mencionei, precisamos analisar um pouco de história aqui para entender o porquê de todos os sistemas operacionais usarem plugins para dispositivos. No final da década de 1950, tiveram uma dura lição de ter que reescrever programas inteiros, não porque a lógica de negócio mudou, mas sim porque mudamos de cartões perfurados para fitas magnéticas. Logo:

Programas devem ser independentes dos dispositivos.

A palavra-chave aqui é "independente", pois antes do polimorfismo, tínhamos uma dependência de chamadas vinculada à camada mais alta do software à mais baixa, começando pela main:

Para arquitetos de software, isso era muito limitante, porque não há nenhuma opção de mudança de fluxo de controle aqui. No entanto, com o polimorfismo, essa dependência é quebrada e as coisas se tornam interessantes.

Agora, a dependência de ML1 aponta para a interface I na direção contrária ao fluxo de controle. Com isso, podemos alterar as dependências do código-fonte sem afetar o fluxo de controle, e vice-versa.

Isso é chamado de inversão de dependência.

Um dos exemplos que podemos fazer com esse novo poder é desacoplar completamente as regras de negócio da interface de usuário e do banco de dados.

Com isso, nossos módulos podem ser desenvolvidos, mantidos e compilados em tempos e formas diferentes. Isso se chama desenvolvimento independente e implantação independente.

Programação funcional

No momento em que estou escrevendo este artigo, a programação funcional está em alta!

Como mencionei na seção sobre programação procedural, há um certo favoritismo por parte de alguns paradigmas em detrimento de outros. No entanto, sinceramente, vejo muitas pessoas defendendo a programação funcional sem compreender verdadeiramente os benefícios e custos associados a ela.

Para a introdução, vamos comparar dois programas que imprimem os quadrados dos primeiros 25 números inteiros, um em Java e outro em Lisp, que é uma linguagem funcional.

Javão da massa

Javão da massa

Lisp

Para deixar mais legível para quem não é acostumado com Lisp:

Programação funcional como um geral trabalham executando funções de dentro para fora, então leia-se:

(fn (* x x)) Esta é uma função anônima que chama a função de multiplicação, passando o argumento de entrada duas vezes.

A função "range" retorna uma lista infinita de números que começa com 0.

Essa lista é passada para o "map", que chama a função anônima mencionada anteriormente.

A lista de quadrados é passada para a função "take", que retorna uma nova lista contendo apenas os primeiros 25.

Em seguida, o "println" imprime o resultado.

Quero destacar algo muito importante aqui: criamos uma variável x em cada iteração, pois na programação funcional, as variáveis não... bem... variam.

Esse conceito se chama imutabilidade e serve para prevenir qualquer erro que possa surgir ao alterar uma variável dentro de um escopo. Com isso, o acesso à memória, disco, processamento e outros recursos fica mais seguro, sem deadlocks, race conditions, etc. No entanto, isso exige mais memória e processamento, na verdade, é inversamente proporcional.

Considerando que não temos memória e processamento infinitos, existem algumas maneiras de ajustar isso. Uma delas é separar os componentes imutáveis dos mutáveis, permitindo que eles se comuniquem. Isso é chamado segregação de mutabilidade.

No caso de termos muito armazenamento e processamento disponíveis, podemos reduzir o estado mutável de nossas aplicações. Dessa forma, paramos de lidar com o estado e passamos a trabalhar com transações.

Em um exemplo básico de uma conta bancária, em vez de consultar o saldo do banco, trabalhamos calculando todas as transações desde a origem ou a partir de um certo ponto de verificação. Isso é chamado Event Sourcing.

Conclusão

Ao analisarmos friamente os três tipos de paradigmas, percebemos que, na verdade, estamos defendendo disciplinas que nos impõem restrições. Nas palavras do próprio livro:

  • A programação estruturada é a disciplina imposta sobre a transferência direta de controle.

  • A programação orientada a objetos é a disciplina imposta sobre a transferência indireta de controle.

  • A programação funcional é a disciplina imposta sobre a atribuição de variáveis.

Todas elas nos impõem limitações, e nenhuma delas adiciona algo novo aos nossos poderes. O que aprendemos em meio século é o que não fazer.

Com isso, concluo que o desenvolvimento de software não é uma tecnologia de rápido desenvolvimento. Estamos fazendo as coisas da mesma maneira hoje que fazíamos há 50 anos, apenas em lugares diferentes, mas a essência é a mesma.

O software é composto apenas por sequência, seleção, iteração e direção.

Referencia Bibliografica:

  • Arquitetura Limpa - Capitulos 4 ao 6 - Robert C. Martin.