A programação processual / funcional não é de modo algum mais fraca que a OOP , mesmo sem entrar em argumentos de Turing (minha linguagem tem poder de Turing e pode fazer qualquer outra coisa que faça), o que não significa muito. Na verdade, as técnicas orientadas a objetos foram primeiramente experimentadas em linguagens que não as incorporavam. Nesse sentido, a programação OO é apenas um estilo específico de programação procedural . Mas ajuda a impor disciplinas específicas, como modularidade, abstração e ocultação de informações, essenciais para a compreensão e manutenção do programa.
Alguns paradigmas de programação evoluem da visão teórica da computação. Uma linguagem como Lisp evoluiu do lambda-calculus e da idéia de meta-circularidade das línguas (semelhante à reflexividade na linguagem natural). As cláusulas de Horn criaram o Prolog e a programação de restrições. A família Algol também deve ao cálculo lambda, mas sem reflexividade embutida.
O Lisp é um exemplo interessante, pois foi testado por muita inovação em linguagem de programação, que é rastreável à sua dupla herança genética.
No entanto, os idiomas evoluem, geralmente sob novos nomes. Um fator importante da evolução é a prática de programação. Os usuários identificam práticas de programação que melhoram as propriedades dos programas, como legibilidade, capacidade de manutenção, probabilidade de correção. Em seguida, eles tentam adicionar aos idiomas recursos ou restrições que darão suporte e às vezes reforçam essas práticas, a fim de melhorar a qualidade dos programas.
O que isso significa é que essas práticas já são possíveis na linguagem de programação mais antiga, mas é preciso compreensão e disciplina para usá-las. Incorporá-los em novas linguagens como conceitos primários com sintaxe específica facilita o uso e a compreensão dessas práticas, principalmente para usuários menos sofisticados (ou seja, a grande maioria). Isso também facilita a vida dos usuários sofisticados.
De alguma forma, é para o design da linguagem o que é um subprograma / função / procedimento para um programa. Depois que o conceito útil é identificado, ele recebe um nome (possivelmente) e uma sintaxe, para que possa ser usado facilmente em todos os programas desenvolvidos com esse idioma. E, quando for bem-sucedido, também será incorporado em idiomas futuros.
Exemplo: recriando a orientação do objeto
Agora tento ilustrar isso em um exemplo (que certamente poderia ser mais polido, com o tempo). O objetivo do exemplo não é mostrar que um programa orientado a objetos pode ser escrito no estilo de programação procedural, possivelmente à custa da lisibilidade e manutenção. Tentarei mostrar que algumas linguagens sem instalações OO podem realmente usar funções de ordem superior e estrutura de dados para criar meios de imitar efetivamente a Orientação a Objetos , a fim de se beneficiar de suas qualidades em relação à organização do programa, incluindo modularidade, abstração e ocultação de informações. .
Como eu disse, o Lisp foi o teste de muita evolução da linguagem, incluindo o paradigma OO (embora o que pudesse ser considerado a primeira linguagem OO fosse o Simula 67, na família Algol). O Lisp é muito simples, e o código para seu intérprete básico é menor que uma página. Mas você pode fazer programação OO no Lisp. Tudo que você precisa é de funções de ordem superior.
Não usarei a sintaxe esotérica do Lisp, mas sim o pseudo-código, para simplificar a apresentação. E considerarei um problema essencial simples: ocultação de informações e modularidade . Definir uma classe de objetos e impedir que o usuário acesse (a maioria) a implementação.
Suponha que eu queira criar uma classe chamada vetor, representando vetores bidimensionais, com métodos que incluem: adição de vetor, tamanho de vetor e paralelismo.
function vectorrec () {
function createrec(x,y) { return [x,y] }
function xcoordrec(v) { return v[0] }
function ycoordrec(v) { return v[1] }
function plusrec (u,v) { return [u[0]+v[0], u[1]+v[1]] }
function sizerec(v) { return sqrt(v[0]*v[0]+v[1]*v[1]) }
function parallelrec(u,v) { return u[0]*v[1]==u[1]*v[0]] }
return [createrec, xcoordrec, ycoordrec, plusrec, sizerec, parallelrec]
}
Então eu posso atribuir o vetor criado aos nomes das funções reais a serem usadas.
[vector, xcoord, ycoord, vplus, vsize, vparallel] = vectorclass ()
Por que ser tão complicado? Porque eu posso definir na função construções intermediárias vectorrec que não quero que sejam visíveis para o resto do programa, de modo a preservar a modularidade.
Podemos fazer outra coleção em coordenadas polares
function vectorpol () {
...
function pluspol (u,v) { ... }
function sizepol (v) { return v[0] }
...
return [createpol, xcoordpol, ycoordpol, pluspol, sizepol, parallelpol]
}
Mas eu posso querer usar indiferentemente as duas implementações. Uma maneira de fazer isso é adicionar um componente de tipo a todos os valores e definir todas as funções acima no mesmo ambiente: Então eu posso definir cada uma das funções retornadas para que primeiro teste o tipo de coordenadas e aplique a função específica por isso.
function vector () {
...
function plusrec (u,v) { ... }
...
function pluspol (u,v) { ... }
...
function plus (u,v) { if u[2]='rec' and v[2]='rec'
then return plusrec (u,v) ... }
return [ ..., plus, ...]
}
O que eu ganhei: as funções específicas permanecem invisíveis (por causa do escopo dos identificadores locais) e o restante do programa pode usar apenas os mais abstratos retornados pela chamada para a classe de vetor.
Uma objeção é que eu poderia definir diretamente cada uma das funções abstratas no programa e deixar dentro da definição das funções dependentes do tipo coordenada. Então estaria escondido também. Isso é verdade, mas o código para cada tipo de coordenada seria cortado em pequenos pedaços espalhados pelo programa, o que é menos redutível e sustentável.
Na verdade, eu nem preciso dar um nome a eles, e eu poderia manter os valores funcionais como anônimos em uma estrutura de dados indexada pelo tipo e uma string que representa o nome da função. Essa estrutura sendo local para o vetor de função seria invisível para o restante do programa.
Para simplificar o uso, em vez de retornar uma lista de funções, posso retornar uma única função chamada apply, tendo como argumento um valor explícito do tipo e uma string, e aplicar a função com o tipo e o nome adequados. Parece muito com chamar um método para uma classe OO.
Vou parar por aqui, nesta reconstrução de uma instalação orientada a objetos.
O que tentei fazer é mostrar que não é muito difícil criar orientação para objetos utilizáveis em uma linguagem suficientemente poderosa, incluindo herança e outros recursos. A metacircularidade do intérprete pode ajudar, mas principalmente em nível sintático, que ainda está longe de ser insignificante.
Os primeiros usuários de orientação a objetos experimentaram os conceitos dessa maneira. E isso geralmente acontece com muitas melhorias nas linguagens de programação. Obviamente, a análise teórica também tem um papel e ajudou a entender ou refinar esses conceitos.
Mas a ideia de que idiomas que não possuem recursos de OO estão fadados ao fracasso em alguns projetos é simplesmente injustificada. Se necessário, eles podem imitar a implementação desses recursos com bastante eficiência. Muitas linguagens têm o poder sintático e semântico de orientar objetos de maneira bastante eficaz, mesmo quando não está embutido. E isso é mais do que um argumento de Turing.
OOP não trata das limitações de outras linguagens, mas suporta ou aplica metodologias de programação que ajudam a escrever programas melhores, ajudando usuários menos experientes a seguir boas práticas que programadores mais avançados estão usando e desenvolvendo sem esse suporte.
Acredito que um bom livro para entender tudo isso possa ser Abelson & Sussman: estrutura e interpretação de programas de computador .