Objets mutables vs immuables

J’essaie de me concentrer sur les objects mutables vs immuables. Utiliser des objects mutables génère beaucoup de mauvaises pressions (par exemple, renvoyer un tableau de chaînes à partir d’une méthode), mais j’ai du mal à comprendre quels en sont les impacts négatifs. Quelles sont les meilleures pratiques d’utilisation des objects mutables? Devez-vous les éviter autant que possible?

Eh bien, il y a quelques aspects à cela. Premièrement, les objects mutables sans identité de référence peuvent causer des bogues à des moments étranges. Par exemple, considérons un bean Person avec une méthode equals valeur:

 Map map = ... Person p = new Person(); map.put(p, "Hey, there!"); p.setName("Daniel"); map.get(p); // => null 

L’instance Person est “perdue” dans la carte lorsqu’elle est utilisée comme clé, car le hashCode et l’égalité sont basés sur des valeurs mutables. Ces valeurs ont changé en dehors de la carte et tout le hachage est devenu obsolète. Les théoriciens aiment parler de ce point, mais dans la pratique, je ne l’ai pas trouvé trop problématique.

Un autre aspect est la logique du «raisonnable» de votre code. C’est un terme difficile à définir, englobant tout, de la lisibilité au stream. Généralement, vous devriez pouvoir regarder un morceau de code et comprendre facilement ce qu’il fait. Mais plus important que cela, vous devriez pouvoir vous convaincre qu’il fait ce qu’il fait correctement . Lorsque des objects peuvent changer indépendamment d’un code à un autre “domaine”, il devient parfois difficile de savoir où et pourquoi (“action fantasmagorique à distance”). C’est un concept plus difficile à illustrer, mais c’est quelque chose qui est souvent rencontré dans des architectures plus grandes et plus complexes.

Enfin, les objects mutables tuent dans des situations concurrentes. Chaque fois que vous accédez à un object mutable à partir de threads distincts, vous devez gérer le locking. Cela réduit le débit et rend votre code beaucoup plus difficile à gérer. Un système suffisamment compliqué élimine ce problème tellement hors de proportion qu’il devient presque impossible de le maintenir (même pour les experts en concurrence).

Les objects immuables (et plus particulièrement les collections immuables) évitent tous ces problèmes. Une fois que vous aurez compris comment ils fonctionnent, votre code deviendra quelque chose de plus facile à lire, plus facile à maintenir et moins susceptible d’échouer de manière étrange et imprévisible. Les objects immuables sont encore plus faciles à tester, en raison non seulement de leur facilité de manipulation, mais aussi des modèles de code qu’ils ont tendance à appliquer. Bref, ce sont de bonnes pratiques tout autour!

Cela dit, je ne suis guère un fanatique en la matière. Certains problèmes ne modélisent simplement pas bien quand tout est immuable. Mais je pense que vous devriez essayer de pousser autant de votre code dans cette direction que possible, en supposant bien sûr que vous utilisiez un langage qui en fasse une opinion défendable (C / C ++ rend cela très difficile, tout comme Java) . En bref: les avantages dépendent quelque peu de votre problème, mais je préférerais plutôt l’immuabilité.

Objets immuables vs collections immuables

L’un des points les plus subtils du débat sur les objects mutables par rapport aux objects immuables est la possibilité d’étendre le concept d’immuabilité aux collections. Un object immuable est un object qui représente souvent une seule structure logique de données (par exemple une chaîne immuable). Lorsque vous avez une référence à un object immuable, le contenu de l’object ne changera pas.

Une collection immuable est une collection qui ne change jamais.

Lorsque j’effectue une opération sur une collection modifiable, je modifie la collection en place et toutes les entités ayant des références à la collection voient la modification.

Lorsque j’effectue une opération sur une collection immuable, une référence est renvoyée dans une nouvelle collection reflétant le changement. Toutes les entités ayant des références aux versions précédentes de la collection ne verront pas la modification.

Les implémentations intelligentes n’ont pas nécessairement besoin de copier (cloner) la collection entière pour fournir cette immuabilité. L’exemple le plus simple est la stack implémentée sous la forme d’une liste liée et des opérations push / pop. Vous pouvez réutiliser tous les noeuds de la collection précédente dans la nouvelle collection, en ajoutant uniquement un seul noeud pour la diffusion et en ne clonant aucun noeud pour la pop. Par contre, l’opération push_tail sur une liste à liens simples n’est ni simple ni efficace.

Variables / références immuables vs Mutables

