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 .
Réponses:
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.
la source
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 :)
la source
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.
la source
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.
la source
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,
Map
s etSet
s 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é.
la source
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.
la source