A história esquecida da POO

Eric Elliott Blocked Unblock Seguir Seguindo 31 de outubro de 2018 Fumaça Art Cubes to Smoke – Mattys Flicks – (CC BY 2.0)

Nota: Esta é parte da “Compor Software” serie s (agora um livro!) Sobre a aprendizagem de programação funcional e técnicas de software de composição em JavaScript ES6 + a partir do zero. Fique ligado. Há muito mais disso por vir!
<Anterior | << Começar de novo na Parte 1

Os paradigmas de programação funcional e imperativa que usamos hoje foram explorados matematicamente na década de 1930 com o cálculo lambda e a máquina de Turing, que são formulações alternativas de computação universal (sistemas formalizados que podem realizar cálculos gerais). A Tese de Turing da Igreja mostrou que o cálculo lambda e as máquinas de Turing são funcionalmente equivalentes – que qualquer coisa que possa ser calculada usando uma máquina de Turing pode ser calculada usando cálculo lambda e vice-versa.

Nota: Existe um equívoco comum que as máquinas de Turing podem computar qualquer coisa computável. Existem classes de problemas (por exemplo, o problema de parada ) que podem ser computáveis em alguns casos, mas geralmente não são computáveis para todos os casos que usam máquinas de Turing. Quando eu uso a palavra “computável” neste texto, quero dizer “computável por uma máquina de Turing”.

O cálculo lambda representa uma abordagem de aplicação de função de cima para baixo para computação, enquanto a formulação ticker tape / register machine da máquina de Turing representa uma abordagem ascendente, imperativa (passo-a-passo) para computação.

Idiomas de baixo nível como código de máquina e montagem apareceram na década de 1940 e, no final da década de 1950, surgiram as primeiras linguagens populares de alto nível. Os dialetos Lisp ainda estão em uso comum hoje, incluindo Clojure, Scheme, AutoLISP, etc. FORTRAN e COBOL apareceram na década de 1950 e são exemplos de linguagens imperativas de alto nível ainda em uso atualmente, embora as linguagens da família C tenham substituído COBOL e FORTRAN para a maioria das aplicações.

Tanto a programação imperativa quanto a programação funcional têm suas raízes na matemática da teoria da computação, antecedendo os computadores digitais. "Programação orientada a objeto" (OOP) foi cunhada por Alan Kay por volta de 1966 ou 1967, quando ele estava na faculdade.

A aplicação seminal de Sketchpad de Ivan Sutherland foi uma inspiração inicial para OOP. Foi criado entre 1961 e 1962 e publicado em sua 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 via delegados dinâmicos, que Ivan Sutherland chamou de "mestres" em sua tese. Qualquer objeto poderia se tornar 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 prototípica do JavaScript.

Nota: O TX-2 no MIT Lincoln Laboratory foi um dos primeiros usos de um monitor gráfico de computador que emprega interação direta na tela usando uma caneta de luz. O EDSAC, que estava operacional entre 1948 e 1958, podia exibir gráficos em uma tela. O Whirlwind no MIT tinha uma tela de osciloscópio funcionando em 1949. A motivação do projeto era criar um simulador de vôo geral capaz de simular o feedback do instrumento para várias aeronaves. Isso levou ao desenvolvimento do sistema de computação SAGE. O TX-2 era um computador de teste para o 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 classes, subclasses e métodos virtuais.

Nota: Um método virtual é um método definido em uma classe projetada para ser substituída por subclasses. Os métodos virtuais permitem que um programa chame métodos que podem não existir no momento em que o código é compilado, empregando o despacho dinâmico para determinar qual método concreto invocar no 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 a programadores. Em outras palavras, todos os métodos em JavaScript usam o método de envio em tempo de execução, portanto, os métodos em JavaScript não precisam ser declarados como “virtuais” para suportar o recurso.

A grande ideia

“Eu inventei o termo 'orientado a objetos', e posso dizer que não tenho C ++ em mente.” ~ Alan Kay, OOPSLA '97

