web-dev-qa-db-fra.com

Pourquoi le "couplage étroit entre les fonctions et les données" est-il mauvais?

J'ai trouvé cette citation dans " The Joy of Clojure " à la p. 32 ans, mais quelqu'un m'a dit la même chose au cours du dîner la semaine dernière et je l'ai également entendu ailleurs:

[Un] inconvénient de la programmation orientée objet est le couplage étroit entre la fonction et les données.

Je comprends pourquoi un couplage inutile est mauvais dans une application. Je suis également à l'aise de dire que l'état mutable et l'héritage doivent être évités, même dans la programmation orientée objet. Mais je ne vois pas pourquoi coller des fonctions sur des classes est intrinsèquement mauvais.

Je veux dire, ajouter une fonction à une classe ressemble à marquer un mail dans Gmail, ou coller un fichier dans un dossier. C'est une technique d'organisation qui vous aide à la retrouver. Vous choisissez des critères, puis vous assemblez des choses semblables. Avant la POO, nos programmes étaient à peu près de gros sacs de méthodes dans des fichiers. Je veux dire, vous devez mettre des fonctions quelque part. Pourquoi ne pas les organiser?

S'il s'agit d'une attaque voilée sur les types, pourquoi ne disent-ils pas simplement que restreindre le type d'entrée et de sortie à une fonction est mauvais? Je ne sais pas si je pourrais être d'accord avec cela, mais au moins je connais les arguments pour et contre la sécurité de type. Cela me semble être une préoccupation essentiellement distincte.

Bien sûr, parfois les gens se trompent et mettent des fonctionnalités sur la mauvaise classe. Mais par rapport à d'autres erreurs, cela semble être un inconvénient très mineur.

Donc, Clojure a des espaces de noms. En quoi le collage d'une fonction sur une classe dans OOP est-il différent du collage d'une fonction dans un espace de noms dans Clojure et pourquoi est-ce si mauvais? Rappelez-vous, les fonctions dans une classe ne fonctionnent pas nécessairement uniquement sur les membres Regardez Java.lang.StringBuilder - il fonctionne sur n'importe quel type de référence, ou via l'auto-boxing, sur n'importe quel type.

P.S. Cette citation fait référence à un livre que je n'ai pas lu: Multiparadigm Programming in Leda: Timothy Budd, 1995 .

38
GlenPeterson

En théorie, un couplage fonction-données lâche facilite l'ajout de fonctions pour travailler sur les mêmes données. L'inconvénient est qu'il rend plus difficile de changer la structure de données elle-même, c'est pourquoi dans la pratique, un code fonctionnel bien conçu et un code OOP bien conçu ont des niveaux de couplage très similaires.

Prenez un graphe acyclique dirigé (DAG) comme exemple de structure de données. Dans la programmation fonctionnelle, vous avez encore besoin d'abstraction pour éviter de vous répéter, vous allez donc créer un module avec des fonctions pour ajouter et supprimer des nœuds et des bords, trouver des nœuds accessibles à partir d'un nœud donné, créer un tri topologique, etc. Ces fonctions sont effectivement étroitement liés aux données, même si le compilateur ne les applique pas. Vous pouvez ajouter un nœud à la dure, mais pourquoi voudriez-vous? La cohésion au sein d'un module empêche un couplage serré dans tout le système.

Inversement du côté OOP, toutes les fonctions autres que les opérations DAG de base vont être effectuées dans des classes "view" distinctes, avec l'objet DAG passé en paramètre. Il est tout aussi facile de ajoutez autant de vues que vous le souhaitez qui opèrent sur les données DAG, créant le même niveau de découplage des données de fonction que vous le trouveriez dans le programme fonctionnel. Le compilateur ne vous empêchera pas de tout entasser dans une classe, mais vos collègues .

Changer les paradigmes de programmation ne change pas les meilleures pratiques d'abstraction, de cohésion et de couplage, cela change juste les pratiques que le compilateur vous aide à appliquer. Dans la programmation fonctionnelle, lorsque vous voulez un couplage fonction-données, il est appliqué par un gentleman's agreement plutôt que par le compilateur. Dans la POO, la séparation entre vues de modèle est appliquée par un gentleman's agreement plutôt que par le compilateur.

34
Karl Bielefeldt

Au cas où vous ne le saviez pas, prenez déjà cette idée: les concepts de orienté objet et fermetures sont les deux faces d'une même médaille. Cela dit, qu'est-ce qu'une fermeture? Il prend des variables ou des données de la portée environnante et les lie à l'intérieur de la fonction, ou d'un point de vue OO, vous faites effectivement la même chose lorsque, par exemple, vous passez quelque chose à un constructeur afin que plus tard vous puissiez l'utiliser morceau de données dans une fonction membre de cette instance. Mais prendre des choses de la portée environnante n'est pas une bonne chose à faire - plus la portée environnante est grande, plus il est mal de faire cela (bien que pragmatiquement, un peu de mal soit souvent nécessaire pour faire le travail). L'utilisation de variables globales pousse cela à l'extrême, où les fonctions d'un programme utilisent des variables à la portée du programme - vraiment vraiment mauvais. Il y a bonnes descriptions ailleurs sur pourquoi les variables globales sont mauvaises.

