Eu realmente amo o tema, mas quanto mais eu leio, mais eu tenho a impressão que enquanto é útil dividir entre duas escolas principalmente para não ser pedante ao conversar sobre o assunto no dia-a-dia, a verdade é que só uma representa de fato um novo paradigma.
A escola de Simula (C++, Java, etc) não é exatamente um novo paradigma.
Eu vejo que a única escola com espinha dorsal mesmo para ser um paradigma seria a tradição do Smalltalk (Pharo, Ruby, Crystal, etc) mesmo, pois é a única baseada num novo modelo conceitual de verdade mesmo.
Veja se a gente pegar os textos que deram origem a Simula, ela era só uma linguagem procedural com uma feature nova que ajudava naquele domínio que ela atuava, era quase uma DSL em cima de uma linguagem procedural comum.
Herança mesmo vinha de um paper também sobre o paradigma da época que era a programação imperativa/procedural/estruturada. Tanto que o paper falava sobre estender structs (records).
Ela focava exclusivamente em reúso de código.
"Encapsulamento" também não era exatamente algo que nasceu no contexto orientado a objetos já que information hiding já era algo bem discutido muito antes disso e tem exemplos de linguagens procedurais adotando mecanismos similares ao que a gente tem de modificadores de visibilidade já naquela época.
Como o próprio Alan Kay reclama no famoso e-mail com a definição de OO, polimorfismo é uma propriedade de funções não de objetos. Na época que eu li, eu não tinha entendido isso, mas hoje em dia, eu percebo que realmente não faz sentido falar em polimorfismo quando se tem troca de mensagens visto que elas são um mecanismo totalmente diferente de expressar comportamento (embora eu não goste do nome que ele deu também).
Simula deu origem ao sistema de objetos que usamos hoje em dia, mas não a POO.
Por outro lado, quando a gente pega a pesquisa acadêmica no pós-Simula, principalmente os trabalhos da Liskov por exemplo, a gente nota que o pessoal tinha um entusiasmo no desenvolvimento de novas técnicas relacionadas a sistemas de tipos e não sobre um novo paradigma.
Todo o hype em torno de herança, na verdade, vinha na direção contrária do que foi o interessante que fez ela nascer.
Era um hype focado nas possibilidades de ADTs e nas propriedades de substitucionalidade descobertas por Liskov (o L do SOLID).
Isso que gerou a contradição que é a feature de herança na maioria das linguagens e depois foi resolvida separando ela nos dois componentes que ela representava (interfaces + traits ou mixins).
Como dá pra notar, isso é muito mais próximo de uma evolução do próprio paradigma procedural para enquadrar elementos de programação modular no paradigma do que a criação de um novo paradigma.
O core de como se pensa numa aplicação e estrutura a lógica não muda muito.
É semelhante a como sistemas baseados em efeitos estão surgindo na FP agora, mas a gente não chama isso de programação orientada a efeitos, a gente continua chamando de FP por continuar a tradição lógica do paradigma.
Mas por que a gente tem uma impressão de que a OO muda tudo? Que procedural não se parece nada com OO?
Depois de me questionar muito sobre esse tema, a minha conclusão é que isso acontece por uma mudança de paradigma não na programação, mas sim na engenharia de software.
É interessante pegar livros influentes da era de ouro da OO de autores consagrados pra ver o que eles entendiam como OO.
E quanto mais perto você chega de autores relacionados a UML ou do OOD, mais você nota que eles não estão discutindo um paradigma de programação ali, mas sim engenharia de software.
Não a toa, as coisas se misturaram, pois na época o que trouxe o hype mesmo da OO na indústria foi uma mudança de mentalidade quanto a como a gente faz engenharia de software não sobre como se programa em si.
A gente transitou de pensar nas coisas como algoritmos e visualizar sistemas como coleções de fluxogramas para as técnicas de OOD e mais tarde se consolidando na UML.
É daí que sai a noção de que OO é programar representando o mundo real pois a gente teve uma mudança de paradigma em como nos aproximamos do problema e modelamos sistemas.
Muito embora essa definição sirva pra qualquer paradigma, em procedural já era comum dizer que você conhece o sistema pelas suas estruturas de dados e em FP a gente tem type driven development pela proeminência de sistemas de tipos algébricos nessas linguagens, mas mesmo em linguagens dinâmicas se usam módulos pra criar coisas muito semelhantes a classes em linguagens como Elixir por exemplo.
Ótimo, mas mesmo se a gente aceita essa premissa, como a tradição do Smalltalk seria diferente?
Ela é diferente pois a lógica toda tem que ser pensada num meio totalmente diferente que é a troca de mensagens.
Quando se entende isso, a gente nota que a confusão começa por chamar tudo de objeto, mas a definição de Smalltalk é diferente da de Simula.
Um objeto em Smalltalk é simplesmente um receptor de mensagens, enquanto um objeto em Simula é um conjunto de dados e funções.
Isso tem uma implicação até na semântica do código, pois uma chamada de função pode ter o seu dado inexistente, já quando você envia uma mensagem, o objeto pode não existir (por default seria null, mas ele também é um objeto), o método pode não existir e o dado também não é necessário existir.
Isso porque objetos podem lidar com mensagens que eles não conhecem então não importa se o método existe ou não, a mensagem pode ser enviada ao objeto pois ele tem um mecanismo para lidar com mensagens desconhecidas.
Você não consegue fazer isso com funções sem recriar uma estrutura que tenta imitar a infraestrutura que faz os objetos funcionarem.
Troca de mensagens implica que você tem um receptor na equação, que é um elemento que simplesmente não existe quando a gente fala de funções.
Como você não tem garantia de nada, você programa só confiando em contratos no sentido de que X sabe como responder a determinada mensagem.
Que é semelhante ao que acontece em sistemas com arquiteturas baseadas em eventos atualmente.
Isso é interessante pois você pode escolher responder, ignorar, redirecionar (delegar), fazer broadcast, modificar e reenviar, etc. Com mensagens, e isso é algo que no mínimo é bem diferente quando a gente trata de funções mesmo considerando as de alta ordem. O valor aqui é menos na composição e mais em como você decide quem lida com aquilo.
Pode não parecer tão interessante assim, até você notar o poder de programar com proxies.
Teoricamente, esse modelo é null-safe por padrão, visto que você pode ter um objeto null que simplesmente implementa o method_missing e qualquer coisa que não existe redireciona para ele que pode decidir só ignorar qualquer imagem recebida, você pode implementar o protocolo null em todos os objetos e realmente ter cada operador de null safety como um método presente em todos os objetos (o que faz sentido já que todo objeto é potencialmente nulo).
Tem um artigo ótimo da comunidade Objective-C que foi o que abriu minha mente que troca de mensagens realmente era algo completamente diferente de funções que é o paper sobre HOMs (High Order Messages).
Lá o autor discute uma técnica sobre como você usa proxies dinamicos pra reificar mensagens e assim conseguir manipular elas.
O uso principal é lidar com coleções, implementar o famoso mao, filter e reduce usando só troca de mensagens.
Mas o artigo vai além mostrando coisas que seriam difíceis de implementar só com high order functions tipo broadcast cruzado de mensagens entre duas coleções.
O truque que eu achei mais interessante é como troca de mensagens te dá concorrência de graça.
Aqui encapsulamento ganhou um significado a mais para mim.
Encapsulamento é sobre ter posse do seu próprio estado, então não é tanto sobre as garantias ou proteger contra invariantes e etc. Isso são bônus legais que você ganha com encapsulamento.
O ponto mesmo é cada unidade ser dona do seu próprio estado mesmo que ele seja público.
Isso porque se o encapsulamento realmente for perfeito, você pode converter um objeto num processo ou numa thread sem problema nenhum.
Concorrência é simplesmente colocar um proxy na frente do objeto para isolar ele fisicamente ou virtualmente, e redirecionar todas as mensagens do processo ou thread pro objeto.
Comunicação entre processos também não é nada de outro mundo, visto que ela se resume a simplesmente troca de mensagens também.
Isso é possível pois você pode escrever uma HOM que enfilera todas as mensagens recebidas de forma assíncrona, e depois faz o replay delas pro objeto dentro do proxy quase que criando um ator de pobre.
Como pro objeto as mensagens vem em sequência, você pode ter estado mutável pois isso não é compartilhado entre threads ou processos ou atores, etc. Você não tem problemas com race conditions que levam a corrupções (ainda teria algum problema de um edge case com deadlocks, mas isso teria que ser resolvido com outro paradigma como o Actor Model que é desenhado para ser 100% compatível com concorrência).
Ainda nesse assunto, acho que o que mostra como objetos são diferentes de funções é que em FP você precisa de containers pra lidar com concorrência então você tem algo como monads ou as promises do JS.
Se você se baseia em troca de mensagens, você pode escrever código assíncrono de forma sincrona sem async/await pois você não tem o problema da função colorida.
Você pode usar HOMs pra implementar promises transparentes que é como alguns dialetos de Smalltalk implementam Futures.
Do lado de como a gente pensa no código também muda bastante pois você tem que pensar em termos de conversas entre objetos.
Isso pode parecer só um jargão, mas eu vejo isso como programar com Tell Don't Ask.
Normalmente essa é só uma nota de rodapé no estudo da OO até porque o type system de muitas das linguagens mainstream não serve pra implementar double dispatch (visitors por exemplo são muito verbosos e as pessoas odeiam, por isso que pattern matching + algebraic data types ganhou tanta força como um appeal das linguagens funcionais).
Mas eu gosto muito da explicação da Sandi Metz sobre como programar com patos em Ruby, o trabalho dela é sensacional em explicar os benefícios de abraçar o TDA e muitas vezes acabar representando a conversa entre objetos por meio de double dispatch.
Isso não funciona bem numa linguagem como Java pois seria extremamente verboso e ainda teria alguns problemas com acoplamento.
Mas ao abraçar o espírito da troca de mensagens, se vê que é uma forma bem interessante até de programar e que inclusive é o que faz muitas pessoas cairem nas graças de linguagens como Go ou Elixir hoje em dia (embora o mecanismo seja invertido então você não tem os benefícios da troca de mensagens nesses casos).
Código OO tende lembrar mais uma coreografia do que uma orquestração se a gente for colocar em termos de sistemas distribuídos.
Por esses e outros motivos que eu considero que as ideias de Kay realmente eram um novo paradigma enquanto Simula foi um novo paradigma só na engenharia de software, mas não na programação, como paradigma de programação, ela é só uma continuação de como procedural já estava avançando na época.