Comment décide-t-on si un type d'objet de données doit être conçu pour être immuable?

23

J'adore le «modèle» immuable en raison de ses points forts, et dans le passé, je l'ai trouvé bénéfique pour concevoir des systèmes avec des types de données immuables (certains, la plupart ou même tous). Souvent, lorsque je le fais, je me retrouve à écrire moins de bogues et le débogage est beaucoup plus facile.

Cependant, mes pairs en général évitent l'immuable. Ils ne sont pas du tout inexpérimentés (loin de là), mais ils écrivent des objets de données de manière classique - des membres privés avec un getter et un setter pour chaque membre. Ensuite, généralement, leurs constructeurs ne prennent aucun argument, ou peuvent simplement prendre quelques arguments pour plus de commodité. Si souvent, la création d'un objet ressemble à ceci:

Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Peut-être qu'ils font ça partout. Peut-être qu'ils ne définissent même pas un constructeur qui prend ces deux chaînes, quelle que soit leur importance. Et peut-être qu'ils ne changent pas la valeur de ces chaînes plus tard et n'en ont jamais besoin. De toute évidence, si ces choses sont vraies, l'objet serait mieux conçu comme immuable, non? (le constructeur prend les deux propriétés, pas de setters du tout).

Comment décidez-vous si un type d'objet doit être conçu comme immuable?Existe-t-il un bon ensemble de critères pour le juger?

Je suis actuellement en train de débattre de la nécessité de changer certains types de données de mon propre projet en immuables, mais je devrais le justifier auprès de mes pairs, et les données dans les types pourraient (TRÈS rarement) changer - à quel moment vous pouvez bien sûr changer il de façon immuable (créez-en un nouveau, en copiant les propriétés de l'ancien objet à l'exception de celles que vous souhaitez modifier). Mais je ne sais pas si c'est juste mon amour pour les immuables qui transparaît, ou s'il y a un réel besoin / bénéfice d'eux.

Ricket
la source
1
Programme à Erlang et tout le problème est résolu, tout est immuable
Zachary K
1
@ZacharyK Je pensais en fait à mentionner quelque chose à propos de la programmation fonctionnelle, mais je me suis abstenu en raison de mon expérience limitée avec les langages de programmation fonctionnels.
Ricket

Réponses:

27

On dirait que vous vous en approchez à l'envers. Il faut par défaut immuable. Rendez un objet modifiable uniquement si vous devez absolument / ne pouvez tout simplement pas le faire fonctionner comme un objet immuable.

Brian Knoblauch
la source
11

Le principal avantage des objets immuables est de garantir la sécurité des fils. Dans un monde où plusieurs cœurs et threads sont la norme, cet avantage est devenu très important.

Mais l'utilisation d'objets mutables est très pratique. Ils fonctionnent bien et tant que vous ne les modifiez pas à partir de threads séparés et que vous avez une bonne compréhension de ce que vous faites, ils sont assez fiables.

Robert Harvey
la source
15
L'avantage des objets immuables est qu'ils sont plus faciles à raisonner. Dans un environnement multithread, c'est beaucoup plus facile; dans les algorithmes simples à un seul thread, c'est encore plus facile. Par exemple, vous n'avez jamais à vous soucier de la cohérence de l'état, l'objet a-t-il passé toutes les mutations nécessaires pour être utilisé dans un certain contexte, etc. Les meilleurs objets mutables vous permettent également de vous soucier peu de leur état exact: par exemple, les caches.
9000
-1, je suis d'accord avec @ 9000. La sécurité des threads est secondaire (considérez les objets qui apparaissent immuables mais qui ont un état mutable interne en raison, par exemple, de la mémorisation). De plus, la mise en place de performances modifiables est une optimisation prématurée, et il est possible de tout défendre si vous exigez que l'utilisateur "sache ce qu'il fait". Si je savais ce que je faisais tout le temps, je n'écrirais jamais un programme avec un bug.
Doval
4
@Doval: Il n'y a rien à redire. 9000 est absolument correct; les objets immuables sont plus faciles à raisonner. C'est en partie ce qui les rend si utiles pour la programmation simultanée. L'autre partie est que vous n'avez pas à vous soucier du changement d'état. L'optimisation prématurée n'est pas pertinente ici; l'utilisation d'objets immuables concerne la conception, pas les performances, et un mauvais choix de structures de données à l'avance est une pessimisation prématurée.
Robert Harvey
@RobertHarvey Je ne suis pas sûr de ce que vous entendez par "mauvais choix de structure de données à l'avance". Il existe des versions immuables de la plupart des structures de données mutables offrant des performances similaires. Si vous avez besoin d'une liste et que vous avez le choix d'utiliser une liste immuable et une liste mutable, vous choisissez celle immuable jusqu'à ce que vous sachiez avec certitude qu'il s'agit d'un goulot d'étranglement dans votre application et que la version mutable fonctionnera mieux.
Doval
2
@Doval: J'ai lu Okasaki. Ces structures de données ne sont pas couramment utilisées, sauf si vous utilisez un langage qui prend pleinement en charge le paradigme fonctionnel, comme Haskell ou Lisp. Et je conteste l'idée que les structures immuables sont le choix par défaut; la grande majorité des systèmes informatiques d'entreprise sont encore conçus autour de structures mutables (c'est-à-dire des bases de données relationnelles). Commencer avec des structures de données immuables est une bonne idée, mais c'est toujours une tour très ivoire.
Robert Harvey
6

