Eu consigo entender a intenção do Post, o que ele critica, e nesse sentido até poderia concordar com mais pontos, mas ou o título é muito click bait, ou você está batendo num espantalho o artigo todo.

Você falou muito que OOP é lixo, mas não apresentou muitos argumentos sobre os motivos que levam a isso, cada parágrafo apresenta um efeito, sem realmente mostrar que a OOP é a causa dele, outros você simplesmente joga dois pedaços de código, e não faz nenhum juízo de valor (exceto dizer que a versão procedural é mais simples sem elaborar muito também), ou referencia vários links extras sem fazer nenhum comentário por cima, o que não ajuda muito que está lendo o seu artigo a entender o seu ponto, não é nem por uma questão de má vontade, é só que não ajuda muito a criar uma linha de raciocínio se eu tenho que parar para ver uma talk a cada parágrafo do texto para conseguir seguir a sua linha de raciocínio.

Agora pegando ponto-a-ponto do seu texto.

Sobre os motivos para se ignorar a POO, a performance realmente é algo importante, mas assim, claramente não se utiliza OO por conta da performance, se performance fosse o argumento mais importante de todos, a gente não utilizaria outro paradigma que não fosse procedural, ou quem sabe abandonar tudo isso e voltar pro assembly não-estruturado.

Quanto a ter vários livros que tentam corrigir a OO, se algo for tão explorado na academia, e no mercado ao ponto de ser usado em todo lugar religiosamente, até quando não for o ideal, certamente acredito que teríamos livros semelhantes dessa coisa também.

Daquela lista, eu conheço o Elegant Objects, e apesar de eu gostar bastante das opiniões dele, e algumas coisas realmente estarem mais alinhadas com os princípios, claramente o que ele quer é uma forma diferente de FP, e não de OOP, é só acompanhar o próprio blog do autor para entender isso. Alias, esse é um caminho bem parecido com o que o Alan Kay tomou para criar o Smalltalk (já que ele via o Lisp como algo falho), embora claramente as conclusões são diferentes devido aos objetivos serem diferentes.

