Immutabilité complète et programmation orientée objet

43

Dans la plupart des langages POO, les objets sont généralement modifiables avec un nombre limité d'exceptions (comme par exemple les n-uplets et les chaînes en python). Dans la plupart des langages fonctionnels, les données sont immuables.

Les objets mutables et immuables apportent toute une liste d'avantages et d'inconvénients.

Il y a des langues qui essayent de marier les deux concepts, par exemple scala où vous avez (explicitement déclaré) des données mutables et immuables (corrigez-moi si je me trompe, ma connaissance de scala est plus que limitée).

Ma question est: Est -ce que complet immuabilité (de sic!) -Ie aucun objet ne peut muter une fois qu'il a été créé- aucun sens dans un contexte de POO?

Existe-t-il des conceptions ou des implémentations d'un tel modèle?

Fondamentalement, l'immuabilité (complète) et la POO sont-elles opposées ou orthogonales?

Motivation: En POO vous fonctionner normalement sur les données, le changement (muter) les informations sous - jacentes, en gardant les références entre ces objets. Par exemple, un objet de classe Personavec un membre fatherréférençant un autre Personobjet. Si vous modifiez le nom du père, cela est immédiatement visible pour l'objet enfant sans besoin de mise à jour. Étant immuable, vous auriez besoin de construire de nouveaux objets pour le père et l'enfant. Mais vous auriez beaucoup moins de problèmes avec les objets partagés, le multi-threading, GIL, etc.

Hyperboreus
la source
2
L’immuabilité peut être simulée dans un langage POO, en exposant uniquement les points d’accès aux objets sous forme de méthodes ou de propriétés en lecture seule qui ne modifient pas les données. L'immuabilité fonctionne de la même manière dans les langages POO que dans n'importe quel langage fonctionnel, sauf que certaines fonctionnalités de langage fonctionnel peuvent vous manquer.
Robert Harvey
5
La mutabilité n'est pas une propriété de langages POO tels que C # et Java, ni l'immutabilité. Vous spécifiez la mutabilité ou l’immuabilité en fonction de la manière dont vous écrivez la classe.
Robert Harvey
9
Votre présomption semble être que la mutabilité est une caractéristique essentielle de l'orientation objet. Ce n'est pas. La mutabilité est simplement une propriété d'objets ou de valeurs. L'orientation objet englobe un certain nombre de concepts intrinsèques (encapsulation, polymorphisme, héritage, etc.) qui n'ont que peu ou rien à voir avec la mutation, et vous en tireriez tout de même les avantages. même si vous avez tout rendu immuable.
Robert Harvey
2
@MichaelT La question n'est pas de rendre des choses spécifiques mutables, mais de rendre toutes les choses immuables.

Réponses:

43

La POO et l'immuabilité sont presque complètement orthogonales les unes aux autres. Cependant, la programmation impérative et l'immuabilité ne le sont pas.

