Question

Au cours de quelques projets, j'ai développé un modèle pour créer des objets immuables (en lecture seule) et des graphes d'objets immuables. Les objets immuables présentent l'avantage d'être 100% sécurisés pour les threads et peuvent donc être réutilisés sur les threads. Dans mon travail, j'utilise très souvent ce modèle dans les applications Web pour les paramètres de configuration et d'autres objets que je charge et met en mémoire cache. Les objets mis en cache doivent toujours être immuables pour garantir qu'ils ne soient pas modifiés de manière inattendue.

Maintenant, vous pouvez bien sûr facilement concevoir des objets immuables comme dans l'exemple suivant:

public class SampleElement
{
  private Guid id;
  private string name;

  public SampleElement(Guid id, string name)
  {
    this.id = id;
    this.name = name;
  }

  public Guid Id
  {
    get { return id; }
  }

  public string Name
  {
    get { return name; }
  }
}

Cela convient pour les classes simples - mais pour les classes plus complexes, je n’aime pas le concept de transmission de toutes les valeurs par un constructeur. Il est plus souhaitable d’avoir des paramètres sur les propriétés et votre code créant un nouvel objet devient plus facile à lire.

Alors, comment créez-vous des objets immuables avec des setters?

Dans mon modèle, les objets commencent par être totalement mutables jusqu'à ce que vous les figiez avec un seul appel de méthode. Une fois qu'un objet est gelé, il restera immuable pour toujours - il ne peut plus être transformé en objet mutable. Si vous avez besoin d’une version mutable de l’objet, il vous suffit de le cloner.

Ok, passons maintenant à du code. J'ai dans les extraits de code suivants essayé de réduire le motif à sa forme la plus simple. IElement est l'interface de base que tous les objets immuables doivent implémenter.

public interface IElement : ICloneable
{
  bool IsReadOnly { get; }
  void MakeReadOnly();
}

La classe Element est l'implémentation par défaut de l'interface IElement:

public abstract class Element : IElement
{
  private bool immutable;

  public bool IsReadOnly
  {
    get { return immutable; }
  }

  public virtual void MakeReadOnly()
  {
    immutable = true;
  }

  protected virtual void FailIfImmutable()
  {
    if (immutable) throw new ImmutableElementException(this);
  }

  ...
}

Refactorisons la classe SampleElement ci-dessus pour implémenter le modèle d'objet immuable:

public class SampleElement : Element
{
  private Guid id;
  private string name;

  public SampleElement() {}

  public Guid Id
  {
    get 
    { 
      return id; 
    }
    set
    {
      FailIfImmutable();
      id = value;
    }
  }

  public string Name
  {
    get 
    { 
      return name; 
    }
    set
    {
      FailIfImmutable();
      name = value;
    }
  }
}

Vous pouvez maintenant modifier les propriétés Id et Name tant que l'objet n'a pas été marqué comme immuable en appelant la méthode MakeReadOnly (). Une fois qu’il est immuable, appeler un setter donnera une ImmutableElementException.

Note finale: Le modèle complet est plus complexe que les extraits de code illustrés ici. Il prend également en charge des collections d'objets immuables et des graphiques d'objet complets de graphiques d'objet immuables. Le modèle complet vous permet de rendre immuable un graphe d'objet entier en appelant la méthode MakeReadOnly () sur l'objet le plus à l'extérieur. Une fois que vous commencez à créer des modèles d'objet plus volumineux à l'aide de ce modèle, le risque de fuite d'objets augmente. Un objet qui fuit est un objet qui n'appelle pas la méthode FailIfImmutable () avant d'apporter une modification à l'objet. Pour tester les fuites, j'ai également développé une classe générique de détecteurs de fuites à utiliser dans les tests unitaires. Il utilise la réflexion pour vérifier si toutes les propriétés et méthodes jettent Imm ImmutableElementException à l'état immuable. En d’autres termes, TDD est utilisé ici.

J'ai beaucoup appris à aimer ce modèle et à en tirer de grands avantages. Ce que j'aimerais savoir, c'est si certains d'entre vous utilisent des modèles similaires? Si oui, connaissez-vous de bonnes ressources qui le documentent? Je suis essentiellement à la recherche d’améliorations potentielles et de normes éventuellement existantes sur ce sujet.

Était-ce utile?

La solution