Il existe deux façons principales de décider si un objet est immuable.

a) En fonction de la nature de l'objet

Il est facile d'attraper ces situations car nous savons que ces objets ne changeront pas après leur construction. Par exemple, si vous avez une RequestHistoryentité et par nature, les entités historiques ne changent pas une fois qu'elle est construite. Ces objets peuvent être directement conçus comme des classes immuables. Gardez à l'esprit que l'objet de requête est modifiable car il peut changer son état et à qui il est affecté, etc. au fil du temps, mais l'historique des demandes ne change pas. Par exemple, un élément d'historique a été créé la semaine dernière lorsqu'il est passé de l'état soumis à l'état affecté ET cet historique ne peut jamais changer. Il s'agit donc d'un cas immuable classique.

b) En fonction du choix de conception, des facteurs externes

Ceci est similaire à l'exemple java.lang.String. Les chaînes peuvent en fait changer au fil du temps, mais par conception, elles l'ont rendu immuable en raison de facteurs de mise en cache / pool de chaînes / simultanéité. De même, la mise en cache / simultanéité, etc. peut jouer un bon rôle pour rendre un objet immuable si la mise en cache / simultanéité et les performances associées sont vitales dans l'application. Mais cette décision doit être prise très soigneusement après avoir analysé tous les impacts.

Le principal avantage des objets immuables est qu'ils ne sont pas soumis à un motif de désherbage. L'objet ne détectera aucun changement pendant la durée de vie et rend le codage et la maintenance très très faciles.

java_mouse
la source
4

Je suis actuellement en train de débattre de la nécessité de changer certains types de données de mon propre projet en immuables, mais je devrais le justifier auprès de mes pairs, et les données dans les types pourraient (TRÈS rarement) changer - à quel moment vous pouvez bien sûr changer il de façon immuable (créez-en un nouveau, en copiant les propriétés de l'ancien objet à l'exception de celles que vous souhaitez modifier).

Minimiser l'état d'un programme est très bénéfique.

Demandez-leur s'ils souhaitent utiliser un type de valeur mutable dans l'une de vos classes pour le stockage temporaire à partir d'une classe client.

S'ils disent oui, demandez pourquoi? L'état mutable n'appartient pas à un scénario comme celui-ci. Les forcer à créer l'état auquel il appartient réellement et à rendre l'état de vos types de données aussi explicite que possible sont d'excellentes raisons.

Brian
la source
4

La réponse est quelque peu dépendante de la langue. Votre code ressemble à Java, où ce problème est aussi difficile que possible. En Java, les objets ne peuvent être transmis que par référence et le clone est complètement rompu.

Il n'y a pas de réponse simple, mais pour certains, vous voulez rendre les objets de petite valeur immuables. Java a correctement rendu les chaînes immuables, mais a incorrectement modifié la date et le calendrier.

Rendez donc les objets de petite valeur immuables et implémentez un constructeur de copie. Oubliez tout sur Cloneable, il est si mal conçu qu'il est inutile.

Pour les objets de plus grande valeur, s'il est gênant de les rendre immuables, rendez-les faciles à copier.