Certains langages fonctionnels intègrent le concept d’immuabilité dans les références d’objects elles-mêmes, ne permettant qu’une seule atsortingbution de référence.

  • Dans Erlang, cela est vrai pour toutes les “variables”. Je ne peux affecter des objects à une référence qu’une seule fois. Si je devais opérer sur une collection, je ne serais pas en mesure de réaffecter la nouvelle collection à l’ancienne référence (nom de la variable).
  • Scala construit également ceci dans le langage avec toutes les références étant déclarées avec var ou val , les vals n’étant qu’une seule affectation et promouvant un style fonctionnel, mais les vars permettant une structure de programme plus c-like ou java-like.
  • La déclaration var / val est requirejse, alors que de nombreux langages traditionnels utilisent des modificateurs facultatifs tels que final dans java et const dans c.

Facilité de développement et performance

Presque toujours, la raison d’utiliser un object immuable est de promouvoir une programmation sans effets secondaires et un raisonnement simple sur le code (en particulier dans un environnement hautement concurrent / parallèle). Vous n’avez pas à vous soucier de la modification des données sous-jacentes par une autre entité si l’object est immuable.

Le principal inconvénient est la performance. Voici un article sur un test simple que j’ai fait en Java en comparant certains objects immuables et mutables dans un problème de jouet.

Les problèmes de performances ne sont pas évidents dans de nombreuses applications, mais pas tous, ce qui explique pourquoi de nombreux packages numériques volumineux, tels que la classe Numpy Array de Python, autorisent les mises à jour sur place de grands tableaux. Cela serait important pour les domaines d’application utilisant des opérations masortingcielles et vectorielles importantes. Ces grands problèmes de parallélisation des données et de calcul intensif accélèrent les opérations en place.

Les objects immuables sont un concept très puissant. Ils évitent d’essayer de garder les objects / variables cohérents pour tous les clients.

Vous pouvez les utiliser pour les objects non polymorphes de bas niveau – comme une classe CPoint – utilisés principalement avec la sémantique des valeurs.

Ou vous pouvez les utiliser pour des interfaces polymorphes de haut niveau – comme une IFunction représentant une fonction mathématique – utilisée exclusivement avec la sémantique des objects.

Le plus grand avantage: immuabilité + sémantique de l’object + les pointeurs intelligents rendent la propriété de l’object non problématique, tous les clients de l’object ont leur propre copie privée par défaut. Implicitement, cela signifie également un comportement déterministe en présence de la concurrence.

Inconvénient: lorsqu’elle est utilisée avec des objects contenant beaucoup de données, la consommation de mémoire peut devenir un problème. Une solution à cela pourrait être de garder les opérations sur un object symbolique et de faire une évaluation paresseuse. Cependant, cela peut conduire à des chaînes de calculs symboliques, qui peuvent influencer négativement les performances si l’interface n’est pas conçue pour accueillir des opérations symboliques. Quelque chose à éviter absolument dans ce cas est de renvoyer d’énormes morceaux de mémoire d’une méthode. En combinaison avec des opérations symboliques en chaîne, cela pourrait entraîner une consommation massive de mémoire et une dégradation des performances.

Les objects immuables sont donc ma principale façon de penser la conception orientée object, mais ils ne sont pas un dogme. Ils résolvent beaucoup de problèmes pour les clients des objects, mais en créent également beaucoup, en particulier pour les implémenteurs.

Consultez ce billet de blog: http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html . Cela explique pourquoi les objects immuables sont mieux que mutables. En bref:

  • les objects immuables sont plus simples à construire, à tester et à utiliser
  • les objects vraiment immuables sont toujours sécuritaires pour les threads
  • ils aident à éviter le couplage temporel
  • leur utilisation est sans effet secondaire (pas de copies défensives)
  • le problème de la mutabilité de l’identité est évité
  • ils ont toujours atomicité d’échec
  • ils sont beaucoup plus faciles à mettre en cache

Vous devez spécifier la langue dont vous parlez. Pour les langages de bas niveau tels que C ou C ++, je préfère utiliser des objects modifiables pour économiser de l’espace et réduire la perte de mémoire. Dans les langages de niveau supérieur, les objects immuables facilitent la reflection sur le comportement du code (en particulier le code multithread), car il n’y a pas “d’action fantasmagorique à distance”.

Un object mutable est simplement un object pouvant être modifié après sa création / instanciation, par rapport à un object immuable qui ne peut pas être modifié (voir la page Wikipedia sur le sujet). Un exemple de cela dans un langage de programmation est les listes et les tuples Pythons. Les listes peuvent être modifiées (par exemple, de nouveaux éléments peuvent être ajoutés après leur création) alors que les tuples ne le peuvent pas.

Je ne pense pas vraiment qu’il y ait une réponse claire à la question de savoir laquelle est la meilleure pour toutes les situations. Ils ont tous deux leur place.