Si vous suivez les techniques OO vous acceptez essentiellement que chaque module de votre programme aura un certain niveau minimum de mal. Si vous adoptez une approche fonctionnelle de la programmation, vous visez un idéal où aucun Le module de votre programme contiendra le mal de fermeture, même si vous en avez peut-être encore, mais ce sera beaucoup moins que OO.

C'est l'inconvénient de OO - il encourage ce genre de mal, le couplage des données pour fonctionner en rendant les fermetures standard (une sorte de théorie des fenêtres brisées de programmation).

Le seul avantage est que, si vous saviez que vous alliez utiliser beaucoup de fermetures pour commencer, OO vous fournit au moins un cadre généalogique pour aider à organiser cette approche afin que le programmeur moyen peut le comprendre. En particulier, les variables fermées sont explicites dans le constructeur plutôt que simplement prises implicitement dans une fermeture de fonction. Les programmes fonctionnels qui utilisent beaucoup de fermetures sont souvent plus cryptiques que l'équivalent OO programme, mais pas nécessairement moins élégant :)

13
Benedict

Il s'agit du couplage de type :

ne fonction intégrée à un objet pour travailler sur cet objet ne peut pas être utilisée sur d'autres types d'objets.

Dans Haskell, vous écrivez des fonctions pour travailler contre les classes de type - il existe donc de nombreux types d'objets différents contre lesquels une fonction donnée peut fonctionner, tant qu'il s'agit d'un type de la classe donnée sur laquelle la fonction fonctionne.

Les fonctions autonomes permettent un tel découplage que vous n'obtenez pas lorsque vous vous concentrez sur l'écriture de vos fonctions pour qu'elles fonctionnent à l'intérieur du type A, car vous ne pouvez pas les utiliser si vous n'avez pas d'instance de type A, même si la fonction peut sinon, être assez général pour être utilisé sur une instance de type B ou de type C.

7
Jimmy Hoffa

Dans Java et incarnations similaires de POO, les méthodes d'instance (contrairement aux fonctions libres ou aux méthodes d'extension) ne peuvent pas être ajoutées à partir d'autres modules.

Cela devient davantage une restriction lorsque vous considérez des interfaces qui ne peuvent être implémentées que par les méthodes d'instance. Vous ne pouvez pas définir une interface et une classe dans différents modules, puis utiliser le code d'un troisième module pour les lier ensemble. Une approche plus flexible, comme les classes de types de Haskell, devrait pouvoir le faire.

4
CodesInChaos

L'orientation des objets concerne fondamentalement l'abstraction des données procédurales (ou l'abstraction des données fonctionnelles si vous éliminez les effets secondaires qui sont un problème orthogonal). Dans un sens, Lambda Calculus est le langage orienté objet le plus ancien et le plus pur, car il niquement fournit l'abstraction des données fonctionnelles (car il n'a pas de constructions en plus des fonctions).

Seules les opérations d'un objet nique peuvent inspecter la représentation des données de cet objet. Même les autres objets du même type ne peuvent pas faire cela. (C'est la principale différence entre l'abstraction de données orientée objet et les types de données abstraits: avec les ADT, les objets du même type peuvent inspecter la représentation des données les uns des autres, seule la représentation des objets de types autre est masquée. )

Cela signifie que plusieurs objets du même type peuvent avoir des représentations de données différentes. Même le même objet peut avoir différentes représentations de données à différents moments. (Par exemple, dans Scala, Maps et Sets basculent entre un tableau et un tri de hachage en fonction du nombre d'éléments car pour les très petits nombres, la recherche linéaire dans un tableau est plus rapide que la recherche logarithmique dans un arbre de recherche en raison des très petits facteurs constants.)

De l'extérieur d'un objet, vous ne devriez pas, vous ne pouvez pas connaître sa représentation des données. C'est le ci-contre de couplage serré.

3
Jörg W Mittag

Un couplage serré entre les données et les fonctions est mauvais parce que vous voulez pouvoir changer chacun indépendamment de l'autre et le couplage serré rend cela difficile parce que vous ne pouvez pas changer l'un sans connaître et éventuellement changer l'autre.

Vous souhaitez que différentes données présentées à la fonction ne nécessitent aucune modification de la fonction et, de la même manière, vous souhaitez pouvoir apporter des modifications à la fonction sans avoir besoin de modifier les données sur lesquelles elle opère pour prendre en charge ces changements de fonction.

2
Michael Durrant