Alan Kay cunhou o termo “programação orientada a objeto” na graduação em 1966 ou 1967. A grande ideia era usar mini-computadores encapsulados em software que se comunicavam através de passagem de mensagens em vez de compartilhamento direto de dados – parar de dividir programas em “dados separados”. estruturas ”e“ procedimentos ”.

“O princípio básico do design recursivo é fazer com que as partes tenham o mesmo poder que o todo.” ~ Bob Barton, o principal projetista do B5000, um mainframe otimizado para executar o Algol-60.

O Smalltalk foi desenvolvido por Alan Kay, Dan Ingalls, Adele Goldberg e outros na Xerox PARC. O Smalltalk era mais orientado a objetos do que Simula – tudo em Smalltalk é um objeto, incluindo classes, inteiros e blocos (closures). O Smalltalk-72 original não apresentava subclasses. Isso foi introduzido em Smalltalk-76 por Dan Ingalls .

Enquanto Smalltalk suportava classes e, eventualmente, subclasse, Smalltalk não era sobre classes ou subclasses de coisas. Era uma linguagem funcional inspirada em Lisp e em Simula. Alan Kay considera que o foco da indústria na subclasse é uma distração dos verdadeiros benefícios da programação orientada a objetos.

“Sinto muito que há muito tempo cunhei o termo“ objetos ”para esse tópico porque faz com que muitas pessoas se concentrem na ideia menor. A grande ideia é enviar mensagens.
~ Alan Kay

Em uma troca de e-mails em 2003 , Alan Kay esclareceu o que ele queria dizer quando chamou Smalltalk de "orientado a objetos":

"OOP para mim significa apenas mensagens, retenção local e proteção e ocultação de processos de estado e extrema ligação tardia de todas as coisas."
~ Alan Kay

Em outras palavras, de acordo com Alan Kay, os ingredientes essenciais da OOP são:

  • Passagem de mensagem
  • Encapsulamento
  • Ligação dinâmica

Notavelmente, o polimorfismo de herança e subclasse não foram considerados ingredientes essenciais da OOP por Alan Kay, o homem que cunhou o termo e trouxe OOP para as massas.

A essência da OOP

A combinação de passagem de mensagens e encapsulamento serve para alguns propósitos importantes:

  • Evitar o estado mutável compartilhado , encapsulando o estado e isolando outros objetos das alterações de estado locais. A única maneira de afetar o estado de outro objeto é pedir (não comandar) esse objeto para alterá-lo enviando uma mensagem. Mudanças de estado são controladas em nível local, celular, em vez de expostas a acesso compartilhado.
  • Desacoplar objetos um do outro – o remetente da mensagem é apenas fracamente acoplado ao receptor da mensagem, através da API do sistema de mensagens.
  • Adaptabilidade e resiliência a mudanças em tempo de execução via ligação tardia. A adaptabilidade em tempo de execução fornece muitos grandes benefícios que Alan Kay considerava essenciais para a POO.

Essas idéias foram inspiradas por células biológicas e / ou computadores individuais em uma rede através do histórico de Alan Kay em biologia e influência do design da Arpanet (uma versão inicial da internet). Mesmo assim, Alan Kay imaginou um software rodando em um computador gigante e distribuído (a internet), onde computadores individuais agiam como células biológicas, operando independentemente em seu próprio estado isolado e se comunicando via transmissão de mensagens.

“Percebi que a metáfora célula / computador inteiro se livraria dos dados […]”
~ Alan Kay

Por "livrar-se dos dados", Alan Kay certamente estava ciente dos problemas de estado mutáveis compartilhados e do forte acoplamento causado por dados compartilhados – temas comuns hoje em dia.

Mas no final dos anos 1960, os programadores do ARPA estavam frustrados com a necessidade de escolher uma representação de modelo de dados para seus programas antes de construir um software. Procedimentos que eram muito fortemente acoplados a estruturas de dados particulares não eram resilientes a mudanças. Eles queriam um tratamento mais homogêneo dos dados.

“[…] o objetivo da OOP não é ter que se preocupar com o que está dentro de um objeto. Objetos feitos em máquinas diferentes e com idiomas diferentes devem ser capazes de falar uns com os outros […] ”~ Alan Kay

