Lors de la conception de classes pour contenir votre modèle de données, j'ai lu qu'il peut être utile de créer des objets immuables, mais à quel moment la charge des listes de paramètres de constructeur et des copies complètes devient-elle trop lourde et vous devez abandonner la restriction immuable?
Par exemple, voici une classe immuable pour représenter une chose nommée (j'utilise la syntaxe C # mais le principe s'applique à tous les langages OO)
class NamedThing
{
private string _name;
public NamedThing(string name)
{
_name = name;
}
public NamedThing(NamedThing other)
{
this._name = other._name;
}
public string Name
{
get { return _name; }
}
}
Les choses nommées peuvent être construites, interrogées et copiées dans de nouvelles choses nommées mais le nom ne peut pas être changé.
Tout cela est bien, mais que se passe-t-il lorsque je veux ajouter un autre attribut? Je dois ajouter un paramètre au constructeur et mettre à jour le constructeur de copie; ce qui n'est pas trop de travail mais les problèmes commencent, autant que je sache, quand je veux rendre immuable un objet complexe .
Si la classe contient de nombreux attributs et collections, contenant d'autres classes complexes, il me semble que la liste des paramètres du constructeur deviendrait un cauchemar.
Alors à quel moment une classe devient-elle trop complexe pour être immuable?
Réponses:
Quand ils deviennent un fardeau? Très rapidement (spécialement si la langue de votre choix ne fournit pas un support syntaxique suffisant pour l'immuabilité.)
L'immuabilité est vendue comme la solution miracle pour le dilemme multicœur et tout cela. Mais l'immuabilité dans la plupart des langages OO vous oblige à ajouter des artefacts et des pratiques artificielles dans votre modèle et votre processus. Pour chaque classe complexe immuable, vous devez avoir un constructeur également complexe (au moins en interne). Peu importe comment vous le concevez, il introduit toujours un couplage fort (nous avons donc mieux une bonne raison de les introduire.)
Il n'est pas nécessairement possible de tout modéliser dans de petites classes non complexes. Donc, pour les grandes classes et les structures, nous les partitionnons artificiellement - non pas parce que cela a du sens dans notre modèle de domaine, mais parce que nous devons gérer leur instanciation complexe et les constructeurs dans le code.
C'est encore pire quand les gens poussent trop loin l'idée d'immuabilité dans un langage à usage général comme Java ou C #, ce qui rend tout immuable. Ensuite, en conséquence, vous voyez des gens forcer des constructions d'expression s dans des langages qui ne prennent pas facilement en charge de telles choses.
L'ingénierie est l'acte de modéliser à travers des compromis et des compromis. Rendre tout immuable par édit parce que quelqu'un a lu que tout est immuable en langage fonctionnel X ou Y (un modèle de programmation complètement différent), ce n'est pas acceptable. Ce n'est pas une bonne ingénierie.
De petites choses, éventuellement unitaires, peuvent devenir immuables. Des choses plus complexes peuvent être rendues immuables quand cela a du sens . Mais l'immuabilité n'est pas une solution miracle. La capacité de réduire les bugs, d'augmenter l'évolutivité et les performances, ceux-ci ne sont pas la seule fonction de l'immuabilité. C'est une fonction de bonnes pratiques d'ingénierie . Après tout, les gens ont écrit de bons logiciels évolutifs sans immuabilité.
L'immuabilité devient un fardeau très rapidement (elle ajoute à la complexité accidentelle) si elle est effectuée sans raison, lorsqu'elle est effectuée en dehors de ce qu'elle a de sens dans le contexte d'un modèle de domaine.
Pour ma part, j'essaie de l'éviter (sauf si je travaille dans un langage de programmation avec un bon support syntaxique pour cela.)
la source
J'ai traversé une phase d'insistance pour que les cours soient immuables autant que possible. Avait des constructeurs pour à peu près tout, des tableaux immuables, etc., etc. J'ai trouvé la réponse à votre question simple: à quel moment les classes immuables deviennent-elles un fardeau? Très rapidement. Dès que vous voulez sérialiser quelque chose, vous devez pouvoir désérialiser, ce qui signifie qu'il doit être modifiable; dès que vous souhaitez utiliser un ORM, la plupart insistent pour que les propriétés soient mutables. Etc.
J'ai finalement remplacé cette politique par des interfaces immuables vers des objets mutables.
Maintenant, l'objet a de la flexibilité, mais vous pouvez toujours dire au code appelant qu'il ne doit pas modifier ces propriétés.
la source
IComparable<T>
garantit que siX.CompareTo(Y)>0
etY.CompareTo(Z)>0
, alorsX.CompareTo(Z)>0
. Les interfaces ont des contrats . Si le contrat pourIImmutableList<T>
spécifie que les valeurs de tous les éléments et propriétés doivent être "figées" avant qu'une instance ne soit exposée au monde extérieur, toutes les implémentations légitimes le feront. Rien n'empêche uneIComparable
implémentation de violer la transitivité, mais les implémentations qui le font sont illégitimes. Si unSortedDictionary
dysfonctionnement lorsqu'il est donné un illégitimeIComparable
, ...IReadOnlyList<T>
seront immuables, étant donné que (1) aucune exigence de ce type n'est indiquée dans la documentation de l'interface, et (2) l'implémentation la plus couranteList<T>
, n'est-elle même pas en lecture seule ? Je ne sais pas très bien ce qui est ambigu dans mes termes: une collection est lisible si les données qu'elle contient peuvent être lues. Il est en lecture seule s'il peut promettre que les données contenues ne peuvent pas être modifiées à moins qu'une référence externe ne soit conservée par un code qui le changerait. Il est immuable s'il peut garantir qu'il ne peut pas être changé, point final.Je ne pense pas qu'il y ait de réponse générale à cela. Plus une classe est complexe, plus il est difficile de raisonner sur ses changements d'état et plus il est coûteux d'en créer de nouvelles copies. Ainsi, au-dessus d'un certain niveau (personnel) de complexité, il deviendra trop pénible de rendre / maintenir une classe immuable.
Notez qu'une classe trop complexe ou une longue liste de paramètres de méthode sont des odeurs de conception en soi, indépendamment de l'immuabilité.
Donc, généralement, la solution préférée serait de diviser une telle classe en plusieurs classes distinctes, chacune pouvant être rendue mutable ou immuable par elle-même. Si cela n'est pas possible, il peut être transformé en mutable.
la source
Vous pouvez éviter le problème de copie si vous stockez tous vos champs immuables dans un intérieur
struct
. Il s'agit essentiellement d'une variation du modèle de souvenir. Ensuite, lorsque vous voulez faire une copie, copiez simplement le souvenir:la source
Vous avez deux ou trois choses à l'œuvre ici. Les ensembles de données immuables sont parfaits pour une évolutivité multithread. Essentiellement, vous pouvez optimiser votre mémoire un peu afin qu'un ensemble de paramètres soit une instance de la classe - partout. Parce que les objets ne changent jamais, vous n'avez pas à vous soucier de la synchronisation autour de l'accès à ses membres. C'est une bonne chose. Cependant, comme vous le faites remarquer, plus l'objet est complexe, plus vous avez besoin d'une certaine mutabilité. Je commencerais par raisonner dans ce sens:
Dans les langues qui ne prennent en charge que les objets immuables (comme Erlang), s'il existe une opération qui semble modifier l'état d'un objet immuable, le résultat final est une nouvelle copie de l'objet avec la valeur mise à jour. Par exemple, lorsque vous ajoutez un élément à un vecteur / liste:
Cela peut être une façon sensée de travailler avec des objets plus compliqués. Lorsque vous ajoutez un nœud d'arbre par exemple, le résultat est un nouvel arbre avec le nœud ajouté. La méthode de l'exemple ci-dessus renvoie une nouvelle liste. Dans l'exemple de ce paragraphe, le
tree.add(newNode)
retournerait une nouvelle arborescence avec le nœud ajouté. Pour les utilisateurs, il devient facile de travailler avec. Pour les rédacteurs de bibliothèque, cela devient fastidieux lorsque le langage ne prend pas en charge la copie implicite. Ce seuil dépend de votre propre patience. Pour les utilisateurs de votre bibliothèque, la limite la plus saine que j'ai trouvée est d'environ trois à quatre sommets de paramètres.la source
Si vous avez plusieurs membres de la classe finale et que vous ne voulez pas qu'ils soient exposés à tous les objets qui doivent le créer, vous pouvez utiliser le modèle de générateur:
l'avantage est que vous pouvez facilement créer un nouvel objet avec seulement une valeur différente d'un nom différent.
la source
newObject
.NamedThing
dans ce cas)Builder
est réutilisé, il y a un risque réel que la chose se produise que j'ai mentionnée. Quelqu'un pourrait construire beaucoup d'objets et décider que puisque la plupart des propriétés sont les mêmes, pour simplement réutiliser leBuilder
, et en fait, faisons-en un singleton global qui est injecté dans les dépendances! Oups. Bugs majeurs introduits. Je pense donc que ce modèle de mélange instancié et statique est mauvais.À mon avis, cela ne vaut pas la peine de rendre les petites classes immuables dans des langues comme celle que vous montrez. J'utilise petit ici et pas complexe , car même si vous ajoutez dix champs à cette classe et qu'il y a vraiment des opérations fantaisistes sur eux, je doute que ça va prendre des kilo-octets et encore moins des mégaoctets et encore moins des gigaoctets, donc toute fonction utilisant des instances de votre La classe peut simplement faire une copie bon marché de l'objet entier pour éviter de modifier l'original si elle veut éviter de provoquer des effets secondaires externes.
Structures de données persistantes
Là où je trouve que l'utilisation personnelle de l'immuabilité est pour les grandes structures de données centrales qui regroupent un tas de données minuscules comme des instances de la classe que vous montrez, comme celle qui en stocke un million
NamedThings
. En appartenant à une structure de données persistante qui est immuable et en se trouvant derrière une interface qui ne permet qu'un accès en lecture seule, les éléments qui appartiennent au conteneur deviennent immuables sans que la classe d'élément (NamedThing
) n'ait à y faire face.Copies bon marché
La structure de données persistante permet à certaines de ses régions d'être transformées et rendues uniques, en évitant les modifications de l'original sans avoir à copier la structure de données dans son intégralité. C'est ça la vraie beauté. Si vous vouliez écrire naïvement des fonctions qui évitent les effets secondaires qui entrent dans une structure de données qui prend des gigaoctets de mémoire et ne modifie que la valeur d'un mégaoctet de mémoire, alors vous devrez copier la totalité de la chose pour éviter de toucher l'entrée et renvoyer une nouvelle production. Il s'agit soit de copier des gigaoctets pour éviter les effets secondaires ou de provoquer des effets secondaires dans ce scénario, ce qui vous oblige à choisir entre deux choix désagréables.
Avec une structure de données persistante, il vous permet d'écrire une telle fonction et d'éviter de faire une copie de la structure de données entière, ne nécessitant environ un mégaoctet de mémoire supplémentaire pour la sortie que si votre fonction a seulement transformé la valeur de la mémoire d'un mégaoctet.
Fardeau
Quant au fardeau, il y en a un au moins dans mon cas. J'ai besoin de ces constructeurs dont les gens parlent ou de "transitoires" comme je les appelle pour pouvoir exprimer efficacement les transformations de cette structure de données massive sans y toucher. Code comme celui-ci:
... doit alors être écrit comme ceci:
Mais en échange de ces deux lignes de code supplémentaires, la fonction est désormais sûre d'appeler à travers des threads avec la même liste d'origine, elle ne provoque aucun effet secondaire, etc. Cela rend également très facile de faire de cette opération une action utilisateur annulable puisque le annuler peut simplement stocker une copie peu profonde bon marché de l'ancienne liste.
Sécurité d'exception ou récupération d'erreur
Tout le monde ne pourrait pas bénéficier autant que moi des structures de données persistantes dans des contextes comme ceux-ci (j'ai trouvé tellement d'utilité pour eux dans les systèmes d'annulation et l'édition non destructive qui sont des concepts centraux dans mon domaine VFX), mais une chose applicable à peu près tout le monde à considérer est la sécurité d'exception ou la récupération d'erreur .
Si vous souhaitez sécuriser la fonction de mutation d'origine sans exception, il faut une logique de restauration, pour laquelle la mise en œuvre la plus simple nécessite de copier la liste entière :
À ce stade, la version mutable sans exception est encore plus coûteuse en termes de calcul et sans doute encore plus difficile à écrire correctement que la version immuable utilisant un "générateur". Et beaucoup de développeurs C ++ négligent simplement la sécurité des exceptions et c'est peut-être bien pour leur domaine, mais dans mon cas, j'aime m'assurer que mon code fonctionne correctement même en cas d'exception (même en écrivant des tests qui lèvent délibérément des exceptions pour tester l'exception) sécurité), et cela fait que je dois pouvoir annuler tous les effets secondaires qu'une fonction provoque à mi-chemin de la fonction si quelque chose se déclenche.
Lorsque vous voulez être protégé contre les exceptions et récupérer des erreurs avec élégance sans que votre application ne se bloque et ne brûle, vous devez annuler / annuler les effets secondaires qu'une fonction peut provoquer en cas d'erreur / d'exception. Et là, le constructeur peut réellement économiser plus de temps programmeur qu'il n'en coûte avec du temps de calcul car: ...
Revenons donc à la question fondamentale:
Ils sont toujours un fardeau dans les langages qui tournent plus autour de la mutabilité que de l'immuabilité, c'est pourquoi je pense que vous devriez les utiliser lorsque les avantages l'emportent largement sur les coûts. Mais à un niveau suffisamment large pour des structures de données suffisamment grandes, je pense qu'il existe de nombreux cas où c'est un compromis valable.
Toujours dans le mien, je n'ai que quelques types de données immuables, et ce sont toutes d'énormes structures de données destinées à stocker un nombre massif d'éléments (pixels d'une image / texture, entités et composants d'un ECS, et sommets / bords / polygones de un maillage).
la source