Pourquoi le «couplage étroit entre fonctions et données» est-il mauvais?

38

J'ai trouvé cette citation dans " La joie de Clojure " à la p. 32 ans, mais quelqu'un m'a dit la même chose au dîner la semaine dernière et je l'ai aussi entendu à d'autres endroits:

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

Je comprends pourquoi le couplage inutile est mauvais dans une application. De plus, je suis à l'aise pour 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.

Ajouter une fonction à une classe revient à marquer un courrier dans Gmail ou à coller un fichier dans un dossier. C'est une technique d'organisation qui vous aide à le retrouver. Vous choisissez des critères, puis mettez les choses comme ensemble. Avant la programmation orientée objet, nos programmes étaient plutôt volumineux en méthodes. 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 des types, pourquoi ne pas simplement dire que restreindre le type d'entrée et de sortie à une fonction est une erreur? Je ne suis pas sûr de pouvoir être d'accord avec cela, mais au moins, je connais les arguments pour ou contre la sécurité. Cela me semble être une préoccupation essentiellement séparée.

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

Clojure a donc des espaces de noms. En quoi coller une fonction sur une classe dans la POO est-il différent de coller une fonction dans un espace de noms dans Clojure et pourquoi est-ce si grave? N'oubliez pas que les fonctions d'une classe ne fonctionnent pas nécessairement sur les membres de cette classe. Regardez java.lang.StringBuilder - il fonctionne sur tout type de référence, ou via la boxe automatique, sur n'importe quel type.

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

GlenPeterson
la source
20
Je pense que l’écrivain n’a tout simplement pas bien compris la POO et qu’il lui fallait une raison de plus pour dire que Java est mauvais et que Clojure est bon. / Rant
Euphoric
6
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 plus 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. Une approche plus flexible, comme les classes de types de haskell, devrait pouvoir le faire.
CodesInChaos
4
@Euphoric Je crois que l'auteur a bien compris, mais la communauté Clojure semble aimer faire de la paille un homme de paille et la graver à l'effigie de tous les maux de la programmation avant d'avoir une bonne collecte des ordures, beaucoup de mémoire, des processeurs rapides, et beaucoup d'espace disque. Je souhaite qu'ils cessent de battre sur la POO et ciblent les vraies causes: l'architecture Von Neuman, par exemple.
GlenPeterson
4
Mon impression est que la plupart des critiques de la programmation orientée objet sont en réalité des critiques de la programmation orientée objet telle que mise en œuvre en Java. Non pas parce que c'est un homme de paille délibéré, mais parce que c'est ce qu'ils associent à la POO. Il y a des problèmes assez similaires avec les personnes qui se plaignent du typage statique. La plupart des problèmes ne sont pas inhérents au concept, mais seulement des failles dans une implémentation populaire de ce concept.
CodesInChaos
3
Votre titre ne correspond pas au corps de votre question. Il est facile d’expliquer pourquoi le couplage étroit de fonctions et de données est mauvais, mais votre texte pose les questions suivantes: "Est-ce que la POO fait cela?", "Si oui, pourquoi?" et "Est-ce une mauvaise chose?". Jusqu'ici, vous avez eu la chance de recevoir des réponses qui traitent d'une ou plusieurs de ces trois questions et aucune ne se base sur la question plus simple du titre.
Itsbruce

Réponses:

34

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

Prenons un graphe acyclique dirigé (DAG) comme exemple de structure de données. En programmation fonctionnelle, vous avez encore besoin d'une certaine abstraction pour éviter de vous répéter. Vous allez donc créer un module avec des fonctions permettant d'ajouter et de supprimer des nœuds et des arêtes, de rechercher des nœuds accessibles depuis un nœud donné, de créer un tri topologique, etc. sont efficacement couplé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 dans un module empêche un couplage étroit dans tout le système.

À l'inverse, du côté de la programmation orientée objet, toutes les fonctions autres que les opérations de base du DAG seront effectuées dans des classes "view" distinctes, l'objet DAG étant transmis en tant que paramètre. Il est tout aussi facile d'ajouter autant de vues que vous le souhaitez, qui fonctionnent sur les données du DAG, en créant le même niveau de découplage des données de fonction que vous retrouveriez dans le programme fonctionnel. Le compilateur ne vous empêchera pas de tout mettre dans une classe, mais vos collègues le feront.

Changer les paradigmes de programmation ne change pas les meilleures pratiques d'abstraction, de cohésion et de couplage, mais change simplement les pratiques que le compilateur vous aide à appliquer. En programmation fonctionnelle, lorsque vous souhaitez un couplage fonction-donnée, il est imposé par un gentleman's agreement plutôt que par le compilateur. Dans OOP, la séparation modèle-vue est imposée par un gentleman's agreement plutôt que par le compilateur.