Os objetos podem abstrair e ocultar as implementações da estrutura de dados. A implementação interna de um objeto pode mudar sem quebrar outras partes do sistema de software. De fato, com a ligação tardia extrema, um sistema de computador totalmente diferente poderia assumir as responsabilidades de um objeto, e o software poderia continuar funcionando. Objetos, enquanto isso, podem expor uma interface padrão que funciona com qualquer estrutura de dados que o objeto tenha usado internamente. A mesma interface pode funcionar com uma lista vinculada, uma árvore, um fluxo e assim por diante.

Alan Kay também viu objetos como estruturas algébricas, que fornecem certas garantias matematicamente prováveis sobre seus comportamentos:

“Meu histórico de matemática me fez perceber que cada objeto poderia ter várias álgebras associadas a ele, e poderia haver famílias delas, e que elas seriam muito úteis”.
~ Alan Kay

Isso se provou verdadeiro e forma a base para objetos como promessas e lentes, ambos inspirados pela teoria de categorias.

A natureza algébrica da visão de Alan Kay para objetos permitiria que os objetos fornecessem verificações formais, comportamento determinístico e melhor testabilidade, porque álgebras são essencialmente operações que obedecem a algumas regras na forma de equações.

No jargão do programador, as álgebras são como abstrações compostas de funções (operações) acompanhadas por leis específicas impostas por testes de unidade que essas funções devem passar (axiomas / equações).

Essas ideias foram esquecidas durante décadas na maioria das linguagens OO da família C, incluindo C ++, Java, C #, etc., mas estão começando a encontrar o caminho de volta para as versões recentes das linguagens OO mais utilizadas.

Você pode dizer que o mundo da programação está redescobrindo os benefícios da programação funcional e do pensamento racional no contexto das linguagens OO.

Como o JavaScript e Smalltalk antes, a maioria das linguagens OO modernas estão se tornando cada vez mais “linguagens multi-paradigmáticas”. Não há razão para escolher entre programação funcional e OOP. Quando olhamos para a essência histórica de cada um, eles não são apenas idéias compatíveis, mas complementares.

Como eles compartilham muitos recursos em comum, eu gosto de dizer que o JavaScript é a vingança da Smalltalk contra a incompreensão do mundo da OOP. Suporte para Smalltalk e JavaScript:

  • Objetos
  • Funções e fechamentos de primeira classe
  • Tipos dinâmicos
  • Ligação tardia (funções / métodos variáveis em tempo de execução)
  • OOP sem herança de classe

O que é essencial para a POO (de acordo com Alan Kay)?

  • Encapsulamento
  • Passagem de mensagem
  • Ligação dinâmica (a capacidade do programa de evoluir / adaptar em tempo de execução)

O que não é essencial?

  • Classes
  • Herança de classe
  • Tratamento especial para objetos / funções / dados
  • A new palavra-chave
  • Polimorfismo
  • Tipos estáticos
  • Reconhecendo uma classe como um "tipo"

Se seu background é Java ou C #, você pode estar pensando que tipos estáticos e polimorfismo são ingredientes essenciais, mas Alan Kay preferiu lidar com comportamentos genéricos de forma algébrica. Por exemplo, de Haskell:

 fmap :: (a -> b) -> fa -> fb 

Este é o mapa de assinatura functor, que atua genericamente sobre tipos não especificados a e b , aplicando uma função a partir a para b no contexto de um functor de a para produzir um functor de b . Functor é um jargão matemático que significa essencialmente "apoiar a operação do mapa". Se você estiver familiarizado com [].map() em JavaScript, você já sabe o que isso significa.

Aqui estão dois exemplos em JavaScript:

 // isEven = Number => Boolean 
const isEven = n => n% 2 === 0;
 const nums = [1, 2, 3, 4, 5, 6]; 
 // map tem uma função `a => b` e uma matriz de` a`s (via `this`) 
// e retorna uma matriz de `b`s.
// neste caso, `a` é` Number` e `b` e` Boolean`
resultados const = nums.map (isEven);
 console.log (resultados); 
