Pouvons-nous vraiment utiliser l'immuabilité dans la POO sans perdre toutes les fonctionnalités clés de la POO?


11

Je vois les avantages de rendre les objets de mon programme immuables. Quand je pense vraiment à un bon design pour mon application, j'arrive souvent naturellement à ce que beaucoup de mes objets soient immuables. Cela arrive souvent au point où j'aimerais que tous mes objets soient immuables.

Cette question traite de la même idée, mais aucune réponse ne suggère ce qu'est une bonne approche de l'immuabilité et quand l'utiliser réellement. Existe-t-il de bons modèles de conception immuables? L'idée générale semble être de "rendre les objets immuables sauf si vous en avez absolument besoin pour changer", ce qui est inutile dans la pratique.

D'après mon expérience, l'immuabilité conduit de plus en plus mon code au paradigme fonctionnel et cette progression se produit toujours:

  1. Je commence à avoir besoin de structures de données persistantes (au sens fonctionnel) comme des listes, des cartes, etc.
  2. Il est extrêmement gênant de travailler avec des références croisées (par exemple, un nœud d'arbre référençant ses enfants pendant que les enfants référencent leurs parents), ce qui me fait ne pas utiliser du tout de références croisées, ce qui rend encore une fois mes structures de données et mon code plus fonctionnels.
  3. L'héritage s'arrête pour avoir un sens et je commence à utiliser la composition à la place.
  4. Toutes les idées de base de la POO comme l'encapsulation commencent à s'effondrer et mes objets commencent à ressembler à des fonctions.

À ce stade, je n'utilise pratiquement plus rien du paradigme OOP et je peux simplement passer à un langage purement fonctionnel. Ainsi ma question: existe-t-il une approche cohérente pour une bonne conception OOP immuable ou est-il toujours le cas que lorsque vous prenez l'idée immuable à son plein potentiel, vous finissez toujours par programmer dans un langage fonctionnel qui n'a plus besoin de rien du monde OOP? Existe-t-il de bonnes directives pour décider quelles classes doivent être immuables et lesquelles doivent rester modifiables pour s'assurer que la POO ne se désagrège pas?

Pour plus de commodité, je vais vous donner un exemple. Ayons un ChessBoardcomme une collection immuable de pièces d'échecs immuables (extension de la classe abstraitePiece). Du point de vue POO, une pièce est responsable de générer des mouvements valides à partir de sa position sur le plateau. Mais pour générer les mouvements, la pièce a besoin d'une référence à sa planche tandis que la planche doit avoir une référence à ses pièces. Eh bien, il existe quelques astuces pour créer ces références croisées immuables en fonction de votre langage OOP mais elles sont difficiles à gérer, mieux vaut ne pas avoir de pièce pour référencer sa carte. Mais alors la pièce ne peut pas générer ses mouvements car elle ne connaît pas l'état du plateau. Ensuite, la pièce devient simplement une structure de données contenant le type de pièce et sa position. Vous pouvez ensuite utiliser une fonction polymorphe pour générer des mouvements pour toutes sortes de pièces. Ceci est parfaitement réalisable dans la programmation fonctionnelle mais presque impossible dans la POO sans vérifications de type à l'exécution et autres mauvaises pratiques de POO ... Ensuite,


3
J'aime la question de base, mais j'ai du mal avec les détails. Par exemple, pourquoi l'héritage cesse-t-il d'avoir un sens lorsque vous maximisez l'immuabilité?
Martin Ba

1
Vos objets n'ont pas de méthodes?
Arrêtez de nuire à Monica le


4
"Du point de vue POO, une pièce est responsable de générer des mouvements valides à partir de sa position sur le plateau." - certainement pas, la conception d'un morceau comme ça nuirait très probablement à SRP.
Doc Brown

1
@lishaak Vous "avez du mal à expliquer le problème de l'héritage en termes simples" car ce n'est pas un problème; une mauvaise conception est un problème. L'hérédité est l'essence même de la POO, la condition sine qua non si vous voulez. Beaucoup de soi-disant «problèmes d'héritage» sont en fait des problèmes avec les langues n'ayant pas de syntaxe de substitution explicite, et ils sont beaucoup moins problématiques dans les langues mieux conçues. Sans méthodes virtuelles et polymorphisme, vous n'avez pas de POO; vous avez une programmation procédurale avec une syntaxe d'objet drôle. Il n'est donc pas étonnant que vous ne voyiez pas les avantages de l'OO si vous l'évitez!
Mason Wheeler

Réponses:


24

Pouvons-nous vraiment utiliser l'immuabilité dans la POO sans perdre toutes les fonctionnalités clés de la POO?

Je ne vois pas pourquoi. Je le fais depuis des années avant que Java 8 ne soit de toute façon fonctionnel. Avez-vous déjà entendu parler de Strings? Agréable et immuable depuis le début.

  1. Je commence à avoir besoin de structures de données persistantes (au sens fonctionnel) comme des listes, des cartes, etc.

J'en ai aussi eu besoin depuis le début. Invalider mes itérateurs parce que vous avez muté la collection pendant que je la lisais est tout simplement grossier.

  1. Il est extrêmement gênant de travailler avec des références croisées (par exemple, un nœud d'arbre référençant ses enfants pendant que les enfants référencent leurs parents), ce qui me fait ne pas utiliser du tout de références croisées, ce qui rend encore une fois mes structures de données et mon code plus fonctionnels.

Les références circulaires sont un enfer spécial. L'immuabilité ne vous en sauvera pas.

  1. L'héritage s'arrête pour avoir un sens et je commence à utiliser la composition à la place.

Eh bien, je suis avec toi mais ne vois pas ce que cela a à voir avec l'immuabilité. La raison pour laquelle j'aime la composition n'est pas parce que j'aime le modèle de stratégie dynamique, c'est parce qu'il me permet de changer mon niveau d'abstraction.

  1. Toutes les idées de base de la POO comme l'encapsulation commencent à s'effondrer et mes objets commencent à ressembler à des fonctions.

Je frémis de penser à ce que vous pensez de "la POO comme encapsulation". Si cela implique des getters et des setters, arrêtez simplement d'appeler cette encapsulation, car ce n'est pas le cas. Ça ne l'a jamais été. Il s'agit d'une programmation manuelle orientée aspect. Une chance de valider et un endroit pour mettre un point d'arrêt est sympa mais ce n'est pas de l'encapsulation. L'encapsulation préserve mon droit de ne pas savoir ou de me soucier de ce qui se passe à l'intérieur.

Vos objets sont censés ressembler à des fonctions. Ce sont des sacs de fonctions. Ce sont des sacs de fonctions qui se déplacent ensemble et se redéfinissent ensemble.

La programmation fonctionnelle est en vogue en ce moment et les gens rejettent certaines idées fausses sur la POO. Ne vous laissez pas déconcerter par la fin de la POO. Fonctionnel et OOP peuvent très bien cohabiter.

  • La programmation fonctionnelle est formelle quant aux affectations.

  • La POO est formelle concernant les pointeurs de fonction.

Vraiment ça. Dykstra nous a dit que gotoc'était dangereux, alors nous avons formalisé cela et créé une programmation structurée. Tout comme cela, ces deux paradigmes consistent à trouver des moyens de faire avancer les choses tout en évitant les pièges qui découlent de ces choses gênantes.

Laisse moi te montrer quelque chose:

f n (x)

C'est une fonction. C'est en fait un continuum de fonctions:

f 1 (x)
f 2 (x)
...
f n (x)

Devinez comment nous exprimons cela dans les langues OOP?

n.f(x)

Ce peu nlà choisit quelle implémentation fest utilisée ET il décide quelles sont certaines des constantes utilisées dans cette fonction (ce qui signifie franchement la même chose). Par exemple:

f 1 (x) = x + 1
f 2 (x) = x + 2

C'est la même chose que les fermetures. Lorsque les fermetures font référence à leur portée englobante, les méthodes objet font référence à leur état d'instance. Les objets peuvent mieux faire des fermetures. Une fermeture est une fonction unique renvoyée par une autre fonction. Un constructeur renvoie une référence à un ensemble complet de fonctions:

g 1 (x) = x 2 + 1
g 2 (x) = x 2 + 2

Oui vous l'avez deviné:

n.g(x)

f et g sont des fonctions qui changent et se déplacent ensemble. Nous les plaçons donc dans le même sac. C'est vraiment ce qu'est un objet. Maintenir nconstant (immuable) signifie simplement qu'il est plus facile de prédire ce que cela fera lorsque vous les appellerez.

Maintenant, c'est juste la structure. La façon dont je pense à la POO est un tas de petites choses qui parlent à d'autres petites choses. Si tout va bien à un petit groupe choisi de petites choses. Quand je code, je m'imagine COMME l'objet. Je regarde les choses du point de vue de l'objet. Et j'essaie d'être paresseux pour ne pas trop travailler l'objet. Je prends des messages simples, je les travaille un peu et n'envoie des messages simples qu'à mes meilleurs amis. Quand j'en ai fini avec cet objet, j'en saute dans un autre et regarde les choses dans leur perspective.

Les cartes de responsabilité de classe ont été les premières à m'apprendre à penser de cette façon. Mec, j'étais confus à leur sujet à l'époque, mais bon sang si elles ne sont toujours pas pertinentes aujourd'hui.

Ayons un ChessBoard comme une collection immuable de pièces d'échecs immuables (extension de la classe abstraite Piece). Du point de vue POO, une pièce est responsable de générer des mouvements valides à partir de sa position sur le plateau. Mais pour générer les mouvements, la pièce a besoin d'une référence à sa planche tandis que la planche doit avoir une référence à ses pièces.

Arg! Encore une fois avec les références circulaires inutiles.

Comment faire: A ChessBoardDataStructuretransforme les cordons xy en références de pièces. Ces pièces ont une méthode qui prend x, y et un particulier ChessBoardDataStructureet le transforme en une collection de s flambant neufs ChessBoardDataStructure. Pousse ensuite cela dans quelque chose qui peut choisir le meilleur coup. Maintenant, ChessBoardDataStructureles pièces peuvent être immuables. De cette façon, vous n'avez qu'un seul pion blanc en mémoire. Il y a juste plusieurs références dans les bons emplacements xy. Orienté objet, fonctionnel et immuable.

Attends, on n'a pas déjà parlé des échecs?



1
Et enfin et surtout le Traité d'Orlando .
Jörg W Mittag

1
@Euphoric: Je pense que c'est une formulation assez standard qui correspond aux définitions standard de "programmation fonctionnelle", "programmation impérative", "programmation logique", etc. Sinon, C est un langage fonctionnel parce que vous pouvez encoder FP en lui, un langage logique, car vous pouvez y coder une programmation logique, un langage dynamique, car vous pouvez y coder une frappe dynamique, un langage d'acteur, parce que vous pouvez y coder un système d'acteur, etc. Et Haskell est le plus grand langage impératif du monde car vous pouvez réellement nommer les effets secondaires, les stocker dans des variables, les passer comme…
Jörg W Mittag

1
Je pense que nous pouvons utiliser des idées similaires à celles de l'article de génie de Matthias Felleisen sur l'expressivité du langage. Le déplacement d'un programme OO de, disons, Java vers C♯ peut se faire avec seulement des transformations locales, mais le déplacer vers C nécessite une restructuration globale (fondamentalement, vous devez introduire un mécanisme de répartition des messages et rediriger tous les appels de fonctions à travers lui), donc Java et C♯ peuvent exprimer OO et C peut "seulement" le coder.
Jörg W Mittag

1
@lishaak Parce que vous le spécifiez à différentes choses. Cela le maintient à un niveau d'abstraction et empêche la duplication du stockage d'informations de position qui, autrement, pourrait devenir incohérent. Si vous n'aimez simplement pas la saisie supplémentaire, insérez-la dans une méthode afin de ne la saisir qu'une seule fois ... Un morceau n'a pas à se rappeler où il se trouve si vous lui dites où il se trouve. Désormais, les pièces ne stockent que des informations telles que la couleur, les mouvements et l'image / l'abréviation, qui sont toujours vraies et immuables.
candied_orange

2

À mon avis, les concepts les plus utiles introduits dans le courant dominant par la POO sont les suivants:

  • Modularisation appropriée.
  • Encapsulation de données.
  • Séparation claire entre les interfaces privées et publiques.
  • Mécanismes clairs pour l'extensibilité du code.

Tous ces avantages peuvent également être réalisés sans les détails d'implémentation traditionnels, comme l'héritage ou même les classes. L'idée originale d'Alan Kay d'un "système orienté objet" utilisait des "messages" au lieu de "méthodes", et était plus proche d'Erlang que par exemple C ++. Regardez Go qui supprime de nombreux détails d'implémentation de POO traditionnels, mais qui semble toujours raisonnablement orienté objet.

Si vous utilisez des objets immuables, vous pouvez toujours utiliser la plupart des goodies OOP traditionnels: interfaces, répartition dynamique, encapsulation. Vous n'avez pas besoin de setters, et souvent même pas besoin de getters pour des objets plus simples. Vous pouvez également profiter des avantages de l'immuabilité: vous êtes toujours sûr qu'un objet n'a pas changé entre-temps, pas de copie défensive, pas de course aux données, et il est facile de rendre les méthodes pures.

Découvrez comment Scala essaie de combiner les approches d'immutabilité et de PF avec la POO. Certes, ce n'est pas la langue la plus simple et la plus élégante. Il est cependant pratiquement réussi. Jetez également un œil à Kotlin qui fournit de nombreux outils et approches pour un mélange similaire.

Le problème habituel d'essayer une approche différente de celle que les créateurs de langage avaient en tête il y a N ans est le «décalage d'impédance» avec la bibliothèque standard. Les écosystèmes OTOH Java et .NET ont actuellement un support de bibliothèque standard raisonnable pour les structures de données immuables; bien sûr, des bibliothèques tierces existent également.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.