Karl Bielefeldt
la source
13

Si vous ne le saviez pas, prenez déjà cette idée: les concepts d' objet et de fermeture sont les deux faces d'une même pièce. 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 du point de vue objet-objet, vous faites effectivement la même chose lorsque, par exemple, vous transmettez quelque chose dans un constructeur afin que vous puissiez ensuite l'utiliser ultérieurement. 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 malfaisant de le faire (même si, pragmatiquement, un mal est souvent nécessaire pour que le travail soit effectué). L'utilisation de variables globales va à l'extrême, les fonctions d'un programme utilisant des variables à la portée du programme - vraiment très pervers. Il y abonnes descriptions ailleurs sur la raison pour laquelle les variables globales sont mauvaises.

Si vous suivez des techniques OO, vous acceptez en principe déjà que chaque module de votre programme aura un certain niveau minimum de mal. Si vous utilisez une approche fonctionnelle de la programmation, vous visez un idéal dans lequel aucun module de votre programme ne contiendra mal la fermeture, bien que vous en ayez peut-être encore, mais ce sera beaucoup moins que OO.

C'est l'inconvénient de OO: cela encourage ce genre de mal à faire en sorte que les données soient couplées en rendant les fermetures standard (une sorte de théorie de la programmation avec une fenêtre brisée ).

Le seul avantage, c’est que si vous saviez que vous alliez commencer par utiliser beaucoup de fermetures, OO vous fournit au moins un cadre idéologique pour vous aider à organiser cette approche de sorte que le programmeur moyen puisse la comprendre. En particulier, les variables en cours de fermeture sont explicites dans le constructeur plutôt que prises simplement de manière implicite dans une fermeture de fonction. Les programmes fonctionnels qui utilisent beaucoup de fermetures sont souvent plus cryptés que les programmes équivalents, mais pas nécessairement moins élégants :)

Benoît
la source
8
Citation du jour: "un peu de mal est souvent nécessaire pour que le travail soit fait"
GlenPeterson
5
Vous n'avez pas vraiment expliqué pourquoi les choses que vous appelez le mal sont mauvaises; vous les appelez simplement le mal. Explique pourquoi ils sont pervers et tu auras peut-être une réponse à la question de ce monsieur.
Robert Harvey
2
Votre dernier paragraphe enregistre cependant la réponse. Ce sera peut-être le seul avantage, selon vous, mais ce n’est pas une mince affaire. Nous, les "programmeurs moyens", accueillons en fait un certain nombre de cérémonies, suffisamment pour nous laisser savoir ce qui se passe.
Robert Harvey
Si OO et fermetures sont synonymes, pourquoi tant de langages OO n'ont-ils pas pu les prendre en charge de manière explicite? La page wiki C2 que vous citez est encore plus controversée (et moins consensuelle) qu’elle est normale pour ce site.
Itsbruce
1
@itsbruce Ils sont largement inutiles. Les variables qui seraient "fermées" deviendront des variables de classe transmises à l'objet.
Izkata
7

Il s'agit du couplage de type :

Une fonction intégrée dans 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 des classes de types . Il existe donc de nombreux types d'objets différents pour lesquels une fonction donnée peut fonctionner, à condition qu'il s'agisse 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 dans le 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, soyez assez général pour être utilisé sur une instance de type B ou une instance de type C.

Jimmy Hoffa
la source
3
N'est-ce pas le but des interfaces? Fournir les éléments qui permettent aux types B et C d'avoir la même apparence pour votre fonction, afin qu'elle puisse fonctionner sur plus d'un type?
Random832
2
@ Random832 absolument, mais pourquoi intégrer une fonction dans un type de données si ce n'est pour travailler avec ce type de données? La réponse: c'est la seule raison pour incorporer une fonction dans un type de données. Vous pourriez n'écrire que des classes statiques et faire en sorte que toutes vos fonctions ne se soucient pas du type de données dans lequel elles sont encapsulées pour les rendre complètement découplées de leur propre type, mais pourquoi alors se donner la peine de les insérer dans un type? L'approche fonctionnelle dit: Ne vous embêtez pas, écrivez vos fonctions pour travailler avec des interfaces de toutes sortes, et il n'y a alors aucune raison de les encapsuler avec vos données.
Jimmy Hoffa
Vous devez encore implémenter les interfaces.
Random832
2
@ Random832 les interfaces sont des types de données; ils n'ont pas besoin de fonctions encapsulées dans eux. Avec les fonctions libres, toutes les interfaces dont vous avez besoin sont les données qu’elles mettent à la disposition des fonctions.
Jimmy Hoffa
2
@ Random832 pour établir une relation avec des objets du monde réel comme il est si courant dans OO, pensez à l'interface d'un livre: il présente des informations (données), c'est tout. Vous avez la fonction gratuite de turn-page qui va à l’encontre de la classe des types qui ont des pages, cette fonction s’applique à toutes sortes de livres, de journaux, aux affiches de K-Mart, aux cartes de vœux, aux courriers, à tout ce qui est agrafé dans le coin. Si vous avez implémenté le turn-page en tant que membre du livre, vous ratez toutes les choses que vous pourriez utiliser, en tant que fonction libre, ce n'est pas lié; il jette simplement une exception PartyFoulException sur de la bière.
Jimmy Hoffa
4