// [falso, verdadeiro, falso, verdadeiro, falso, verdadeiro]

O método .map() é genérico no sentido de que a e b podem ser de qualquer tipo, e .map() lida bem com isso porque matrizes são estruturas de dados que implementam as leis de functor algébricas. Os tipos não importam para .map() porque ele não tenta manipulá-los diretamente, em vez disso, aplica uma função que espera e retorna os tipos corretos para o aplicativo.

 // matches = a => booleano 
// aqui, `a` pode ser qualquer tipo comparável
const matches = controle => entrada => entrada === controle;
 strings const = ['foo', 'bar', 'baz']; 
 resultados const = strings.map (matches ('bar')); 
 console.log (resultados); 
// [false, true, false]

Esse tipo de relacionamento genérico é difícil de expressar corretamente e completamente em uma linguagem como o TypeScript, mas era bem fácil de expressar nos tipos Hindley Milner de Haskell, com suporte para tipos maiores (tipos de tipos).

A maioria dos sistemas de tipos tem sido muito restritivos para permitir a livre expressão de idéias dinâmicas e funcionais, como composição de funções, composição de objetos livres, extensão de objetos em tempo de execução, combinadores, lentes etc. Em outras palavras, tipos estáticos freqüentemente dificultam escrever composable Programas.

Se o seu sistema de tipos é muito restritivo (por exemplo, TypeScript, Java), você é forçado a escrever um código mais complicado para atingir os mesmos objetivos. Isso não significa que os tipos estáticos são uma má ideia, ou que todas as implementações do tipo estático são igualmente restritivas. Eu encontrei muito menos problemas com o sistema de tipos de Haskell.

Se você é um fã de tipos estáticos e não se importa com as restrições, mais poder para você, mas se achar que alguns dos conselhos neste texto são difíceis porque é difícil digitar funções compostas e estruturas algébricas compostas, culpe o tipo sistema, não as idéias. As pessoas adoram o conforto de seus SUVs, mas ninguém reclama que elas não permitem que você voe. Para isso, você precisa de um veículo com mais graus de liberdade.

Se as restrições tornarem o seu código mais simples, ótimo! Mas se as restrições forçam você a escrever códigos mais complicados, talvez as restrições estejam erradas.

O que é um objeto?

Objetos assumiram claramente 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 transmissão de mensagens de Alan Kay.

Em JavaScript, esses objetos podem e freqüentemente suportam encapsulamento, passagem de mensagens, compartilhamento de comportamento via métodos, até mesmo polimorfismo de subclasse (embora usando uma cadeia de delegação ao invés de um despacho baseado em tipo). Você pode atribuir qualquer função a qualquer propriedade. Você pode criar comportamentos de objetos dinamicamente e alterar o significado de um objeto em tempo de execução. O JavaScript também suporta o encapsulamento usando closures para privacidade da 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 a programação usando esses tipos de objetos não torna seu código “orientado a objeto”, assim como programar com funções torna seu código “funcional”.

OOP não é mais real OOP

Como "objeto" nas linguagens de programação modernas significa muito menos do que para Alan Kay, estou usando "componente" em vez de "objeto" para descrever as regras da POO real. Muitos objetos pertencem e são manipulados diretamente por outro código em JavaScript, mas os componentes devem encapsular e controlar seu próprio estado.

OOP real significa:

  • Programando com componentes (“objeto” de Alan Kay)
  • O estado do componente deve ser encapsulado
  • Usando a passagem de mensagens para comunicação entre objetos
  • Componentes podem ser adicionados / alterados / substituídos em tempo de execução

A maioria dos comportamentos de componentes pode ser especificada genericamente usando estruturas de dados algébricos. 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 classes em JavaScript não significa que você esteja "fazendo OOP". Usando componentes desta forma faz. Mas o uso popular é como as palavras são definidas, então talvez devêssemos abandonar a POO e chamar isso de "Programação Orientada por Mensagens (MOP)" em vez de "Programação Orientada a Objetos (POO)"?

É coincidência que esfregões são usados para limpar bagunças?

Que bom MOP parece

