Por que o “acoplamento entre funções e dados” é ruim?

38

Encontrei esta citação em " The Joy of Clojure "na pág. 32, mas alguém disse a mesma coisa para mim durante o jantar na semana passada e eu também ouvi outros lugares:

[A] downside to object-oriented programming is the tight coupling between function and data.

Eu entendo porque o acoplamento desnecessário é ruim em um aplicativo. Também me sinto confortável em dizer que o estado mutável e a herança devem ser evitados, mesmo na programação orientada a objetos. Mas não consigo ver por que adotar funções nas classes é inerentemente ruim.

Quero dizer, adicionar uma função a uma turma parece marcar um e-mail no Gmail ou colar um arquivo em uma pasta. É uma técnica organizacional que ajuda você a encontrá-lo novamente. Você escolhe alguns critérios e depois coloca as coisas juntos. Antes da POO, nossos programas eram muito grandes pacotes de métodos em arquivos. Quero dizer, você tem que colocar funções em algum lugar. Por que não organizá-los?

Se este é um ataque velado aos tipos, por que eles não dizem apenas que restringir o tipo de entrada e saída para uma função está errado? Eu não tenho certeza se eu poderia concordar com isso, mas pelo menos eu estou familiarizado com os argumentos pro e con tipo de segurança. Isso me parece uma preocupação principalmente separada.

Claro, às vezes as pessoas entendem errado e colocam a funcionalidade na classe errada. Mas comparado a outros erros, isso parece ser um pequeno inconveniente.

Portanto, o Clojure tem namespaces. Como é difícil colocar uma função em uma classe na OOP diferente de colocar uma função em um namespace no Clojure e por que ela é tão ruim? Lembre-se, funções em uma classe não necessariamente operam apenas em membros dessa classe. Olhe para java.lang.StringBuilder - ele opera em qualquer tipo de referência, ou através de auto-boxing, de qualquer tipo.

P.S. Esta citação faz referência a um livro que eu não li: Programação Multiparadigmática em Leda: Timothy Budd, 1995 .

    
por GlenPeterson 25.09.2013 / 14:52
fonte

6 respostas

33

Em teoria, o acoplamento de dados de função frouxa facilita a adição de mais funções para trabalhar nos mesmos dados. A desvantagem é que torna mais difícil alterar a própria estrutura de dados, e é por isso que, na prática, o código funcional bem projetado e o código OOP bem projetado têm níveis muito semelhantes de acoplamento.

Tome um gráfico acíclico direcionado (DAG) como uma estrutura de dados de exemplo. Na programação funcional, você ainda precisa de alguma abstração para evitar se repetir, então você vai fazer um módulo com funções para adicionar e excluir nós e bordas, localizar nós alcançáveis a partir de um determinado nó, criar uma classificação topológica, etc. são efetivamente acoplados aos dados, mesmo que o compilador não os imponha. Você pode adicionar um nó da maneira mais difícil, mas por que você quer? A coesão dentro de um módulo evita o acoplamento apertado em todo o sistema.

Por outro lado, no lado da OOP, todas as outras funções, além das operações básicas do DAG, serão executadas em classes "view" separadas, com o objeto DAG passado como um parâmetro. É tão fácil adicionar quantas visualizações você quiser que operam nos dados do DAG, criando o mesmo nível de desacoplamento de dados de função que você encontraria no programa funcional. O compilador não vai impedi-lo de colocar tudo em uma classe, mas seus colegas o farão.

Alterar os paradigmas de programação não altera as melhores práticas de abstração, coesão e acoplamento, apenas altera as práticas que o compilador ajuda a aplicar. Na programação funcional, quando você deseja que o acoplamento de dados de função seja aplicado por acordo de cavalheiros e não pelo compilador. Em OOP, a separação da vista de modelo é imposta pelo acordo de cavalheiros, e não pelo compilador.

    
por 25.09.2013 / 16:49
fonte
13

Caso você ainda não saiba, leve em consideração: os conceitos de orientados a objetos e encerramentos são dois lados da mesma moeda. Dito isso, o que é um fechamento? Ele pega variável (s) ou dados do escopo circundante e se liga a ele dentro da função, ou de uma perspectiva OO você efetivamente faz a mesma coisa quando você, por exemplo, passa algo para um construtor para que mais tarde você possa usar isso peça de dados em uma função de membro dessa instância. Mas tirar as coisas do escopo ao redor não é uma coisa boa de se fazer - quanto maior o escopo ao redor, mais mal é fazer isso (embora, pragmaticamente, algum mal seja frequentemente necessário para que o trabalho seja feito). O uso de variáveis globais está levando isso ao extremo, onde as funções em um programa estão usando variáveis no escopo do programa - realmente muito mal. Há boas descrições em outros lugares sobre por que as variáveis globais são más.

