FP vs OOP

Há pouco tempo, várias postagens apareceram no hub, contrastando a abordagem funcional e de objeto, o que gerou nos comentários uma discussão acalorada sobre o que realmente é - programação orientada a objeto e como ela difere de funcional. Embora um pouco tarde, quero compartilhar com outras pessoas o que Robert Martin, também conhecido como tio Bob, pensa sobre isso.



Nos últimos anos, pude repetidamente programar em conjunto com pessoas que estudam Programação Funcional que eram tendenciosas em relação à OOP. Isso geralmente era expresso na forma de declarações como: "Bem, isso é muito parecido com algo objeto".


Eu acho que isso vem da crença de que FP e OOP são mutuamente exclusivos. Muitos parecem pensar que, se o programa é funcional, não é orientado a objetos. Eu acredito que a formação dessa opinião é uma consequência lógica do estudo de algo novo.


Quando adotamos uma nova técnica, geralmente começamos a evitar as técnicas antigas que usamos anteriormente. Isso é natural, porque acreditamos que a nova técnica é "melhor" e, portanto, a técnica antiga é provavelmente "pior".


Neste post, justifico-me que, embora OOP e FP sejam ortogonais, esses conceitos não são mutuamente exclusivos. Que um bom programa funcional pode (e deve) ser orientado a objetos. E que um bom programa orientado a objetos pode (e deve) ser funcional. Mas, para fazer isso, precisamos determinar os termos.


O que é OOP?


Abordarei a questão de uma perspectiva reducionista. Existem muitas definições corretas de POO que abrangem muitos conceitos, princípios, técnicas, padrões e filosofias. Pretendo ignorá-los e me concentrar no próprio sal. Aqui, o reducionismo é necessário porque toda essa riqueza de oportunidades em torno da OOP não é realmente algo específico para a OOP; é apenas parte da riqueza de oportunidades encontradas no desenvolvimento de software em geral. Aqui vou me concentrar na parte da OOP, que é definidora e irremovível.


Veja duas expressões:


1: f (o); 2: de ();


Qual a diferença?


Claramente, não há diferença semântica. Toda a diferença está inteiramente na sintaxe. Mas um parece processual e o outro é orientado a objetos. Isso ocorre porque estamos acostumados ao fato de que a expressão 2 implica implicitamente uma semântica de comportamento especial que a expressão 1. Não possui. Essa semântica de comportamento específica é o polimorfismo.


Quando vemos a expressão 1., vemos a função f , que é chamada para a qual o objeto o é transferido. Isso implica que existe apenas uma função chamada f, e não o fato de ser um membro da coorte padrão de funções em torno de o.


Por outro lado, quando vemos a expressão 2., vemos um objeto com o nome o para o qual uma mensagem com o nome f é enviada. Esperamos que haja outros tipos de objetos que recebam a mensagem f e, portanto, não sabemos qual comportamento específico esperar de f após a chamada. O comportamento depende do tipo o. isto é, f é polimórfico.


Esse fato que esperamos dos métodos de comportamento polimórfico é a essência da programação orientada a objetos. Esta é uma definição reducionista e essa propriedade não pode ser removida do OOP. POO sem polimorfismo não é POO. Todas as outras propriedades de POO, como encapsulamento de dados e métodos vinculados a esses dados e até herança, estão mais relacionadas à expressão 1. do que à expressão 2.


Programadores que usam C e Pascal (e até certo ponto até Fortran e Cobol) sempre criaram sistemas de funções e estruturas encapsuladas. Para criar essas estruturas, você nem precisa de uma linguagem de programação orientada a objetos. O encapsulamento e até a simples herança nessas línguas são óbvios e naturais. (Em C e Pascal mais naturalmente do que em outros)


Portanto, o que realmente distingue os programas OOP dos programas não OOP é o polimorfismo.


Você pode argumentar que o poliforismo pode ser feito simplesmente usando o interruptor f interno ou longas cadeias if / else. Isso é verdade, então eu preciso definir outra limitação para OOP.


O uso do polimorfismo não deve criar a dependência do chamador no chamado.


Para explicar isso, vejamos novamente as expressões. A expressão 1: f (o) parece depender da função f no nível do código fonte. Tiramos essa conclusão porque também assumimos que f é apenas um e que, portanto, o chamador deve saber sobre o chamado.


No entanto, quando olhamos para a expressão 2. de () assumimos outra coisa. Sabemos que pode haver muitas realizações de f e não sabemos qual dessas funções f será realmente chamada. Portanto, o código-fonte que contém a expressão 2 é independente da função que está sendo chamada no nível do código-fonte.


Mais especificamente, isso significa que módulos (arquivos com código fonte) que contêm chamadas de função polimórficas não devem se referir a módulos (arquivos com código fonte) que contêm a implementação dessas funções. Não pode haver inclusão, uso ou exigência ou qualquer outra palavra-chave que torne alguns arquivos de código-fonte dependentes de outros.