Na maioria dos softwares modernos, há uma interface do usuário responsável por gerenciar as interações do usuário, algum código gerenciando o estado do aplicativo (dados do usuário) e sistema de gerenciamento de código ou E / S de rede.

Cada um desses sistemas pode exigir processos de longa duração, como ouvintes de eventos, estado para acompanhar coisas como a conexão de rede, o status do elemento ui e o próprio estado do aplicativo.

Um bom MOP significa que, em vez de todos esses sistemas se aproximarem e manipularem diretamente o estado do outro, o sistema se comunica com outros componentes por meio do envio de mensagens. Quando o usuário clica em um botão salvar, uma mensagem "SAVE" pode ser despachada, o que um componente de estado do aplicativo pode interpretar e retransmitir para um manipulador de atualização de estado (como uma função de redutor puro). Talvez depois que o estado tiver sido atualizado, o componente de estado possa enviar uma mensagem "STATE_UPDATED" para um componente de UI, que por sua vez interpretará o estado, reconciliará quais partes da UI precisarão ser atualizadas e retransmitirá o estado atualizado para os subcomponentes que manipulam essas partes da interface do usuário.

Enquanto isso, o componente de conexão de rede pode monitorar a conexão do usuário com outra máquina na rede, ouvir mensagens e despachar representações de estado atualizadas para salvar dados em uma máquina remota. Ele está monitorando internamente um cronômetro de pulsação da rede, se a conexão está atualmente on-line ou off-line e assim por diante.

Esses sistemas não precisam conhecer os detalhes das outras partes do sistema. Apenas sobre suas preocupações individuais e modulares. Os componentes do sistema são decomponíveis e recomponíveis. Eles implementam interfaces padronizadas para que possam interoperar. Contanto que a interface seja satisfeita, você poderia substituir as 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 pode ser descentralizado. O armazenamento de rede pode fragmentar os dados em um sistema de armazenamento descentralizado como o IPFS , de modo que o usuário não dependa da integridade de qualquer máquina específica para garantir o backup seguro de seus dados e de hackers que possam roubá-los.

A POO foi parcialmente inspirada pela Arpanet, e um dos objetivos da Arpanet era construir uma rede descentralizada que pudesse ser resiliente a ataques como bombas atômicas. De acordo com o diretor da DARPA durante o desenvolvimento da Arpanet, Stephen J. Lukasik ( “Por que a Arpanet foi construída” ):

“O objetivo era explorar novas tecnologias de computação para atender às necessidades de comando e controle militar contra ameaças nucleares, conseguir o controle das forças nucleares dos EUA e melhorar a tomada de decisões táticas e de gestão militar.”

Nota: O principal ímpeto da Arpanet foi a conveniência e não a ameaça nuclear, e suas óbvias vantagens de defesa emergiram mais tarde. O ARPA estava usando três terminais de computador separados para se comunicar com três projetos separados de pesquisa de computadores. Bob Taylor queria que uma única rede de computadores conectasse cada projeto com os outros.

Um bom sistema MOP pode compartilhar a robustez da Internet usando componentes que são hot-swappable enquanto o aplicativo está sendo executado. Pode continuar a funcionar se o usuário estiver em um celular e ficar off-line porque entrou em um túnel. Ele pode continuar funcionando se um furacão derrubar a energia de um dos datacenters onde os servidores estão localizados.

Chegou a hora de o mundo do software abandonar o experimento de herança de classe e abraçar os princípios matemáticos e científicos que originalmente definiam o espírito da OOP.

É hora de começarmos a construir softwares mais flexíveis, mais resilientes e melhor compostos, com a MOP e a programação funcional trabalhando em harmonia.

Nota: O acrônimo MOP já é usado para descrever “programação orientada a monitoramento” e sua OOP improvável vai desaparecer silenciosamente.

Não fique chateado se o MOP não entender como linguagem de programação.
Faça MOP até seus OOPs.

Saiba mais em EricElliottJS.com

Aulas de vídeo sobre programação funcional estão disponíveis para os membros do EricElliottJS.com. Se você não é um membro, inscreva-se hoje .

Texto original em inglês.