Se você seguir as técnicas OO, você basicamente já aceita que todos os módulos do seu programa terão um nível mínimo de maldade. Se você adotar uma abordagem funcional para programação, estará mirando em um ideal onde nenhum módulo em seu programa conterá mal de encerramento, embora você ainda possa ter algum, mas será muito menor que OO.

Essa é a desvantagem da OO - ela encoraja esse tipo de mal, o acoplamento de dados para funcionar através do fechamento padrão (uma espécie de teoria da janela quebrada da programação).

O único lado positivo é que, se você soubesse que iria usar muitos fechamentos para começar, o OO pelo menos forneceria uma estrutura idealógica para ajudar a organizar essa abordagem de modo que o programador médio pudesse entendê-la. Em particular, as variáveis que estão sendo fechadas são explícitas no construtor, em vez de apenas tomadas implicitamente em um encerramento de função. Programas funcionais que usam muitos fechamentos são frequentemente mais enigmáticos do que o programa OO equivalente, embora não necessariamente menos elegantes:)

    
por 25.09.2013 / 16:56
fonte
7

É sobre o tipo acoplamento:

Uma função incorporada em um objeto para trabalhar naquele objeto não pode ser usada em outros tipos de objetos.

No Haskell, você escreve funções para trabalhar contra classes do tipo - assim, há muitos tipos diferentes de objetos em que uma dada função pode trabalhar, desde que seja um tipo da classe essa função funciona.

As funções independentes permitem esse desacoplamento, que você não consegue quando se concentra em escrever suas funções para trabalhar dentro do tipo A, porque não pode usá-las se não tiver uma instância do tipo A, embora a função poderia ser genérica o suficiente para ser usada em uma instância do tipo B ou em uma instância do tipo C.

    
por 25.09.2013 / 17:48
fonte
4

Em Java e encarnações semelhantes de OOP, os métodos de instância (ao contrário de funções livres ou métodos de extensão) não podem ser adicionados a partir de outros módulos.

Isso se torna mais uma restrição quando você considera interfaces que só podem ser implementadas pelos métodos da instância. Você não pode definir uma interface e uma classe em módulos diferentes e, em seguida, usar o código de um terceiro módulo para vinculá-los. Uma abordagem mais flexível, como as classes de tipos de Haskell, deve ser capaz de fazer isso.

    
por 25.09.2013 / 15:26
fonte
3

Orientação a Objetos é fundamentalmente sobre Abstração de Dados de Procedimentos (ou Abstração de Dados Funcionais se você eliminar efeitos colaterais que são um problema ortogonal). De certo modo, o Lambda Calculus é a mais antiga e mais pura Linguagem Orientada a Objetos, já que somente fornece Abstração de Dados Funcionais (porque não tem nenhuma construção além de funções).

Somente as operações de um objeto único podem inspecionar a representação de dados desse objeto. Nem mesmo outros objetos do mesmo tipo podem fazer isso. (Esta é a principal diferença entre Abstração de Dados Orientada a Objetos e Tipos de Dados Abstratos: com ADTs, objetos do mesmo tipo podem inspecionar a representação de dados uns dos outros, apenas a representação de objetos de outros tipos está oculta. )

O que isto significa é que vários objetos do mesmo tipo podem ter diferentes representações de dados. Mesmo o mesmo objeto pode ter representações de dados diferentes em momentos diferentes. (Por exemplo, no Scala, Map s e Set s alternam entre uma matriz e um hash trie dependendo do número de elementos, pois, para números muito pequenos, a pesquisa linear em uma matriz é mais rápida que a pesquisa logarítmica em uma árvore de pesquisa. dos fatores constantes muito pequenos.)

Do lado de fora de um objeto, você não deve, você não pode conhecer sua representação de dados. Esse é o oposto do acoplamento apertado.

    
por 25.09.2013 / 16:07
fonte
1

O acoplamento apertado entre dados e funções é ruim porque você deseja alterar cada um independentemente do outro, e o acoplamento rígido torna isso difícil porque você não pode alterar um sem ter conhecimento e possivelmente mudar para o outro.

Você deseja que dados diferentes apresentados à função não requeiram alterações na função e, da mesma forma, você deseje poder fazer alterações na função sem precisar de nenhuma alteração nos dados em que está operando para suportar essas mudanças de função.

    
por 25.09.2013 / 17:31
fonte