A História Esquecida do OOP
Note: Isto faz parte da série “Composing Software” (agora um livro!) sobre aprendizagem de programação funcional e técnicas de software composicional em JavaScript ES6+ a partir do zero. Fique atento. Há muito mais disto por vir!
Buy the Book | Index | < Previous | Next >
Os paradigmas funcionais e imperativos de programação que usamos hoje foram explorados pela primeira vez matematicamente na década de 1930 com o cálculo lambda e a máquina Turing, que são formulações alternativas de computação universal (sistemas formalizados que podem realizar computação geral). A Tese da Igreja Turing mostrou que o cálculo lambda e as máquinas Turing são funcionalmente equivalentes – que qualquer coisa que possa ser computada usando uma máquina Turing pode ser computada usando cálculo lambda, e vice-versa.
Note: Há um equívoco comum de que as máquinas Turing podem computar qualquer coisa computável. Existem classes de problemas (por exemplo, o problema de paragem) que podem ser computados para alguns casos, mas geralmente não são computáveis para todos os casos que utilizam máquinas Turing. Quando eu uso a palavra “computável” neste texto, quero dizer “computável por uma máquina Turing”.
Lambda calculus representa uma abordagem de cima para baixo, aplicação de função para o cálculo, enquanto que a formulação da máquina Turing com fita adesiva/registro representa uma abordagem de baixo para cima, imperativa (passo-a-passo) para o cálculo.
Linguagens de baixo nível como código de máquina e montagem apareceram nos anos 40, e no final dos anos 50, as primeiras linguagens populares de alto nível apareceram. Os dialetos Lisp ainda hoje são de uso comum, incluindo Clojure, Scheme, AutoLISP, etc. FORTRAN e COBOL apareceram nos anos 50 e são exemplos de linguagens imperativas de alto nível ainda hoje em uso, embora as linguagens C-family tenham substituído tanto COBOL quanto FORTRAN para a maioria das aplicações.
A programação imperativa e a programação funcional têm suas raízes na teoria matemática da computação, predatora dos computadores digitais. “Object-Oriented Programming” (OOP) foi cunhado por Alan Kay por volta de 1966 ou 1967 quando ele estava na escola de graduação.
A aplicação seminal do Sketchpad de Ivan Sutherland foi uma inspiração inicial para o OOP. Foi criado entre 1961 e 1962 e publicado na sua Tese de Tese de Sketchpad em 1963. Os objetos eram estruturas de dados representando imagens gráficas exibidas em uma tela de osciloscópio, e apresentavam herança através de delegados dinâmicos, que Ivan Sutherland chamou de “mestres” em sua tese. Qualquer objeto podia tornar-se um “mestre”, e instâncias adicionais dos objetos eram chamadas de “ocorrências”. Os mestres do Sketchpad compartilham muito em comum com a herança protótipo do JavaScript.
Note: O TX-2 no MIT Lincoln Laboratory foi um dos primeiros usos de um monitor gráfico de computador que empregava interação direta na tela utilizando uma caneta de luz. O EDSAC, que esteve operacional entre 1948-1958, podia exibir gráficos em uma tela. O Whirlwind no MIT tinha uma tela de osciloscópio em funcionamento em 1949. A motivação do projeto era criar um simulador de vôo geral capaz de simular o feedback de instrumentos para múltiplas aeronaves. Isso levou ao desenvolvimento do sistema de computação SAGE. O TX-2 foi um computador de teste para SAGE.
A primeira linguagem de programação amplamente reconhecida como “orientada a objetos” foi Simula, especificada em 1965. Como o Sketchpad, Simula apresentou objetos, e eventualmente introduziu classes, herança de classe, subclasses e métodos virtuais.
Note: Um método virtual é um método definido em uma classe que é projetado para ser substituído por subclasses. Os métodos virtuais permitem a um programa chamar métodos que podem não existir no momento em que o código é compilado, empregando o envio dinâmico para determinar que método concreto invocar em tempo de execução. O JavaScript apresenta tipos dinâmicos e usa a cadeia de delegação para determinar quais métodos invocar, portanto não precisa expor o conceito de métodos virtuais aos programadores. Colocando de outra forma, todos os métodos em JavaScript usam o método runtime dispatch, assim os métodos em JavaScript não precisam ser declarados “virtuais” para suportar a funcionalidade.
“Eu inventei o termo ‘orientado ao objeto’, e posso dizer que não tinha C++ em mente”. ~ Alan Kay, OOPSLA ’97
Alan Kay cunhou o termo “programação orientada a objetos” na escola de graduação em 1966 ou 1967. A grande idéia era usar mini-computadores encapsulados em software que se comunicavam através da passagem de mensagens em vez de compartilhamento direto de dados – para parar de quebrar programas em “estruturas de dados” e “procedimentos” separados.
“O princípio básico do projeto recursivo é fazer com que as partes tenham o mesmo poder do todo”. ~ Bob Barton, o projetista principal do B5000, um mainframe otimizado para rodar Algol-60.
Smalltalk foi desenvolvido por Alan Kay, Dan Ingalls, Adele Goldberg, e outros da Xerox PARC. Smalltalk foi mais orientado a objetos do que Simula – tudo em Smalltalk é um objeto, incluindo classes, inteiros, e blocos (fechamentos). O Smalltalk-72 original não apresentava subclassificação. Isso foi introduzido em Smalltalk-76 por Dan Ingalls.
Enquanto Smalltalk suportava classes e eventualmente subclassificações, Smalltalk não era sobre classes ou coisas de subclassificação. Era uma linguagem funcional inspirada em Lisp assim como em Simula. Alan Kay considera o foco da indústria na subclassificação como uma distração dos verdadeiros benefícios da programação orientada a objetos.
“Desculpe-me por ter cunhado há muito tempo o termo “objetos” para este tópico, pois isso faz com que muitas pessoas se concentrem na idéia menor. A grande idéia é o envio de mensagens”
~ Alan Kay
Numa troca de e-mails de 2003, Alan Kay esclareceu o que ele quis dizer quando chamou Smalltalk de “orientado a objetos”:
“OOP para mim significa apenas envio de mensagens, retenção local e proteção e ocultação de processos de estado, e encadernação extrema tardia de todas as coisas.”
~ Alan Kay
Em outras palavras, de acordo com Alan Kay, os ingredientes essenciais do OOP são:
- Message passing
- Encapsulação
- Aglutinação dinâmica
Notavelmente, a herança e o polimorfismo de subclasse NÃO foram considerados ingredientes essenciais de OOP por Alan Kay, o homem que cunhou o termo e trouxe OOP para as massas.
A Essência de OOP
A combinação de passagem de mensagens e encapsulamento serve a alguns propósitos importantes:
- Evite o estado mutável compartilhado ao encapsular o estado e isolar outros objetos das mudanças de estado local. A única maneira de afetar o estado de outro objeto é pedir (não comandar) a esse objeto para mudá-lo, enviando uma mensagem. As mudanças de estado são controladas em nível local, celular ao invés de serem expostas ao acesso compartilhado.
- Desacoplando objetos entre si – o remetente da mensagem só é acoplado frouxamente ao receptor da mensagem, através da API de mensagens.
- Adaptabilidade e resiliência às mudanças em tempo de execução via encapsulamento tardio. A adaptabilidade em tempo de execução fornece muitos grandes benefícios que Alan Kay considerou essenciais ao OOP.
Estas idéias foram inspiradas por células biológicas e/ou computadores individuais em uma rede através do background de Alan Kay em biologia e influência do design do Arpanet (uma versão inicial da internet). Mesmo assim, Alan Kay imaginou um software a correr num computador gigante e distribuído (a Internet), onde os computadores individuais actuavam como células biológicas, operando independentemente no seu próprio estado isolado, e comunicando através da passagem de mensagens.
“Percebi que a metáfora célula/computador completo se livraria dos dados”
~ Alan Kay
Por “livrar-se dos dados”, Alan Kay estava certamente ciente dos problemas de estado mutável compartilhado e do acoplamento apertado causado pelos dados compartilhados – temas comuns hoje em dia.
Mas no final dos anos 60, os programadores ARPA foram frustrados pela necessidade de escolher um modelo de representação de dados para seus programas, antes de construírem software. Procedimentos que eram muito apertados e acoplados a estruturas de dados particulares não eram resilientes a mudanças. Eles queriam um tratamento mais homogêneo dos dados.
” o objetivo do OOP é não ter que se preocupar com o que está dentro de um objeto. Objetos feitos em máquinas diferentes e com linguagens diferentes devem ser capazes de falar uns com os outros ” ~ Alan Kay
Objetos podem abstrair e esconder implementações de estrutura de dados. A implementação interna de um objeto pode mudar sem quebrar outras partes do sistema de software. De fato, com uma ligação extremamente tardia, um sistema de computador completamente diferente poderia assumir as responsabilidades de um objeto, e o software poderia continuar funcionando. Os objetos, entretanto, poderiam expor uma interface padrão que funcionasse com qualquer estrutura de dados que o objeto pudesse usar internamente. A mesma interface poderia funcionar com uma lista ligada, uma árvore, um fluxo, e assim por diante.
Alan Kay também viu objetos como estruturas algébricas, que fazem certas garantias matematicamente prováveis sobre seus comportamentos:
“Meu fundo matemático me fez perceber que cada objeto poderia ter várias algébricas associadas a ele, e que poderia haver famílias destas, e que estas seriam muito úteis.”
~ Alan Kay
Isto provou ser verdade, e forma a base para objetos como promessas e lentes, ambos inspirados pela teoria da categoria.
A natureza algébrica da visão de Alan Kay para objetos permitiria que os objetos permitissem verificações formais, comportamento determinístico e melhor testabilidade, porque as algébras são essencialmente operações que obedecem a algumas regras na forma de equações.
Na linguagem dos programadores, as algébras são como abstrações compostas de funções (operações) acompanhadas de leis específicas aplicadas por testes unitários que essas funções devem passar (axiomas/equações).
Essas idéias foram esquecidas por décadas na maioria das linguagens C-família OO, incluindo C++, Java, C#, etc, mas estão a começar a encontrar o caminho de volta às versões recentes das linguagens OO mais usadas.
Diz-se que o mundo da programação está a redescobrir os benefícios da programação funcional e do pensamento racional no contexto das linguagens OO.
Como JavaScript e Smalltalk antes, a maioria das linguagens OO modernas estão a tornar-se cada vez mais “linguagens multi-paradigma”. Não há razão para escolher entre programação funcional e OOP. Quando olhamos para a essência histórica de cada uma, elas não são apenas compatíveis, mas idéias complementares.
Porque elas compartilham tantas características em comum, eu gosto de dizer que o JavaScript é a vingança do Smalltalk sobre o mal-entendido mundial do OOP. Both Smalltalk and JavaScript support:
- Objects
- First-class functions and closures
- Dynamic types
- Late binding (functions/methods changeable at runtime)
- OOP without class inheritance
What is essential to OOP (according to Alan Kay)?
- Encapsulation
- Message passing
- Dynamic binding (the ability for the program to evolve/adapt at runtime)
What is non-essential?
- Classes
- Class inheritance
- Special treatment for objects/functions/data
- The
new
keyword - Polymorphism
- Static types
- Recognizing a class as a “type”
If your background is Java or C#, you may be thinking static types and Polymorphism are essential ingredients, but Alan Kay preferred dealing with generic behaviors in algebraic form. For example, from Haskell:
fmap :: (a -> b) -> f a -> f b
This is the functor map signature, which acts generically over unspecified types a
and b
, applying a function from a
to b
in the context of a functor of a
to produce a functor of b
. Functor is math jargon that essentially means “supporting the map operation”. If you’re familiar with .map()
in JavaScript, you already know what that means.
Here are two examples in JavaScript:
// isEven = Number => Boolean
const isEven = n => n % 2 === 0;const nums = ;// map takes a function `a => b` and an array of `a`s (via `this`)
// and returns an array of `b`s.
// in this case, `a` is `Number` and `b` is `Boolean`
const results = nums.map(isEven);console.log(results);
//
The .map()
method is generic in the sense that a
and b
can be any type, and .map()
handles it just fine because arrays are data structures that implement the algebraic functor
laws. Os tipos não importam para .map()
porque não tenta manipulá-los diretamente, em vez disso aplica uma função que espera e retorna os tipos corretos para a aplicação.
// matches = a => Boolean
// here, `a` can be any comparable type
const matches = control => input => input === control;const strings = ;const results = strings.map(matches('bar'));console.log(results);
//
Esta relação genérica de tipo é difícil de expressar corretamente e completamente em uma linguagem como o TypeScript, mas foi muito fácil de expressar nos tipos Hindley Milner do Haskell com suporte para tipos mais altos (tipos de tipos).
Os sistemas de tipos mais restritivos têm sido demasiado restritivos para permitir a livre expressão de ideias dinâmicas e funcionais, tais como composição de funções, composição de objectos livres, extensão de objectos de tempo de execução, combinadores, lentes, etc. Em outras palavras, os tipos estáticos freqüentemente tornam mais difícil escrever software compostável.
Se o seu sistema de tipo for muito restritivo (por exemplo, TypeScript, Java), você é forçado a escrever código mais complicado para atingir os mesmos objetivos. Isso não significa que os tipos estáticos sejam uma má idéia, ou que todas as implementações de tipos estáticos sejam igualmente restritivas. Eu tenho encontrado muito menos problemas com o sistema de tipos do Haskell.
Se você é um fã de tipos estáticos e não se importa com as restrições, mais poder para você, mas se você acha alguns dos conselhos neste texto difícil porque é difícil digitar funções compostas e estruturas algébricas compostas, culpe o sistema de tipos, não as idéias. As pessoas adoram o conforto dos seus SUVs, mas ninguém se queixa que não o deixam voar. Para isso, você precisa de um veículo com mais graus de liberdade.
Se as restrições tornam o seu código mais simples, ótimo! Mas se as restrições o forçarem a escrever código mais complicado, talvez as restrições estejam erradas.
O que é um Objecto?
Os objectos têm claramente assumido muitas conotações ao longo dos anos. O que chamamos de “objetos” em JavaScript são simplesmente tipos de dados compostos, sem nenhuma das implicações da programação baseada em classes ou da passagem de mensagens de Alan Kay.
Em JavaScript, esses objetos podem e freqüentemente suportam encapsulamento, passagem de mensagens, compartilhamento de comportamento através de métodos, até mesmo polimorfismo de subclasse (embora usando uma cadeia de delegação em vez de despacho baseado em tipo). Você pode atribuir qualquer função a qualquer propriedade. Você pode construir comportamentos de objetos dinamicamente, e alterar o significado de um objeto em tempo de execução. O JavaScript também suporta encapsulamento usando fechamentos para privacidade de implementação. Mas tudo isso é comportamento opt-in.
Nossa idéia atual de um objeto é simplesmente uma estrutura de dados composta, e não requer nada mais para ser considerado um objeto. Mas programar usando esses tipos de objetos não torna seu código “orientado a objetos” mais do que programar com funções torna seu código “funcional”.
OOP não é mais o OOP real
Porque “objeto” em linguagens modernas de programação significa muito menos do que fez com Alan Kay, estou usando “componente” ao invés de “objeto” para descrever as regras do OOP real. Muitos objectos são propriedade e manipulados directamente por outro código em JavaScript, mas os componentes devem encapsular e controlar o seu próprio estado.
OOP real significa:
- Programação com componentes (o “objeto” de Alan Kay)
- O estado do componente deve ser encapsulado
- Utilizar passagem de mensagem para comunicação entre objetos
- Componentes podem ser adicionados/alterados/substituídos em tempo de execução
Comportamentos da maioria dos componentes podem ser especificados genericamente usando estruturas de dados algébricas. A herança não é necessária aqui. Os componentes podem reutilizar comportamentos de funções compartilhadas e importações modulares sem compartilhar seus dados.
Manipular objetos ou usar herança de classe em JavaScript não significa que você está “fazendo OOP”. Usar componentes desta forma significa. Mas o uso popular é como as palavras são definidas, então talvez devêssemos abandonar o OOP e chamar isso de “Programação Orientada a Mensagens (MOP)” ao invés de “Programação Orientada a Objetos (OOP)”?
É coincidência que mops são usados para limpar messes?
Que Bom MOP Parece
Na maioria dos softwares modernos, há alguma IU responsável por gerenciar as interações dos usuários, algum código gerenciando o estado da aplicação (dados do usuário), e código gerenciando a E/S do sistema ou rede.
Cada um desses sistemas pode requerer processos de longa duração, tais como ouvintes de eventos, estado para acompanhar coisas como a conexão de rede, estado do elemento ui, e o próprio estado da aplicação.
Bom MOP significa que ao invés de todos esses sistemas alcançarem e manipularem diretamente o estado um do outro, o sistema se comunica com outros componentes através do envio de mensagens. Quando o usuário clica em um botão de salvar, uma mensagem "SAVE"
pode ser despachada, que um componente de estado da aplicação pode interpretar e retransmitir para um manipulador de atualização de estado (tal como uma função redutora pura). Talvez após o estado ter sido atualizado, o componente de estado pode enviar uma mensagem "STATE_UPDATED"
para um componente da IU, que por sua vez interpretará o estado, reconciliará quais partes da IU precisam ser atualizadas, e retransmitirá o estado atualizado para os subcomponentes que lidam com aquelas partes da IU.
Mean enquanto isso, o componente de conexão de rede pode estar monitorando a conexão do usuário para outra máquina na rede, escutando mensagens, e enviando representações de estado atualizadas para salvar dados em uma máquina remota. Internamente, ele está mantendo o controle de um temporizador de batimento cardíaco da rede, se a conexão está atualmente online ou offline, e assim por diante.
Estes sistemas não precisam saber sobre os detalhes das outras partes do sistema. Apenas sobre as suas preocupações individuais e modulares. Os componentes do sistema são decomponíveis e recomponíveis. Eles implementam interfaces padronizadas para que eles sejam capazes de interoperar. Desde que a interface esteja satisfeita, você pode substituir substituições que podem fazer a mesma coisa de maneiras diferentes, ou coisas completamente diferentes com as mesmas mensagens. Você pode até fazer isso em tempo de execução, e tudo continuaria funcionando corretamente.
Componentes do mesmo sistema de software podem nem precisar estar localizados na mesma máquina. O sistema poderia ser descentralizado. O armazenamento em rede poderia fragmentar os dados em um sistema de armazenamento descentralizado como o IPFS, de modo que o usuário não dependesse da saúde de nenhuma máquina em particular para garantir o backup de seus dados com segurança, e a salvo de hackers que poderiam querer roubá-los.
OOP foi parcialmente inspirado pelo Arpanet, e um dos objetivos do Arpanet era construir uma rede descentralizada que pudesse ser resiliente a ataques como bombas atômicas. Segundo o diretor da DARPA durante o desenvolvimento da Arpanet, Stephen J. Lukasik (“Why the Arpanet Was Built”):
“O objetivo era explorar novas tecnologias de computador para atender às necessidades do comando e controle militar contra ameaças nucleares, conseguir o controle sobrevivente das forças nucleares dos EUA e melhorar a tática militar e a tomada de decisões de gestão.”
Nota: O principal impulso do Arpanet foi a conveniência e não a ameaça nuclear, e suas óbvias vantagens de defesa surgiram mais tarde. ARPA estava usando três terminais de computador separados para se comunicar com três projetos de pesquisa de computador separados. Bob Taylor queria uma única rede de computadores para conectar cada projeto com os outros.
Um bom sistema MOP poderia compartilhar a robustez da internet usando componentes que são hot-swappable enquanto a aplicação está rodando. Poderia continuar a funcionar se o usuário estiver em um telefone celular e eles ficarem offline porque entraram em um túnel. Pode continuar a funcionar se um furacão derruba a energia de um dos centros de dados onde os servidores estão localizados.
Está na hora do mundo do software largar a experiência da herança de classe falhada, e abraçar os princípios matemáticos e científicos que originalmente definiram o espírito do OOP.
Está na hora de começarmos a construir software mais flexível, mais resiliente e melhor composto, com o MOP e a programação funcional funcionando em harmonia.
Nota: A sigla MOP já é usada para descrever “programação orientada a monitoração” e seu improvável OOP vai embora silenciosamente.
Não fique chateado se o MOP não pegar como linguagem de programação.
Faça MOPs.
Saiba mais em EricElliottJS.com
Aulas de vídeo sobre programação funcional estão disponíveis para membros de EricElliottJS.com. Se você não é um membro, inscreva-se hoje.