Kevin Cline
la source
Cela ressemble beaucoup à la façon de choisir entre pile et tas lors de l'écriture en C ou C ++ :)
Ricket
@Ricket: pas tellement IMO. La pile / tas dépend de la durée de vie de l'objet. Il est très courant en C ++ d'avoir des objets mutables sur la pile.
kevin cline
1

Et peut-être qu'ils ne changent pas la valeur de ces chaînes plus tard et n'en ont jamais besoin. De toute évidence, si ces choses sont vraies, l'objet serait mieux conçu comme immuable, non?

Un peu contre-intuitivement, ne jamais avoir besoin de changer les chaînes plus tard est un assez bon argument selon lequel peu importe si les objets sont immuables ou non. Le programmeur les traite déjà comme effectivement immuables, que le compilateur les applique ou non.

L'immuabilité ne fait généralement pas de mal, mais elle n'aide pas toujours non plus. Le moyen le plus simple de savoir si votre objet pourrait bénéficier de l'immuabilité est de savoir si vous avez besoin de faire une copie de l'objet ou d'acquérir un mutex avant de le modifier . Si cela ne change jamais, alors l'immuabilité ne vous achète vraiment rien et rend parfois les choses plus compliquées.

Vous faire un bon point sur le risque de construire un objet dans un état non valide, mais ce qui est vraiment une question distincte de l' immuabilité. Un objet peut être à la fois modifiable et toujours dans un état valide après sa construction.

L'exception à cette règle est que, puisque Java ne prend en charge ni les paramètres nommés ni les paramètres par défaut, il peut parfois devenir difficile de concevoir une classe qui garantit un objet valide en utilisant simplement des constructeurs surchargés. Pas tellement avec le cas des deux propriétés, mais il y a aussi quelque chose à dire pour la cohérence, si ce modèle est fréquent avec des classes similaires mais plus grandes dans d'autres parties de votre code.

Karl Bielefeldt
la source
Je trouve curieux que les discussions sur la mutabilité ne reconnaissent pas la relation entre l'immuabilité et les façons dont un objet peut être exposé ou partagé en toute sécurité. Une référence à un objet de classe profondément immuable peut être partagée en toute sécurité avec du code non approuvé. Une référence à une instance d'une classe mutable peut être partagée si tous ceux qui détiennent une référence peuvent être autorisés à ne jamais modifier l'objet ou l'exposer au code qui pourrait le faire. Une référence à une instance de classe qui peut être mutée ne doit généralement pas être partagée du tout. Si une seule référence à un objet existe n'importe où dans l'univers ...
supercat
... et rien n'a interrogé son "code de hachage d'identité", ne l'a utilisé pour le verrouillage, ou n'a autrement accédé à ses " Objectfonctionnalités cachées ", puis changer directement un objet ne serait pas sémantiquement différent de remplacer la référence par une référence à un nouvel objet qui était identique mais pour le changement indiqué. L'une des plus grandes faiblesses sémantiques de Java, à mon humble avis, est qu'il n'a aucun moyen par lequel le code peut indiquer qu'une variable devrait seulement la seule référence non éphémère à quelque chose; les références ne doivent être transmissibles qu'aux méthodes qui ne peuvent pas conserver de copie après leur retour.
supercat
0

Je pourrais avoir une vue trop bas de cela et probablement parce que j'utilise C et C ++ qui ne rendent pas exactement si simple de rendre tout immuable, mais je vois les types de données immuables comme un détail d'optimisation afin d'écrire plus des fonctions efficaces dépourvues d'effets secondaires et capables de fournir très facilement des fonctionnalités telles que les systèmes d'annulation et l'édition non destructive.

Par exemple, cela pourrait être extrêmement coûteux:

/// @return A new mesh whose vertices have been transformed
/// by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

... if Meshn'est pas conçu pour être une structure de données persistante et était, à la place, un type de données qui nécessitait de le copier en entier (ce qui pourrait s'étendre sur des gigaoctets dans certains scénarios) même si tout ce que nous allons faire est de changer un une partie de celui-ci (comme dans le scénario ci-dessus où nous ne modifions que les positions des sommets).

C'est donc lorsque j'atteins l'immuabilité et que je conçois la structure de données pour permettre à des parties non modifiées d'être copiées et comptées de manière superficielle, pour permettre à la fonction ci-dessus d'être raisonnablement efficace sans avoir à copier en profondeur des maillages entiers tout en étant en mesure d'écrire le fonctionner sans effets secondaires, ce qui simplifie considérablement la sécurité des threads, la sécurité des exceptions, la possibilité d'annuler l'opération, de l'appliquer de manière non destructive, etc.