La POO peut être résumée par deux caractéristiques principales:

  • Encapsulation : je n’accéderai pas directement au contenu des objets, mais je communiquerais plutôt via une interface spécifique («méthodes») avec cet objet. Cette interface peut me cacher des données internes. Techniquement, cela concerne spécifiquement la programmation modulaire plutôt que la programmation orientée objet. L'accès aux données via une interface définie est à peu près équivalent à un type de données abstrait.

  • Dispatch dynamique : lorsque j'appelle une méthode sur un objet, la méthode exécutée sera résolue au moment de l'exécution. (Par exemple, dans la POO basée sur une classe, je pourrais appeler une sizeméthode sur une IListinstance, mais l'appel pourrait être résolu en une implémentation dans une LinkedListclasse). La répartition dynamique est un moyen d'autoriser le comportement polymorphe.

L'encapsulation a moins de sens sans mutabilité (il n'y a pas d' état interne qui pourrait être corrompu par une ingérence externe), mais elle tend toujours à faciliter les abstractions même lorsque tout est immuable.

Un programme impératif est constitué d'instructions exécutées séquentiellement. Une déclaration a des effets secondaires, tels que la modification de l’état du programme. Avec l'immutabilité, l'état ne peut pas être changé (bien sûr, un nouvel état pourrait être créé). Par conséquent, la programmation impérative est fondamentalement incompatible avec l’immutabilité.

Il se trouve maintenant que la programmation orientée objet a toujours toujours été associée à la programmation impérative (Simula est basée sur Algol) et que tous les langages orientés programmation traditionnels ont des racines impératives (C ++, Java, C #,… sont tous ancrés en C). Cela ne signifie pas que la POO elle-même serait impérative ou modifiable, cela signifie simplement que la mise en œuvre de la POO par ces langages permet la mutabilité.

Amon
la source
2
Merci beaucoup, en particulier pour la définition des deux fonctionnalités principales.
Hyperboreus
La répartition dynamique n'est pas une caractéristique essentielle de la programmation orientée objet. L’encapsulation n’est pas non plus une réalité (comme vous l’avez déjà admis).
Cessez de nuire à Monica
5
@OrangeDog Oui, il n'y a pas de définition universellement acceptée de la programmation orientée objet, mais j'avais besoin d'une définition avec laquelle travailler. J'ai donc choisi quelque chose qui soit aussi proche de la vérité que possible, sans rédiger une thèse complète à ce sujet. Cependant, je considère la répartition dynamique comme la principale caractéristique distincte de la programmation orientée objet des autres paradigmes. Quelque chose qui ressemble à la POO, mais dont tous les appels ont été résolus statiquement, n’est en réalité qu’une programmation modulaire avec polymorphisme ad hoc. Les objets sont une paire de méthodes et de données et sont donc équivalents à des fermetures.
amon
2
"Les objets sont un appariement de méthodes et de données" conviendrait. Tout ce dont vous avez besoin est quelque chose qui n'a rien à voir avec l'immuabilité.
Cessez de nuire à Monica
3
@CodeYogi Le masquage des données est le type le plus courant d'encapsulation. Cependant, la manière dont les données sont stockées en interne par un objet n'est pas le seul détail d'implémentation à masquer. Il est également important de masquer la manière dont l'interface publique est mise en œuvre, par exemple si j'utilise des méthodes d'assistance. De telles méthodes d'assistance devraient également être privées, en général. Donc, pour résumer: l’encapsulation est un principe, alors que la dissimulation de données est une technique d’encapsulation.
amon
25

Notez qu'il existe une culture parmi les programmeurs orientés objet dans laquelle les gens présument que si vous exécutez la POO, la plupart de vos objets seront mutables, mais il s'agit d'un problème distinct de celui de savoir si la POO nécessite une mutabilité. En outre, cette culture semble évoluer lentement vers une plus grande immuabilité, en raison de l'exposition des gens à la programmation fonctionnelle.

Scala est une très bonne illustration que la mutabilité n'est pas requise pour l'orientation objet. Bien que Scala prenne en charge la mutabilité, son utilisation est déconseillée. Scala idiomatique est très orienté objet et presque entièrement immuable. Cela permet surtout la mutabilité pour la compatibilité avec Java et parce que, dans certaines circonstances, les objets immuables sont inefficaces ou compliqués à utiliser.

Comparez une liste Scala et une liste Java , par exemple. La liste immuable de Scala contient toutes les mêmes méthodes objet que la liste mutable de Java. Plus, en fait, parce que Java utilise des fonctions statiques pour des opérations comme le tri , et Scala ajoute des méthodes de style fonctionnel comme map. Toutes les caractéristiques de la programmation orientée objet (encapsulation, héritage et polymorphisme) sont disponibles sous une forme familière pour les programmeurs orientés objet et utilisées de manière appropriée.

La seule différence que vous verrez est que lorsque vous modifiez la liste, vous obtenez un nouvel objet. Cela vous oblige souvent à utiliser des modèles de conception différents de ceux utilisés avec des objets mutables, mais cela ne vous oblige pas à abandonner complètement la POO.

Karl Bielefeldt
la source
17

L’immuabilité peut être simulée dans un langage POO, en exposant uniquement les points d’accès aux objets sous forme de méthodes ou de propriétés en lecture seule qui ne modifient pas les données. L'immuabilité fonctionne de la même manière dans les langages POO que dans n'importe quel langage fonctionnel, sauf que certaines fonctionnalités de langage fonctionnel peuvent vous manquer.

Votre présomption semble être que la mutabilité est une caractéristique essentielle de l'orientation objet. Mais la mutabilité est simplement une propriété d'objets ou de valeurs. L'orientation objet englobe un certain nombre de concepts intrinsèques (encapsulation, polymorphisme, héritage, etc.) qui n'ont que peu ou rien à voir avec la mutation, et vous bénéficierez toujours des avantages de ces caractéristiques, même si vous rendiez tout immuable.

Tous les langages fonctionnels ne nécessitent pas non plus l'immuabilité. Clojure a une annotation spécifique qui permet aux types d'être mutables, et la plupart des langages fonctionnels "pratiques" disposent d'un moyen de spécifier des types mutables.

Une meilleure question à poser pourrait être "L'immuabilité complète a-t-elle un sens dans la programmation impérative ?" Je dirais que la réponse évidente à cette question est non. Pour obtenir une immuabilité complète dans la programmation impérative, vous devez renoncer à des choses comme les forboucles (puisque vous devez muter une variable de boucle) au profit de la récursivité, et vous programmez maintenant essentiellement de manière fonctionnelle.

Robert Harvey
la source
Merci. Pourriez-vous préciser un peu votre dernier paragraphe ("évident" pourrait être un peu subjectif).
Hyperboreus
Déjà fait ....
Robert Harvey
1
@Hyperboreus Il existe de nombreuses façons de réaliser un polymorphisme. Le sous-typage avec dispatch dynamique, le polymorphisme ad hoc statique (alias surcharge de fonction) et le polymorphisme paramétrique (alias génériques) sont les moyens les plus courants de le faire. Tous les moyens ont leurs forces et leurs faiblesses. Les langages POO modernes combinent ces trois méthodes, alors que Haskell repose principalement sur un polymorphisme paramétrique et un polymorphisme ad hoc.
amon
3
@ RobertHarvey Vous dites que vous avez besoin de mutabilité parce que vous devez boucler (sinon vous devez utiliser la récursion). Avant d'utiliser Haskell il y a deux ans, je pensais aussi avoir besoin de variables mutables. Je dis simplement qu'il y a d'autres façons de "boucler" (carte, plier, filtrer, etc.). Une fois que vous avez retiré la boucle de la table, pourquoi auriez-vous besoin de variables mutables?
Cimmanon
1
@RobertHarvey Mais c'est précisément le but des langages de programmation: ce qui vous est exposé et non ce qui se passe sous le capot. Ce dernier est à la charge du compilateur ou de l’interprète, et non du développeur de l’application. Sinon, retour à l'assembleur.
Hyperboreus
5

Il est souvent utile de catégoriser les objets comme encapsulant des valeurs ou des entités, la distinction étant que, si une chose est une valeur, le code qui en fait référence ne devrait jamais voir son état changer d'une manière que le code lui-même n'a pas initiée. En revanche, un code qui contient une référence à une entité peut s’attendre à ce qu’il change de façon totalement indépendante de la volonté du titulaire de la référence.

Bien qu'il soit possible d'utiliser une valeur d'encapsulation à l'aide d'objets de types mutables ou immuables, un objet ne peut se comporter comme une valeur que si au moins l'une des conditions suivantes est remplie:

  1. Aucune référence à l'objet ne sera jamais exposée à quoi que ce soit qui pourrait changer l'état qui y est encapsulé.

  2. Le titulaire d'au moins une des références à l'objet connaît toutes les utilisations auxquelles une référence existante peut être affectée.

Étant donné que toutes les instances de types immuables satisfont automatiquement la première exigence, leur utilisation en tant que valeurs est simple. S'assurer que l'une ou l'autre exigence est remplie lors de l'utilisation de types mutables est, en revanche, beaucoup plus difficile. Alors que les références aux types immuables peuvent être librement échangées pour encapsuler l’état qui y est encapsulé, la transmission d’état stocké dans des types mutables nécessite soit de construire des objets enveloppants immuables, soit de copier l’état encapsulé par des objets privés dans d’autres objets qui sont: soit fourni par, soit construit pour le destinataire des données.

Les types immuables fonctionnent très bien pour transmettre des valeurs et sont souvent au moins quelque peu utilisables pour les manipuler. Cependant, ils ne sont pas très doués pour la gestion des entités. La chose la plus proche que l'on puisse avoir avec une entité dans un système avec des types purement immuables est une fonction qui, étant donné l'état du système, signalera que les attributs d'une partie de celle-ci, ou produira une nouvelle instance d'état du système qui ressemble à une fourni un, sauf pour une partie particulière de celui-ci qui sera différent d’une manière sélectionnable. De plus, si le but d’une entité est d’interfacer un code avec quelque chose qui existe dans le monde réel, il peut être impossible pour l’entité d’éviter d’exposer un état mutable.

Par exemple, si on reçoit des données via une connexion TCP, on peut produire un nouvel objet "Etat du monde" qui inclut ces données dans sa mémoire tampon sans affecter les références à l'ancien "état du monde", mais d'anciennes copies de l'état du monde qui n'inclut pas le dernier lot de données sera défectueux et ne devra pas être utilisé car il ne correspondra plus à l'état du socket TCP du monde réel.

supercat
la source
4

En c #, certains types sont immuables, comme string.

Cela semble en outre suggérer que le choix a été fortement pris en compte.

Bien sûr, il est très exigeant en performances d'utiliser des types immuables si vous devez modifier ce type des centaines de milliers de fois. C'est la raison pour laquelle il est suggéré d'utiliser la StringBuilderclasse à la place de la stringclasse dans ce cas.

J'ai fait une expérience avec un profileur et utiliser le type immuable est vraiment plus exigeant en ressources de processeur et de RAM.

C'est aussi intuitif si vous considérez que pour modifier une seule lettre sur une chaîne de 4 000 caractères, vous devez copier chaque caractère dans une autre zone de la RAM.

Révieux
la source
6
La modification fréquente de données immuables ne doit pas nécessairement être extrêmement lente, contrairement à une stringconcaténation répétée . Pour pratiquement tous les types de données / cas d'utilisation, une structure persistante efficace peut être (souvent déjà) inventée. La plupart d’entre eux ont des performances à peu près égales, même si les facteurs constants sont parfois pires.
@delnan Je pense aussi que le dernier paragraphe de la réponse concerne davantage les détails de la mise en œuvre que la (im) mutabilité.
Hyperboreus
@ Hyperboreus: pensez-vous que je devrais supprimer cette partie? Mais comment une chaîne peut-elle changer si elle est immuable? Je veux dire .. à mon avis, mais à coup sûr je peux me tromper, cela pourrait être la raison principale pour laquelle les objets ne sont pas immuables.
Revious
1
@ Revious En aucun cas. Laissez-le, il en résulte une discussion et des opinions et points de vue plus intéressants.
Hyperboreus
1
@ Revious Oui, la lecture serait plus lente, mais pas aussi lente que de changer de string(représentation traditionnelle). Une "chaîne" (dans la représentation dont je parle) après 1000 modifications serait comme une chaîne fraîchement créée (contenu modulo); aucune structure de données persistante utile ou largement utilisée ne dégrade la qualité après X opérations. La fragmentation de la mémoire n'est pas un problème sérieux (vous auriez beaucoup d'allocations, oui, mais la fragmentation n'est pas un problème dans les éboueurs modernes)
0

La totale immuabilité de tout n'a pas beaucoup de sens en POO, ou dans la plupart des autres paradigmes d'ailleurs, pour une très grande raison:

Chaque programme utile a des effets secondaires.

Un programme qui ne fait rien changer, ne vaut rien. Vous pouvez aussi ne pas l'avoir même exécuté, car l'effet sera identique.

Même si vous pensez que vous ne changez rien et que vous additionnez simplement une liste de chiffres que vous avez reçus, considérez que vous devez faire quelque chose avec le résultat - que vous l’imprimiez sur une sortie standard, l’écriviez dans un fichier, ou n'importe où. Et cela implique la mutation d'un tampon et la modification de l'état du système.

Il peut être judicieux de limiter la mutabilité aux parties qui doivent pouvoir être modifiées. Mais si absolument rien ne doit changer, alors vous ne faites rien qui vaille la peine d'être fait.

chao
la source
4
Je ne vois pas comment votre réponse se rapporte à la question car je n’ai pas abordé les langages fonctionnels purs. Prenons par exemple Erlang: données immuables, pas d’assignation destructive, pas d’intérêt pour les effets secondaires. Aussi, vous avez state dans un langage fonctionnel, seulement que l'état "coule" à travers les fonctions, contrairement aux fonctions opérant sur l'état. L'état change, mais il ne mute pas en place, mais un état futur remplace l'état actuel. L’immutabilité ne consiste pas à savoir si un tampon de mémoire est modifié ou non, mais à déterminer si ces mutations sont visibles de l’extérieur.
Hyperboreus
Et un état futur remplace l'actuel. Comment exactement? Dans un programme OO, cet état est une propriété d'un objet quelque part. Remplacer l'état nécessite de changer un objet (ou de le remplacer par un autre, ce qui nécessite une modification des objets qui y font référence (ou de le remplacer par un autre, qui ... eh. Vous obtenez le point)). Vous pouvez imaginer un type de piratage monadique, dans lequel chaque action crée une toute nouvelle application ... mais même dans ce cas, l'état actuel du programme doit être enregistré quelque part.
cHao
7
-1. Ceci est une erreur. Vous confondez les effets secondaires avec la mutation et, même s'ils sont souvent traités de la même manière par les langages fonctionnels, ils sont différents. Chaque programme utile a des effets secondaires; tous les programmes utiles ne sont pas mutés.
Michael Shaw
@Michael: En ce qui concerne la POO, les mutations et les effets secondaires sont si étroitement liés qu'ils ne peuvent pas être séparés de manière réaliste. Si vous n’avez pas de mutation, vous ne pouvez pas avoir d’effets secondaires sans quantités massives de piratage.
cHao
-2

Je pense que cela dépend si votre définition de la programmation orientée objet est qu’elle utilise un style de transmission de messages.

Les fonctions pures ne doivent rien muter car elles renvoient des valeurs que vous pouvez stocker dans de nouvelles variables.

var brandNewVariable = pureFunction(foo);

Avec le style de transmission de message, vous indiquez à un objet de stocker de nouvelles données au lieu de lui demander quelles nouvelles données vous devez stocker dans une nouvelle variable.

sameOldObject.changeMe(foo);

Il est possible d'avoir des objets et de ne pas les muter, en faisant de ses méthodes des fonctions pures qui vivent à l'intérieur de l'objet plutôt qu'à l'extérieur.

var brandNewVariable = nonMutatingObject.askMe(foo);

Mais il n'est pas possible de mélanger le style de transmission de messages et des objets immuables.

presley
la source