En Java et dans les incarnations de POO similaires, 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 plus 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. Une approche plus flexible, comme les classes de types de Haskell, devrait pouvoir le faire.

CodesInChaos
la source
Vous pouvez le faire facilement à Scala. Je ne connais pas bien Go, mais autant que je sache, vous pouvez le faire là aussi. En Ruby, il est également assez courant d'ajouter des méthodes aux objets après coup pour les rendre conformes à certaines interfaces. Ce que vous décrivez ressemble plutôt à un système de types mal conçu qu'à tout ce qui est lié, même à distance, à OO. Juste comme une expérience de pensée: en quoi votre réponse serait-elle différente quand vous parlez de types de données abstraits au lieu d'objets? Je ne crois pas que cela ferait une différence, ce qui prouverait que votre argument n'est pas lié à OO.
Jörg W Mittag
1
@ JörgWMittag Je pense que vous vouliez parler des types de données algébriques. Et CodesInChaos, Haskell décourage très explicitement ce que vous suggérez. C'est ce qu'on appelle une instance orpheline et émet des avertissements sur GHC.
Daniel Gratzer
3
@ JörgWMittag Mon impression est que beaucoup de ceux qui critiquent la POO critiquent la forme de la POO utilisée dans Java et des langages similaires avec sa structure de classe rigide et ses méthodes de focalisation sur les instances. Mon impression de cette citation est qu’elle critique l’attention portée aux méthodes d’instance et ne s’applique pas vraiment à d’autres saveurs de POO, comme celle utilisée par golang.
CodesInChaos
2
@CodesInChaos Alors peut-être clarifier ceci en tant que "OO basé sur une classe statique"
Daniel Gratzer
@jozefg: Je parle de types de données abstraits. Je ne vois même pas en quoi les types de données algébriques sont pertinents pour cette discussion.
Jörg W Mittag
3

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

Seules les opérations d'un seul objet peuvent inspecter la représentation des données de cet objet. Même d'autres objets du même type ne peuvent le faire. (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 de l'autre, seule la représentation des objets d' autres types est masquée.)

Cela signifie que plusieurs objets du même type peuvent avoir différentes représentations de données. Même le même objet peut avoir différentes représentations de données à différents moments. (Par exemple, en Scala, Maps et Sets basculent d’un tableau à un tableau 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 contraire du couplage serré.

Jörg W Mittag
la source
Certaines classes de la programmation orientée objet changent de structure de données interne en fonction des circonstances. Les instances d'objet de ces classes peuvent donc utiliser des représentations de données très différentes en même temps. Cacher et encapsuler des données de base, je dirais? Alors, en quoi Map in Scala est-il différent d’une classe Map correctement mise en œuvre (masquage et encapsulation de données) dans un langage POO?
Marjan Venema
Dans votre exemple, encapsuler vos données avec des fonctions d'accesseur dans une classe (et donc coupler étroitement ces fonctions à ces données) vous permet en réalité de coupler de manière lâche les instances de cette classe avec le reste de votre programme. Vous réfutez le point central de la citation - très gentil!
GlenPeterson
2

Un couplage étroit entre données et fonctions est mauvais car vous voulez pouvoir les modifier indépendamment les uns des autres et un couplage étroit rend cela difficile, car vous ne pouvez pas changer l’un sans connaître et éventuellement passer à l’autre.

Vous souhaitez que différentes données présentées à la fonction ne nécessitent aucune modification de la fonction et que vous souhaitiez également pouvoir apporter des modifications à la fonction sans nécessiter de modification des données sur lesquelles elle fonctionne pour prendre en charge ces modifications.

Michael Durrant
la source
1
Oui je voudrais ça. Mais mon expérience est que lorsque vous envoyez des données à une fonction non triviale pour laquelle elle n'a pas été explicitement conçue, cette fonction a tendance à se rompre. Je ne parle pas seulement de sécurité de type, mais de toute condition de données qui n'a pas été anticipée par les auteurs de la fonction. Si la fonction est ancienne et souvent utilisée, toute modification permettant à de nouvelles données de la traverser est susceptible de l’interrompre pour une forme de données ancienne qui doit encore fonctionner. Alors que le découplage peut être idéal pour les fonctions par rapport aux données, la réalité de ce découplage peut être difficile et dangereuse.
GlenPeterson