J'ai un projet où j'ai besoin de construire une juste quantité de données de configuration avant que je puisse exécuter un processus. Pendant la phase de configuration, il est très pratique d'avoir les données aussi mutable. Toutefois, une fois la configuration terminée, j'aimerais transmettre une vue immuable de ces données au processus fonctionnel, car ce processus s'appuiera sur l'immuabilité de la configuration pour bon nombre de ses calculs (par exemple, la capacité de pré-calculer ses éléments sur la configuration initiale.) J'ai proposé une solution possible à l'aide d'interfaces pour exposer une vue en lecture seule, mais j'aimerais savoir si quelqu'un a rencontré des problèmes avec ce type d'approche ou s'il existe d'autres recommandations pour comment résoudre ce problème.
Un exemple du motif que j'utilise actuellement: p> modifier p> basé sur la contribution de M. LIPPERT et CDHOWIE, je mets ensemble ce qui suit (supprimé certaines propriétés pour simplifier): p> FreezableList
ilist
5 Réponses :
Cela fonctionnera, mais des méthodes "malveillantes" peuvent essayer de lancer un Je fais habituellement quelque chose comme ceci: p> Alternativement, vous pouvez profiter de vos classes mutables dans des classes immuables. p> p> iconfiguration code> à une configuration code> code> et de contourner ainsi vos restrictions imposées par l'interface. Si vous n'êtes pas inquiet à ce sujet, votre approche fonctionnera bien.
Vous avez raison sur la vulnérabilité de casser, mais les deux suggestions cassent le lien. Afin de modifier une vue mutable visible via la vue immuable, il est nécessaire d'ajouter des objets de proxy / wrapper en lecture seule. L'utilisation de confinement au lieu d'héritage pour la réutilisation empêche la distribution.
L'OP n'a aucun indiqué de quelque manière que la vue immuable puisse être modifiée par le client pendant que le serveur le traite.
Eh bien, un vraiment b> client malveillant peut même revenir à la valeur de gelgé code> avec réflexion.
Bien sûr, mais avec réflexion, vous pouvez modifier la valeur de tout champ sur n'importe quel objet. Je me fiche habituellement de la réflexion pour des questions comme celle-ci, car la réflexion fait n'importe quoi i> possible.
La coulée au type dérivé n'est pas vraiment meilleure. Le casting et la réflexion sont des outils .NET valides, bien que hautement non recommandé dans un tel code.
@Vlad: Cdhowie a raison. Les attaques qui présupposent du code malveillant qui a la pleine confiance ne sont pas des attaques intéressantes. Le code entièrement fiable ne doit pas nécessairement utiliser la réflexion pour modifier la valeur de "gelée"; Il peut utiliser un code dangereux pour obtenir un pointeur à la mémoire et entrer et orienter les bits qu'il souhaite changer directement. Le code entièrement fiable peut démarrer un débogueur qui se fixe au processus, pause tous les threads, réécrit complètement les structures de mémoire internes, puis reprend les threads. Le code entièrement fiable peut faire tout ce que vous, le propriétaire de la machine, peut faire.
@cdhowie, cela correspond à ce que je suis après assez étroitement. Il partage une maladresse similaire à l'approche d'Eric, car elle empêche l'utilisation des propriétés automatiques (toutes les propriétés mutables doivent avoir la vérification gelée.) Il n'atteignit également que des échecs d'utilisation au moment de l'exécution (les clients doivent savoir ne pas essayer d'écrire aux propriétés, car la configuration sera gelée.)
@Dan: Rien ne dit que vous ne pouvez pas également utiliser une interface. Les clients ne verraient alors pas les accesseurs de jeu, mais la coulée autour de l'interface ne leur donnerait plus accès; Ils seraient juste salués avec des exceptions.
@CDHOWIE, Ouais, c'est vrai. Je vais devoir faire un travail supplémentaire pour mieux isoler les collections (que je devrais faire quand même.) Votre solution est plus simple que celle d'Eric, mais ses avantages supplémentaires que vous ne pouvez pas obtenir l'interface de distribuer sans geler l'instance en premier.
L'approche que vous décrivez fonctionne bien si le "client" (le consommateur de l'interface) et le "serveur" (le fournisseur de la classe) dispose d'un accord mutuel:
Si vous n'avez pas de bonne relation de travail entre les personnes qui écrivent le client et que les gens écrivent le serveur, les choses vont rapidement en forme de poire. Un client grossier peut bien sûr "jeter" l'immuabilité en casting au type de configuration publique. Un serveur rudé peut distribuer une vue immuable, puis muter l'objet lorsque le client l'attend le moins. p>
Une bonne approche consiste à empêcher le client de voir jamais le type mutable: p> maintenant si vous voulez créer et muter une frobber, vous pouvez faire un frobber.frobbuilder. Lorsque vous avez terminé vos mutations, vous appelez complète et obtenez une interface lecture seule. (Et puis le constructeur devient invalide.) Puisque toutes les détails de la mise en œuvre de l'entretabilité sont cachés dans une classe imbriquée privée, vous ne pouvez pas "jeter" l'interface iReadonly à Realfrobber, uniquement à Frobber, qui n'a pas de méthodes publiques! P > Le client hostile ne peut pas créer son propre frobber, car Frobber est abstrait et possède un constructeur privé. La seule façon de faire une frobber est via le constructeur. P> p>
J'aime cette solution, même si cela rend la mutation un peu plus maladroite. Pour le moment, je possède à la fois les parties du client et du serveur de l'application, mais je peux voir les problèmes pouvant survenir dans la ligne lorsqu'un ingénieur de maintenance décide qu'il est pratique de «lancer» l'immuabilité ou oublie que le serveur ne devrait pas t Modifiez la configuration après avoir été remise. Ma principale préoccupation avec cette approche est que certains des Devs juniors que j'ai travaillés auraient du mal à comprendre ce type de structure.
Je pense que vous vouliez avoir une mise en œuvre de Realfrobber iReadonly, pas Frobber? Sinon, Frobber doit avoir des méthodes publiques ou au moins une mise en œuvre de l'interface explicite. J'ai dû marquer cela comme la solution juste pour l'exhaustivité de sa paranoïa; La classe extérieure «factice» signifie même d'autres classes dans la même assemblée ne peuvent pas briser le contrat d'immutabilité.
Une seule note latérale, ne pourriez-vous pas vous débarrasser complètement de Frobber et que vous ayez simplement une classe privée imbriquée de Frabbuilder?
@Dan: Oui, il y a plusieurs façons de faire cela. La façon dont j'ai suggéré est juste une possibilité. Un usage courant de ce modèle est de disposer d'un tas de différents types de frobbers spécifiques que le constructeur peut construire, tous les détails de la mise en œuvre, qui implémentent toutes toutes les interfaces du type de base, mais que vous n'avez pas à Faites-le de cette façon.
@Dan Bryant: Un design alternatif consiste à avoir un type abstrait FROBBASASE, qui fait du sport propriétés en lecture seule pour tout intérêt, avec une classe d'enfants scellée immutablefrob et une classe éventuellement non scellée mutableFrob. ImmutableFrob doit avoir un constructeur qui copie ses données d'une FROBBASE. Code qui veut que quelque chose connu soit immuable peut prendre un objet de type immutablefrob. Code qui veut quelque chose qu'il peut muté utiliserait mutablefrob. Code qui ne ménagera pas un objet lui-même, mais ne se souciera pas que quelque chose d'autre pourrait, utiliserait Frobbase.
Que diriez-vous:
struct Readonly<T> { private T _value; private bool _hasValue; public T Value { get { if (!_hasValue) throw new InvalidOperationException(); return _value; } set { if (_hasValue) throw new InvalidOperationException(); _value = value; } } } [DataContract] public sealed class Configuration { private Readonly<string> _version; [DataMember] public string Version { get { return _version.Value; } set { _version.Value = value; } } }
Cela fonctionne pour appliquer l'immuabilité au moment de l'exécution en giflant le développeur sur le poignet avec les exceptions levées, mais il est difficile de détecter au moment de la compilation que l'objet est vraiment immuable. Le développeur (et l'utilisateur final si un code d'utilisation incorrect est expédié) ne le découvrira pas avant que l'exécution (si jamais) que le code développé en supposant que la mutabilité ne fonctionne jamais comme prévu. En outre, j'appellerais ce setonce code> au lieu de
loadonly code> car cela décrit le mécanisme plus précisément.
Il n'y a pas de type de structure immuable; Dans votre exemple, n'importe quel code qui avait accès à _version.value code> et voulait réécrire arbitrairement, cela pourrait dire
_version = nouveau loadonly
Lisonly
loadonly
t code>.
Je travaille régulièrement avec un cadre important et com-composé (moteur ArcGIS d'Esri) qui gère des modifications de manière très similaire dans certaines situations: il existe les interfaces "code" Ce cadre est assez connu et je ne suis au courant d'aucune plainte généralisée concernant cette décision de conception particulière derrière elle. P>
Enfin, je pense que cela vaut certainement une pensée supplémentaire pour décider quelle "perspective" doit être la valeur par défaut: la perspective en lecture seule ou l'accès complet. Je ferais personnellement la lecture seule sur la valeur par défaut. P> ifoo code> pour un accès en lecture seule et
IFOOEdit code> Interfaces (le cas échéant) pour modifications. P>
Pourquoi ne pouvez-vous pas fournir une vue immuable séparée de l'objet?
public class ImmutableConfiguration { private Configuration _config; public ImmutableConfiguration(Configuration config) { _config = config; } public string Version { get { return _config.Version; } } }
Une enveloppe immuable est une bonne approche et il y a de bons exemples de son utilisation (readonlycollection, en particulier). Ceci est également plus facile à comprendre que l'approche d'Eric, mais elle ne protège que sur le client effectuant la coulée (le serveur «Rude» peut toujours muté la configuration après avoir passé la vue.)
Avez-vous vraiment besoin d'avoir
configuration code> public?
@Vlad, il est public de prendre en charge un projet d'éditeur de configuration distinct.