Livros sobre como lidar com procedural legado realmente existem, um dos melhores livros de PHP que eu já li fala exatamente sobre esse tema, inclusive a solução que ele propõe é a aplicação de um tom mais OO com design patterns (https://www.amazon.com/Modernizing-Legacy-Applications-Paul-Jones/dp/1787124703).

Não se discute muito mais sobre problemas de procedural, porque os problemas já são bem conhecidos, falta de encapsulamento, mecanismos de abstração tendem a ser mais pobres em linguagens que só suportam esse paradigma, estado global, estado mutável sem controle, spagetti code, e etc. Óbvio que com disciplina, e "boas práticas" é possível ser bem sucedido em qualquer paradigma, seja ele a OO, ou o procedural, um dos melhores exemplos disso é o Linux, que é uma comunidade altamente disciplinada.

Sobre a questão da evolução das linguagens, eu não diria que Erlang é uma linguagem lá muito moderna, ela é bem antiga até para falar a verdade, moderno nesse contexto provavelmente seria o Elixir.

Apesar disso, eu tenho uma boa explicação para isso, a FP vem se popularizando muito nos últimos tempos, então o natural é que as linguagens evoluam na direção de ir adotando mais recursos desse paradigma, foi exatamente o que aconteceu com a OO. O motivo é simplesmente que elas evoluem na direção das ferramentas que podem adicionar algo a mais para elas, Java já implementa muitas coisas de OO, não tem muito mais de OO que implementar ali, logo o caminho natural seria ver como ele poderia se beneficiar de outros paradigmas.

Até porque isso reflete até a maneira que nós programamos no dia-a-dia, é muito difícil classificar algo como puramente de um paradigma, você pode ter um código super imperativo, causando efeitos colaterais, dependendo de estado global, e receber uma callback como parâmetro por exemplo. Não é por conta dessa callback que o código virou FP da noite pro dia, ela continua sendo uma solução muito procedural, só que ela incorporou algumas coisas de FP.

Sobre os usos de OO, as consequências estão corretas (existem outras formas de resolver os mesmos problemas), mas isso não ajuda em nada seu argumento, você não disse que o ponto dos defensores era que OO é a única solução para esses problemas, o ponto é que esses seriam campos onde ela seria uma solução efetiva, não é sobre ser melhor ou pior que outras soluções.

Aqui a gente chega no seu primeiro exemplo de código, nesse ponto eu achei o artigo meio desonesto, se você olhar no repositório essa é a versão 1 do código, sendo que elas vão até a versão 11.

O código da versão 11 ainda poderia ser bem mais OO, mas claramente é bem mais OO que o código da versão 1 que você referencia, este por sua vez é bem procedural, não é porque você está usando classes e objetos que você não pode escrever código procedural, inclusive o erro mais comum de quem vai aprender um novo paradigma é forçar o paradigma que domina nesse outro paradigma.

A sua refatoração melhora aquele código justamente porque ele já era muito procedural para começo de conversa, então implementar ele usando técnicas procedurais naturalmente resultaria numa versão melhor daquele código.

Você não pode dizer o mesmo da versão 11. Claro que ainda tem mais arquivos e LoC (Linhas de Código), mas essa não é a única métrica que importa, se não, não existiriam recursos de abstração na maioria das linguagens, e se o único benefício disso (abstração) fosse reúso de código, então a OOP seria o melhor paradigma de todos, já que nada vence herança múltipla + polimorfismo em termos de reúso de código, o problema são as outras coisas que vem disso.

Não vou falar muito sobre legibilidade pois em muitos casos isso tende a ser meio subjetivo, eu por exemplo acho muito mais simples ler um reduce do que um for hoje em dia para qualquer operação em arrays.

Pelo seu código a gente consegue ver uma área onde a OOP realmente se destaca que é o polimorfismo, o seu código teria problemas para evoluir caso novos tipos de código fossem adicionados, e aquele switch só cresceria (e ficaria mais complexo) ao longo do tempo, com OO, isso seria uma aplicação simples do OCP do SOLID.

Sobre padrões de projeto, isso existe em qualquer lugar, são simplesmente as soluções mais comuns que as pessoas tem para qualquer coisa, coleções de padrões existem até para áreas fora da programação como o design.

Claro que padrões de OO não seriam necessários em programação funcional, é outro paradigma completamente diferente. Você poderia dizer o contrário também, você não precisaria de uma State Monad caso tivesse objetos com encapsulamento simples sendo aplicado, você não precisaria de Optional Monad, se soubesse como usar herança simples para implementar um boolean ou uma variação disso para valores nulos, o Alan Kay mesmo já disse que uma das inspirações que ele teve para OO era a capacidade de compor algebras da matemática, que é um conceito muito bem implementado nessas estruturas algebricas.

Mesmo os padrões de OOP, não precisariam de padrões para serem usados com OOP, boa parte deles é simplesmente polimorfismo básico, o template method é só uma forma inteligente de usar herança, o Visitor é só uma aplicação de double dispatch, e assim por diante. Os padrões são úteis pois assim podemos dar nomes para cada tipo de utilização, o que melhora a comunicação.

Ai o último ponto que eu gostaria de comentar seria a sua segunda refatoração.

Para começar que o código original não é a coisa mais OOP do mundo, "a classe Game" aqui teria controle do estado de toda a aplicação, mesmo tendo pelo menos 3 ou 4 objetos que ela dependeria, e que poderiam sim lidar com o seu próprio estado. O padrão Observer não era necessário em Game, pois ele não usou isso em lugar nenhum depois.

Só que o seu código não simplificou em nada o código original, inclusive depender uma função sendUpdate, meio que tem quase o mesmo efeito de implementar um padrão observer é só que é uma solução mais prática e menos robusta.

O seu código piorou o original, ele faz literalmente a mesma coisa que o original, só que ao invés de separar cada ação que vai alterar o estado da aplicação numa função (o que seria um bom senso até em programação procedural), você colapsou todas as funções dentro de um switch gigante, o que só aumenta a complexidade do código ao invés de diminuir.

Juntar tudo em uma única função tem os seus méritos, mas nesse caso nem ajuda muito, pois agora quem for ler o código onde ela foi usada, vai ter que procurar em qual ponto de uma função enorme, que nem é pura, que aquele comando está sendo tratado, ao invés de simplesmente procurar no módulo que lida com ele.

Mesmo que você possa argumentar que existe uma certa equivalencia num switch com actions (similar a um reducer) e um objeto com métodos, os métodos de um objeto são melhores visto que estão menos propensos a erros de digitação.

A OO é boa para várias coisas sim, o encapsulamento permite que você tenha os mesmos benefícios de uma closure em FP, só que numa situação onde existe mais de uma operação que depende dos mesmos dados.

O encapsulamento provido pelo uso de objetos é uma das melhores formas de lidar com dados mutáveis, pois você consegue restringir as formas com que ele pode ser modificado, o que é algo muito útil para softwares empresariais que precisam de políticas fortes que assegurem as regras de negócio da aplicação.

Claro que todo paradigma permite que você reforce regras de negócio, tal como um if e status code no procedural, ou uma result monad em FP, mas só a OO consegue fazer esse tipo de validação a nível de dado para todas as operações que envolvem ele.

A abordagem da FP para esse caso é garantir que os dados são válidos nas bordas, e assumir que eles são válidos no Core, o que é uma ótima ideia também, mas eu não acho que isso diminui os méritos de garantir essa consistência no próprio dado em si.

O polimorfismo é algo que todos podemos concordar que é uma funcionalidade essencial para desenvolvimento de qq software ser saudável. A capacidade de deixar suas opções abertas para o futuro é algo realmente valioso.

Todo paradigma tem alguma forma de suporte a esse conceito, no procedural, apesar de ser o mais limitado nesse tipo de coisa, você pode ter certo grau de polimorfismo usando polimorfismo paramétrico, e ad-hoc, porém isso se limita só aos dados.

Na FP, apesar dos tipos serem os mesmos do procedural, você tem uma vantagem imensa aqui que é a existencia de type classes, e funções como cidadãs de primeira-classe, assim você consegue quase uma equivalencia com o sistema adotado pela OOP.

Na OO, apesar de poder ser argumentado que ela suporte quase todos os tipos, eu gostaria de focar só no princípal que realmente seria algo singular dela, o de subtype.

Pois, por mais que FP seja incrível, o tipo de polimorfismo que ela oferece só resolve metade do Expression problem (https://en.wikipedia.org/wiki/Expression_problem), o modelo de subtyping da OO resolve a outra metade, e esse é um problema justamente pois não tem como resolver os dois lados ao mesmo tempo.

O modelo da FP permite que você adicione mais tipos de dados facilmente a uma função, enquanto a vantagem da OO é o contrário, facilitar a adição de mais funções a um determinado tipo de dado.

Por isso que problemas onde a OO é melhor, geralmente são problemas onde você consegue abstrair uma estrutura hierarquica entre os objetos.

O que nos leva a herança, que apesar de realmente não ser a melhor coisa do mundo, todos conhecermos a famosa ideia de que composição > herança, tem sim uma função vital no dia-a-dia da maioria dos programadores.

Herança, graças as classes abstratas, é uma ótima forma de pegar boas abstrações que emergirem de uma codebase, e centralizar elas num lugar só, de modo a tornar o desenvolvimento mais incremental.

O padrão template method é uma forma legal de ver como isso poderia ser usado no código do dia-a-dia, mas não é nem sobre ele que eu estava falando. Os frameworks que usamos no dia-a-dia são o maior exemplo dos benefícios que essa funcionalidade pode trazer, apesar de realmente não ser uma boa solução em userland, ter ferramentas que se utilizam disso, é um aspecto positivo sobre esse princípio.

E para fechar, eu gostaria de lembrar que OO é sobre as mensagens trocadas pelos objetos, até porque objetos são receptores de mensagens, tipo como uma forma de reducer glorificado, o que fica evidente no sistema de processos baseado em actor model (que é tipo uma versão async da ideia de OO do Alan Kay) do Erlang/Elixir, por isso que o Alan Kay denomina o paradigma como orientação a objetos.

Então onde cada paradigma vai se destacar vai muito de o que você acha que é mais importante, visto que sistemas são sempre a junção de duas coisas: dados e ações.

Se você vê os dados como mais importantes, então a OO realmente não serve para você, um ótimo exemplo disso foi o que vc deu sobre DoD.

Se você vê os comportamentos como mais importantes, então OO vai acabar sendo melhor, um bom exemplo disso é o sistema de recuperação de falhas de sistemas Erlang de telefonia, que tem quase 0 de downtime desde a sua criação.