Si un type de classe est modifiable, une variable de ce type peut avoir plusieurs significations différentes. Par exemple, supposons qu’un object foo ait un champ int[] arr et qu’il contienne une référence à un int[3] contenant les nombres {5, 7, 9}. Même si le type du champ est connu, il peut représenter au moins quatre choses différentes:

  • Une référence potentiellement partagée, dont tous les détenteurs veillent uniquement à encapsuler les valeurs 5, 7 et 9. Si foo souhaite que les valeurs différentes soient encapsulées, il doit les remplacer par un tableau différent contenant les valeurs souhaitées. Si l’on veut faire une copie de foo , on peut donner à la copie une référence à arr ou un nouveau tableau contenant les valeurs {1,2,3}, selon ce qui est le plus pratique.

  • La seule référence, n’importe où dans l’univers, à un tableau qui encapsule les valeurs 5, 7 et 9. Ensemble de trois emplacements de stockage qui détiennent actuellement les valeurs 5, 7 et 9; Si foo veut qu’il encapsule les valeurs 5, 8 et 9, il peut soit changer le deuxième élément de ce tableau, soit créer un nouveau tableau contenant les valeurs 5, 8 et 9 et abandonner l’ancien. Notez que si vous voulez faire une copie de foo , vous devez remplacer arr par une référence à un nouveau tableau pour que foo.arr rest la seule référence à ce tableau dans l’univers.

  • Une référence à un tableau qui appartient à un autre object qui l’a exposé à foo pour une raison quelconque (par exemple, il veut peut-être y stocker certaines données). Dans ce scénario, arr ne pas encapsuler le contenu du tableau, mais plutôt son identité . Comme remplacer arr par une référence à un nouveau tableau changerait totalement sa signification, une copie de foo devrait contenir une référence au même tableau.

  • Une référence à un tableau dont foo est le seul propriétaire, mais à laquelle les références sont détenues par un autre object pour une raison quelconque (par exemple, il veut que l’autre object stocke les données là-bas – le cas précédent). Dans ce scénario, arr encapsule l’identité de la baie et son contenu. Remplacer arr par une référence à un nouveau tableau changerait totalement sa signification, mais le fait d’avoir un clone pour faire référence à foo.arr irait à l’ foo.arr l’hypothèse que foo est le seul propriétaire. Il n’y a donc aucun moyen de copier foo .

En théorie, int[] devrait être un type simple et bien défini, mais il a quatre significations très différentes. En revanche, une référence à un object immuable (par exemple, Ssortingng ) n’a généralement qu’une seule signification. Une grande partie du “pouvoir” des objects immuables provient de ce fait.

Si vous retournez des références à un tableau ou à une chaîne, le monde extérieur peut modifier le contenu de cet object et le transformer en object modifiable (modifiable).

Les moyens immuables ne peuvent être changés, et les mutations signifient que vous pouvez changer.

Les objects sont différents des primitives en Java. Les primitives sont construites dans des types (booléens, int, etc.) et les objects (classes) sont des types créés par l’utilisateur.

Les primitives et les objects peuvent être mutables ou immuables lorsqu’ils sont définis en tant que variables membres dans l’implémentation d’une classe.

Beaucoup de gens pensent que les primitives et les variables d’object ayant un modificateur final en face d’eux sont immuables, cependant, ce n’est pas exactement vrai. Donc, finale ne signifie presque pas immuable pour les variables. Voir exemple ici
http://www.siteconsortium.com/h/D0000F.php .

Les instances Mutable sont passées par référence.

Les instances immuables sont passées par valeur.

Exemple abstrait Supposons qu’il existe un fichier nommé txtfile dans mon disque dur. Maintenant, quand vous me demandez txtfile , je peux le retourner dans deux modes:

  1. Créer un raccourci vers txtfile et pas de raccourci vers vous, ou
  2. Prenez une copie pour txtfile et pas de copie pour vous.

Dans le premier mode, txtfile renvoyé est un fichier mutable, car lorsque vous effectuez des modifications dans un fichier de raccourci, vous effectuez également des modifications dans le fichier d’origine. L’avantage de ce mode est que chaque raccourci retourné nécessitait moins de mémoire (en RAM ou en disque dur) et l’inconvénient est que tout le monde (pas seulement moi, propriétaire) est autorisé à modifier le contenu du fichier.

Dans le second mode, txtfile renvoyé est un fichier immuable, car toutes les modifications apscopes au fichier reçu ne font pas référence au fichier d’origine. L’avantage de ce mode est que seul moi (le propriétaire) peut modifier le fichier d’origine et l’inconvénient est que chaque copie retournée nécessite de la mémoire (en RAM ou en disque dur).