Dans mon cas, il est trop coûteux (au moins du point de vue de la productivité) de tout rendre immuable, donc je le garde pour les classes qui sont trop chères pour être copiées en profondeur. Ces classes sont généralement des structures de données lourdes comme des maillages et des images et j'utilise généralement une interface mutable pour leur exprimer des modifications via un objet "constructeur" pour obtenir une nouvelle copie immuable. Et je ne fais pas tant cela pour essayer d'obtenir des garanties immuables au niveau central de la classe que pour m'aider à utiliser la classe dans des fonctions qui peuvent être exemptes d'effets secondaires. Mon désir de rendre Meshimmuable ci-dessus n'est pas directement de rendre les mailles immuables, mais de permettre d'écrire facilement des fonctions exemptes d'effets secondaires qui entrent et sortent un nouveau sans payer une énorme mémoire et un coût de calcul en échange.

En conséquence, je n'ai que 4 types de données immuables dans toute ma base de code, et ce sont toutes des structures de données lourdes, mais je les utilise beaucoup pour m'aider à écrire des fonctions qui sont exemptes d'effets secondaires. Cette réponse pourrait être applicable si, comme moi, vous travaillez dans une langue qui ne rend pas si facile tout rendre immuable. Dans ce cas, vous pouvez vous concentrer sur le fait que l'essentiel de vos fonctions évite les effets secondaires, auquel cas vous souhaiterez peut-être rendre certaines structures de données immuables, les types PDS, en tant que détail d'optimisation pour éviter les copies complètes coûteuses. Pendant ce temps, quand j'ai une fonction comme celle-ci:

/// @return The v1 * v2.
Vector3f vec_mul(Vector3f v1, Vector3f v2);

... alors je n'ai aucune tentation de rendre les vecteurs immuables car ils sont assez bon marché pour simplement copier en entier. Il n'y a aucun avantage de performance à gagner ici en transformant les vecteurs en structures immuables qui peuvent copier en profondeur des pièces non modifiées. Ces coûts dépasseraient le coût de la simple copie du vecteur entier.


la source
-1
Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Si Foo n'a que deux propriétés, il est facile d'écrire un constructeur qui accepte deux arguments. Mais supposons que vous ajoutiez une autre propriété. Ensuite, vous devez ajouter un autre constructeur:

public Foo(String a, String b, SomethingElse c)

À ce stade, c'est toujours gérable. Mais que faire s'il y a 10 propriétés? Vous ne voulez pas d'un constructeur avec 10 arguments. Vous pouvez utiliser le modèle de générateur pour construire des instances, mais cela ajoute de la complexité. À ce moment, vous vous demanderez "pourquoi n'ai-je pas simplement ajouté des setters pour toutes les propriétés comme le font les gens normaux"?

Mike Baranczak
la source
4
Un constructeur avec dix arguments est-il pire que l'exception NullPointerException qui se produit lorsque property7 n'a pas été défini? Le modèle de générateur est-il plus complexe que le code pour vérifier les propriétés non initialisées dans chaque méthode? Mieux vaut vérifier une fois quand l'objet est construit.
kevin cline
2
Comme il a été dit il y a des décennies exactement pour ce cas, "Si vous avez une procédure avec dix paramètres, vous en avez probablement manqué quelques-uns."
9000
1
Pourquoi ne voudriez-vous pas un constructeur avec 10 arguments? À quel moment tracez-vous la ligne? Je pense qu'un constructeur avec 10 arguments est un petit prix à payer pour les avantages de l'immuabilité. De plus, soit vous avez une ligne avec 10 choses séparées par des virgules (éventuellement réparties en plusieurs lignes ou même 10 lignes, si cela semble mieux), ou vous avez quand même 10 lignes lorsque vous configurez chaque propriété individuellement ...
Ricket
1
@Ricket: Parce que cela augmente le risque de mettre les arguments dans le mauvais ordre . Si vous utilisez des setters ou le modèle de générateur, c'est assez improbable.
Mike Baranczak
4
Si vous avez un constructeur avec 10 arguments, il est peut-être temps de penser à encapsuler ces arguments dans une classe (ou des classes) qui leur est propre et / ou à jeter un regard critique sur la conception de votre classe.
Adam Lear