Olá Habr!
Hoje, uma publicação traduzida espera por você, refletindo, em certa medida, nossas pesquisas relacionadas a novos livros sobre OOP e FI. Por favor participe da votação.

O paradigma OOP está morto? Podemos dizer que o futuro está na programação funcional? Parece que muitos artigos escrevem sobre isso. Estou inclinado a discordar deste ponto de vista. Vamos conversar!
A cada poucos meses, encontro um post em algum blog em que a autora faz alegações aparentemente bem fundamentadas da programação orientada a objetos, após o que declara OOP uma relíquia do passado, e todos temos que mudar para a programação funcional.
Escrevi anteriormente que OOP e FI não se contradizem. Além disso, consegui combiná-los com muito sucesso.
Por que os autores desses artigos têm tantos problemas com a POO, e por que a FA lhes parece uma alternativa tão óbvia?
Como ensinar OOP
Quando nos ensinam OOP, eles geralmente enfatizam que é baseado em quatro princípios:
encapsulamento ,
herança ,
abstração ,
polimorfismo . São esses quatro princípios que geralmente são criticados em artigos em que os autores argumentam sobre o declínio da OLP.
No entanto, OOP, como FI, é uma ferramenta. Para resolver problemas. Pode ser consumido, também pode ser abusado. Por exemplo, ao criar a abstração incorreta, você abuse do OOP.
Portanto, a classe
Square
nunca deve herdar a classe
Rectangle
. Em um sentido matemático, eles estão, é claro, conectados. No entanto, do ponto de vista da programação, eles não estão em um relacionamento de herança. O fato é que os requisitos para um quadrado são mais rígidos do que para um retângulo. Enquanto em um retângulo existem dois pares de lados iguais, um quadrado deve ter todos os lados iguais.
Herança
Vamos discutir a herança em mais detalhes. Você provavelmente se lembra de exemplos de livros didáticos com belas hierarquias de classes herdadas, e todas essas estruturas trabalham para resolver o problema. No entanto, na prática, a herança não é usada tão frequentemente quanto a composição.
Considere um exemplo. Digamos que temos uma classe muito simples, um controlador em um aplicativo da web. A maioria das estruturas modernas pressupõe que você trabalhará com ela assim:
class BlogController extends FrameworkAbstractController { }
Supõe-se que, dessa maneira, será mais fácil fazer chamadas como
this.renderTemplate(...)
, pois esses métodos são herdados da classe
FrameworkAbstractController
.
Conforme indicado em muitos artigos sobre esse assunto, vários problemas tangíveis surgem aqui. Qualquer função interna na classe base realmente se transforma em uma API. Ela não pode mais mudar. Qualquer variável protegida do controlador base agora se relaciona mais ou menos à API.
Não há nada para se confundir. E se escolhêssemos a abordagem com composição e injeção de dependência, teria sido assim:
class BlogController { public BlogController ( TemplateRenderer templateRenderer ) { } }
Veja bem, você não depende mais de um
FrameworkAbstractController
nebuloso, mas depende de uma coisa muito bem definida e estreita,
TemplateRenderer
. De fato, o
BlogController
não herda de nenhum outro controlador, pois não herda nenhum comportamento.
Encapsulamento
A segunda característica frequentemente criticada do OOP é o encapsulamento. Na linguagem literária, o significado de encapsulamento é formulado da seguinte forma: dados e funcionalidade são entregues juntos, e o estado interno da classe é oculto do mundo externo.
Esta oportunidade, novamente, permite o uso e abuso. O principal exemplo de abuso nesse caso é um estado de vazamento.
Em termos relativos, suponha que a classe
List<>
contenha uma lista de elementos e essa lista possa ser alterada. Vamos criar uma classe para processar uma cesta de pedidos da seguinte maneira:
class ShoppingCart { private List<ShoppingCartItem> items; public List<ShoppingCartItem> getItems() { return this.items; } }
Aqui, na maioria dos idiomas orientados a OOP, acontecerá o seguinte: a variável de itens será retornada por referência. Portanto, ainda podemos fazer isso:
shoppingCart.getItems().clear();
Portanto, limparemos a lista de itens da cesta e o ShoppingCart nem saberá sobre isso. No entanto, se você examinar atentamente este exemplo, fica claro que o problema não está no princípio do encapsulamento. Este princípio é apenas violado aqui, porque o estado interno vaza da classe
ShoppingCart
.
Neste exemplo em particular, o autor da classe
ShoppingCart
pode usar a
imutabilidade para contornar o problema e garantir que o princípio do encapsulamento não seja violado.
Programadores inexperientes freqüentemente violam o princípio do encapsulamento de outra maneira: eles introduzem um estado em que não é necessário. Esses programadores inexperientes geralmente usam variáveis de classe privada para transferir dados de uma função para outra dentro da mesma classe, enquanto seria mais apropriado usar objetos de transferência de dados para transferir uma estrutura complexa para outra função. Como resultado de tais erros, o código é desnecessariamente complicado, o que pode levar a erros.
Em geral, seria bom dispensar completamente o estado - armazenar dados mutáveis em classes sempre que possível. Ao fazer isso, você precisa
garantir um encapsulamento confiável e garantir que não haja vazamentos em nenhum lugar.
Abstração
Abstração, novamente, é entendida de várias maneiras incorretamente. Em nenhum caso você deve preencher o código com classes abstratas e criar hierarquias profundas.
Se você fizer isso sem uma boa razão, estará apenas procurando problemas por conta própria. Não importa como a abstração é feita - como uma classe abstrata ou como uma interface; em qualquer caso, complexidade extra aparecerá no código. Essa complexidade deve ser justificada.
Simplificando, uma interface só pode ser criada se você estiver disposto a gastar tempo e documentar o comportamento esperado da classe que o implementa. Sim, você me leu certo. Não basta apenas fazer uma lista de funções que você precisa implementar - também descrever como (idealmente) elas devem funcionar.
Polimorfismo
Finalmente, vamos falar sobre polimorfismo. Ele sugere que uma classe pode implementar muitos comportamentos. Um exemplo ruim de livro é escrever que o
Square
com polimorfismo pode ser um
Rectangle
ou um
Parallelogram
. Como já mencionei acima, isso no POO é decididamente impossível, uma vez que os comportamentos dessas entidades são diferentes.
Falando em polimorfismo, deve-se ter em mente
comportamentos , não
código . Um bom exemplo é a classe
Soldier
em um jogo de computador. Ele pode implementar comportamento
Movable
(situação: pode se mover) e comportamento
Enemy
(situação: atira em você). Por outro lado, a classe
GunEmplacement
pode implementar apenas o comportamento do
Enemy
.
Portanto, se você escrever
Square implements Rectangle, Parallelogram
, essa afirmação não se torna verdadeira. Suas abstrações devem funcionar de acordo com a lógica de negócios. Você deve pensar mais sobre comportamento do que sobre código.
Por que FP não é uma bala de prata
Então, quando repetimos os quatro princípios básicos do OOP, vamos pensar sobre qual é o recurso da programação funcional e por que não usá-lo para resolver todos os problemas do seu código?
Do ponto de vista de muitos adeptos do FP, as classes são
sacrilégio , e o código deve ser apresentado na forma de
funções . Dependendo do idioma, os dados podem ser transferidos de função para função usando tipos primitivos ou na forma de um ou outro conjunto de dados estruturados (matrizes, dicionários etc.).
Além disso, a maioria das funções não deve ter efeitos colaterais. Em outras palavras, eles não devem alterar dados em nenhum lugar inesperado em segundo plano, mas apenas trabalhar com parâmetros de entrada e produzir saída.
Essa abordagem separa os
dados do
funcional - à primeira vista, esse FP difere radicalmente do OOP. O FP enfatiza que dessa maneira o código permanece simples. Você quer fazer alguma coisa, escrever uma função para esse fim - isso é tudo.
Os problemas começam quando algumas funções precisam depender de outras. Quando a função A chama a função B, e a função B chama outras cinco a seis funções, e no final
é encontrada uma
função de preenchimento zero que pode quebrar - é aqui que você não será invejado.
Muitos programadores que se consideram defensores do FP adoram o FP por sua simplicidade e não consideram esses problemas sérios. Isso é bastante honesto se sua tarefa é simplesmente passar o código e nunca mais pensar nele novamente. Se você deseja criar uma base de código que seja conveniente no suporte, é melhor aderir aos princípios do
código puro , em particular, aplicar a
inversão de dependência , na qual o FI na prática também se torna muito mais complicado.
OOP ou FP?
OOP e FI são
ferramentas . Por fim, não importa qual paradigma de programação você usa. Os problemas descritos na maioria dos artigos neste tópico estão relacionados à organização do código.
Na minha opinião, a macroestrutura do aplicativo é muito mais importante. Quais são os módulos nele? Como eles trocam informações entre si? Quais estruturas de dados são mais comuns com você? Como eles estão documentados? Quais objetos são mais importantes em termos de lógica de negócios?
Todas essas questões não estão de forma alguma ligadas ao paradigma de programação usado; no nível de um paradigma nem sequer podem ser resolvidas. Um bom programador estuda o paradigma para dominar as ferramentas que oferece e, em seguida, escolhe quais são as mais adequadas para resolver a tarefa.