Portanto, nossa definição reducionista de POO é:


Uma técnica que usa o polimorfismo dinâmico para chamar funções e não cria dependências do chamador no chamado no nível do código-fonte.

O que é AF?


E, novamente, usarei a abordagem reducionista. O FP tem tradições e história ricas, cujas raízes são mais profundas do que a própria programação. Existem princípios, técnicas, teoremas, filosofias e conceitos que permeiam esse paradigma. Ignorarei tudo isso e irei direto à essência, à propriedade inerente que separa FP de outros estilos. Aqui está:


f (a) == f (b) se a == b.


Em um programa funcional, chamar uma função com o mesmo argumento fornece o mesmo resultado, não importa quanto tempo o programa esteja em execução. Isso às vezes é chamado de transparência referencial.


Segue-se do exposto acima que f não deve alterar as partes do estado global que afetam o comportamento de f. Além disso, se dissermos que f representa todas as funções do sistema - ou seja, todas as funções do sistema devem ser referencialmente transparentes -, nenhuma função do sistema pode alterar o estado global. Nenhuma função pode fazer algo que possa levar a outra função do sistema retornando um valor diferente com os mesmos argumentos.


Isso tem uma conseqüência mais profunda - nenhum valor nomeado pode ser alterado. Ou seja, não há operador de atribuição.


Se você considerar cuidadosamente essa afirmação, poderá concluir que um programa que consiste apenas em funções transparentemente transparentes não pode fazer nada - já que qualquer comportamento útil do sistema altera o estado de algo; mesmo que seja apenas o estado da impressora ou tela. No entanto, se excluirmos o ferro dos requisitos de transparência referencial e de todos os elementos do mundo ao nosso redor, acontece que podemos criar sistemas muito úteis.


O foco, é claro, está na recursão. Considere uma função que assume uma estrutura com estado como argumento. Este argumento consiste em todas as informações de estado que uma função precisa para funcionar. Quando o trabalho é concluído, a função cria uma nova estrutura com um estado cujo conteúdo é diferente do anterior. E com a última ação, a função se chama com uma nova estrutura como argumento.


Este é apenas um dos truques simples que um programa funcional pode usar para armazenar alterações de estado sem precisar mudar de estado [1].


Portanto, a definição reducionista de programação funcional:


Transparência referencial - Você não pode reatribuir valores.

FP vs OOP


Neste ponto, tanto os defensores da OOP quanto os da FI já estão me olhando através de miras ópticas. Reducionismo não é a melhor maneira de fazer amigos. Mas às vezes é útil. Nesse caso, acho que é útil lançar luz sobre o holivar anti-OOP que está desaparecendo.


É claro que as duas definições reducionistas que eu escolhi são completamente ortogonais. Polimorfismo e Transparência Referencial não têm nada a ver um com o outro. Eles não se cruzam de forma alguma.


Mas a ortogonalidade não implica exclusão mútua (pergunte a James Clerk Maxwell). É inteiramente possível criar um sistema que use polimorfismo dinâmico e transparência referencial. Não é apenas possível, é certo e bom!


Por que essa combinação é boa? Pelas mesmas razões que os dois componentes! Os sistemas construídos com polimorfismo dinâmico são bons porque têm baixa conectividade. Dependências podem ser invertidas e colocadas em lados diferentes dos limites da arquitetura. Esses sistemas podem ser testados usando Moki e Fake e outros tipos de dobros de teste. Os módulos podem ser modificados sem fazer alterações em outros módulos. Portanto, esses sistemas são mais fáceis de modificar e melhorar.


Os sistemas construídos com transparência referencial também são bons porque são previsíveis. A imutabilidade do estado torna esses sistemas mais fáceis de entender, mudar e melhorar. Isso reduz bastante a probabilidade de corridas e outros problemas de multithreading.


A principal idéia aqui é esta:


Não há holivar FP vs OOP

FP e OOP funcionam bem juntos. Ambos são bons e adequados para uso em sistemas modernos. O sistema, baseado em uma combinação dos princípios de OOP e FP, maximiza a flexibilidade, a manutenção, a testabilidade, a simplicidade e a resistência. Se você remover um para adicionar outro, isso só piorará a estrutura do sistema.


[1] Como usamos máquinas com arquitetura Von Neumann, assumimos que elas possuem células de memória cujo estado realmente muda. No mecanismo de recursão que descrevi, a otimização da recursão da cauda não permitirá a criação de novas molduras de vidro e a moldura de vidro original será usada. Mas essa violação da transparência referencial (geralmente) está oculta do programador e não afeta nada.

Source: https://habr.com/ru/post/pt474518/


All Articles