Pour info, la deuxième approche s'appelle & "; Immutabilité du popsicle &";

Eric Lippert a une série d'entrées de blog sur l'immutabilité commençant ici . Je suis encore en train de maîtriser le CTP (C # 4.0), mais il semble intéressant de voir ce que les paramètres optionnels / nommés (pour le .ctor) pourraient faire ici (lorsqu'ils sont mappés sur des champs en lecture seule) ... [mise à jour: j'ai blogué sur ce ici ]

Pour information, je ne rendrais probablement pas ces méthodes virtual - nous ne voulons probablement pas que les sous-classes puissent le rendre non congelable. Si vous voulez qu’ils puissent ajouter du code supplémentaire, je vous suggérerais quelque chose comme:

[public|protected] void Freeze()
{
    if(!frozen)
    {
        frozen = true;
        OnFrozen();
    }
}
protected virtual void OnFrozen() {} // subclass can add code here.

En outre, un AOP (tel que PostSharp) peut constituer une option viable pour l'ajout de tous ces contrôles ThrowIfFrozen ().

(excuses si j'ai changé le nom de la terminologie / méthode - SO ne garde pas le message original visible lors de la composition des réponses)

Autres conseils

Une autre option serait de créer une sorte de classe Builder.

Par exemple, en Java (et C # et de nombreux autres langages), String est immuable. Si vous souhaitez effectuer plusieurs opérations pour créer une chaîne, vous utilisez un StringBuilder. Ceci est modifiable, puis une fois que vous avez terminé, il vous renvoie l'objet final String. A partir de là, c'est immuable.

Vous pouvez faire quelque chose de similaire pour vos autres cours. Vous avez votre élément immuable, puis un ElementBuilder. Tout ce que le constructeur ferait, c’est de stocker les options que vous définissez, puis, une fois finalisées, il construit et renvoie l’élément immuable.

C’est un peu plus de code, mais je pense que c’est plus propre que d’avoir une classe supposée immuable.

Après mon malaise initial au sujet du fait que je devais créer un nouveau System.Drawing.Point à chaque modification, j’ai entièrement adopté le concept il ya quelques années. En fait, je crée maintenant chaque champ comme readonly par défaut et ne le modifie en mutable que s'il existe une raison impérieuse & # 8211; qui est étonnamment rarement.

Je ne me soucie pas beaucoup des problèmes de cross-threading, cependant (j'utilise rarement du code lorsque cela est pertinent). Je le trouve beaucoup, beaucoup mieux à cause de l'expression sémantique. L’immuabilité est au cœur d’une interface difficile à utiliser de manière incorrecte.

Vous avez toujours affaire à un état et vous pouvez donc toujours être piqué si vos objets sont parallélisés avant d’être rendus immuables.

Une méthode plus fonctionnelle pourrait être de renvoyer une nouvelle instance de l'objet avec chaque intervenant. Ou créez un objet mutable et transmettez-le au constructeur.

Le paradigme de conception de logiciel (relativement) nouveau appelé Conception dirigée par le domaine, fait la distinction entre les objets d'entité et les objets de valeur.

Les objets d'entité sont définis comme tout élément devant être mappé sur un objet géré par clé dans un magasin de données persistant, comme un employé, un client, ou une facture, etc., où la modification des propriétés de l'objet implique que vous devez enregistrer la modification dans un magasin de données quelque part et l'existence de plusieurs instances d'une classe avec le même " clé " Cela implique la nécessité de les synchroniser ou de coordonner leur persistance avec le magasin de données afin que les modifications d'une instance n'écrase pas les autres. Changer les propriétés d’un objet entité implique que vous changiez quelque chose à propos de l’objet - ne changez pas quel objet vous référencez ...

Les objets de valeur otoh, sont des objets pouvant être considérés comme immuables, dont l'utilitaire est défini strictement par leurs valeurs de propriété et pour lesquels plusieurs instances n'ont pas besoin d'être coordonnés de quelque façon que ce soit ... comme les adresses ou les numéros de téléphone, ou les roues d'une voiture, ou les lettres d'un document ... ces choses sont totalement définies par leurs propriétés ... un objet 'A' majuscule dans un éditeur de texte peut être interchangé de manière transparente avec tout autre objet 'A' majuscule Dans le document, vous n'avez pas besoin d'une clé pour le distinguer de tous les autres "A". En ce sens, il est immuable, car si vous le changez en "B" (comme si vous changiez la chaîne de numéro de téléphone dans un objet de numéro de téléphone, vous ne modifiez pas les données associées à une entité mutable, vous passez d'une valeur à une autre ... comme lorsque vous modifiez la valeur d'une chaîne ...

System.String est un bon exemple de classe immuable avec des setters et des méthodes de mutation, sauf que chaque méthode de mutation renvoie une nouvelle instance.

Développer le point de @Cory Foy et @Charles Bretana où il existe une différence entre les entités et les valeurs. Alors que les objets de valeur doivent toujours être immuables, je ne pense vraiment pas qu'un objet puisse se figer ou se permettre de se figer de manière arbitraire dans la base de code. Ça sent vraiment mauvais, et je crains qu'il ne soit difficile de localiser exactement où un objet a été gelé, et pourquoi il a été gelé, et le fait qu'entre les appels sur un objet, il puisse changer d'état, de décongelé en gelé. .

Cela ne veut pas dire que vous voulez parfois donner une entité (modifiable) à quelque chose et vous assurer que cela ne va pas être changé.

Ainsi, au lieu de figer l'objet lui-même, une autre possibilité consiste à copier la sémantique de ReadOnlyCollection < T & Gt;

List<int> list = new List<int> { 1, 2, 3};
ReadOnlyCollection<int> readOnlyList = list.AsReadOnly();

Votre objet peut prendre une partie aussi modifiable quand il en a besoin, puis être immuable lorsque vous le souhaitez.

Notez que ReadOnlyCollection < T & Gt; implémente également ICollection < T & Gt; qui a une méthode Add( T item) dans l'interface. Toutefois, bool IsReadOnly { get; } est également défini dans l'interface pour que les utilisateurs puissent vérifier avant d'appeler une méthode qui lève une exception.

La différence est que vous ne pouvez pas simplement définir IsReadOnly sur false. Une collection est ou n'est pas en lecture seule, et cela ne change jamais pour la durée de vie de la collection.

Il serait bien qu’à l’heure actuelle la correction exacte que C ++ vous donne au moment de la compilation, mais cela commence à poser ses propres problèmes et je suis heureux que C # n’y aille pas.

ICloneable : je pensais simplement revenir à ce qui suit:

  

Ne pas mettre en œuvre ICloneable

     

N'utilisez pas ICloneable dans les API publiques

Brad Abrams - Règles de conception, code géré et. NET Framework

Il s’agit d’un problème important, et j’aime beaucoup voir un soutien plus direct au cadre / langage pour le résoudre. La solution à votre disposition nécessite beaucoup de passe-partout. Il pourrait être simple d’automatiser une partie de la plate-forme à l’aide de la génération de code.

Vous généreriez une classe partielle contenant toutes les propriétés gelables. Pour ce faire, il serait assez simple de créer un modèle T4 réutilisable.

Le modèle prendrait ceci pour l'entrée:

  • espace de noms
  • nom de classe
  • liste de la propriété nom / type tuples

Et générerait un fichier C # contenant:

  • déclaration d'espace de noms
  • classe partielle
  • chacune des propriétés, avec les types correspondants, un champ de sauvegarde, un getter et un séparateur invoquant la méthode FailIfFrozen

Les balises AOP sur les propriétés gelables pourraient également fonctionner, mais elles nécessiteraient davantage de dépendances, alors que T4 est intégré aux versions plus récentes de Visual Studio.

Un autre scénario qui ressemble beaucoup à ceci est l'interface INotifyPropertyChanged. Les solutions à ce problème sont probablement applicables à ce problème.

Mon problème avec ce modèle est que vous n'imposez aucune contrainte de temps de compilation à l'immutabilité. Il incombe au codeur de s’assurer qu’un objet est défini sur immuable avant de l’ajouter par exemple à un cache ou à une autre structure non compatible avec les threads.

C'est pourquoi j'étendrais ce modèle de codage avec une contrainte au moment de la compilation sous la forme d'une classe générique, comme ceci:

public class Immutable<T> where T : IElement
{
    private T value;

    public Immutable(T mutable) 
    {
        this.value = (T) mutable.Clone();
        this.value.MakeReadOnly();
    }

    public T Value 
    {
        get 
        {
            return this.value;
        }
    }

    public static implicit operator Immutable<T>(T mutable) 
    {
        return new Immutable<T>(mutable);
    }

    public static implicit operator T(Immutable<T> immutable)
    {
        return immutable.value;
    }
}

Voici un exemple d'utilisation:

// All elements of this list are guaranteed to be immutable
List<Immutable<SampleElement>> elements = 
    new List<Immutable<SampleElement>>();

for (int i = 1; i < 10; i++) 
{
    SampleElement newElement = new SampleElement();
    newElement.Id = Guid.NewGuid();
    newElement.Name = "Sample" + i.ToString();

    // The compiler will automatically convert to Immutable<SampleElement> for you
    // because of the implicit conversion operator
    elements.Add(newElement);
}

foreach (SampleElement element in elements)
    Console.Out.WriteLine(element.Name);

elements[3].Value.Id = Guid.NewGuid();      // This will throw an ImmutableElementException

Juste un conseil pour simplifier les propriétés de l'élément: Utilisez propriétés automatiques avec private set et éviter de déclarer explicitement le champ de données. par exemple

public class SampleElement {
  public SampleElement(Guid id, string name) {
    Id = id;
    Name = name;
  }

  public Guid Id {
    get; private set;
  }

  public string Name {
    get; private set;
  }
}

Voici une nouvelle vidéo sur la chaîne 9 où Anders Hejlsberg, de l’entrevue à partir de 36h30, commence à parler de l’immuabilité en C #. Il donne un très bon exemple d'utilisation de l'immutabilité du popsicle et explique comment vous devez actuellement le mettre en œuvre. C’était de la musique à mes oreilles en l’entendant dire qu'il valait la peine de réfléchir à un meilleur support pour la création de graphiques d'objets immuables dans les futures versions de C #

D'expert à expert: Anders Hejlsberg - L'avenir de C #

Deux autres options pour votre problème particulier qui n'ont pas été discutées:

  1. Construisez votre propre désérialiseur, pouvant appeler un opérateur de définition de propriété privée. Bien que l'effort de construction du désérialiseur au début soit beaucoup plus important, cela rend les choses plus propres. Le compilateur vous empêchera même d'appeler les setters et le code de vos classes sera plus facile à lire.

  2. Placez un constructeur dans chaque classe qui prend un XElement (ou un autre type de modèle d’objet XML) et le remplit. Évidemment, à mesure que le nombre de classes augmente, cela devient rapidement une solution moins souhaitable.

Que diriez-vous d'avoir une classe abstraite ThingBase, avec les sous-classes MutableThing et ImmutableThing? ThingBase contiendrait toutes les données dans une structure protégée, fournissant des propriétés publiques en lecture seule pour les champs et une propriété protégée en lecture seule pour sa structure. Elle fournirait également une méthode AsImmutable pouvant être remplacée qui renverrait un ImmutableThing.

MutableThing masquerait les propriétés avec les propriétés lecture / écriture et fournirait à la fois un constructeur par défaut et un constructeur qui accepte une ThingBase.

Immutable est une classe scellée qui remplace AsImmutable pour simplement se retourner. Il fournirait également un constructeur qui accepte une ThingBase.

Je n'aime pas l'idée de pouvoir changer un objet d'un état mutable à un état immuable, cela semble en quelque sorte aller à l'encontre du but de la conception. Quand avez-vous besoin de faire ça? Seuls les objets représentant des VALEURS doivent être immuables

Vous pouvez utiliser des arguments nommés facultatifs avec nullables pour créer un séparateur immuable avec très peu de passe-partout. Si vous voulez vraiment définir une propriété sur null, alors vous pouvez avoir quelques problèmes supplémentaires.

class Foo{ 
    ...
    public Foo 
        Set
        ( double? majorBar=null
        , double? minorBar=null
        , int?        cats=null
        , double?     dogs=null)
    {
        return new Foo
            ( majorBar ?? MajorBar
            , minorBar ?? MinorBar
            , cats     ?? Cats
            , dogs     ?? Dogs);
    }

    public Foo
        ( double R
        , double r
        , int l
        , double e
        ) 
    {
        ....
    }
}

Vous l'utiliseriez comme ça

var f = new Foo(10,20,30,40);
var g = f.Set(cat:99);
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top