Classe avec des membres qui sont mutables pendant la création mais immuables après

22

J'ai un algorithme qui crée une collection d'objets. Ces objets sont mutables lors de la création, car ils commencent avec très peu, mais ils sont ensuite remplis de données à différents endroits de l'algorithme.

Une fois l'algorithme terminé, les objets ne doivent jamais être modifiés, mais ils sont consommés par d'autres parties du logiciel.

Dans ces scénarios, est-il considéré comme une bonne pratique d'avoir deux versions de la classe, comme décrit ci-dessous?

  • Le mutable est créé par l'algorithme, puis
  • À la fin de l'algorithme, les données sont copiées dans des objets immuables qui sont retournés.
Paul Richards
la source
3
Pourriez-vous modifier votre question pour clarifier quel est votre problème / question?
Simon Bergot
Vous pouvez également être intéressé par cette question: Comment figer un popsicle dans .NET (rendre une classe immuable)
R0MANARMY

Réponses:

46

Vous pouvez peut-être utiliser le modèle de générateur . Il utilise un objet «constructeur» distinct dans le but de collecter les données nécessaires, et lorsque toutes les données sont collectées, il crée l'objet réel. L'objet créé peut être immuable.

JacquesB
la source
Votre solution était la bonne pour mes besoins (qui n'étaient pas tous indiqués). Après enquête, les objets retournés n'ont pas tous les mêmes caractéristiques. La classe de générateur a beaucoup de champs nullables et lorsque les données ont été construites, elle choisit laquelle des 3 classes différentes à générer: celles-ci sont liées les unes aux autres par héritage.
Paul Richards
2
@Paul Dans ce cas, si cette réponse a résolu votre problème, vous devez la marquer comme acceptée.
Riking
24

Un moyen simple d'y parvenir serait d'avoir une interface qui permet de lire les propriétés et d'appeler uniquement des méthodes en lecture seule et une classe qui implémente cette interface qui vous permet également d'écrire cette classe.

Votre méthode qui le crée, traite le premier, puis renvoie le second fournissant uniquement une interface en lecture seule avec laquelle interagir. Cela ne nécessiterait aucune copie et vous permet d'affiner facilement les comportements que vous souhaitez mettre à la disposition de l'appelant plutôt que du créateur.

Prenez cet exemple:

public interface IPerson 
{
    public String FirstName 
    {
        get;
    }

    public String LastName 
    {
        get;
    }
} 

public class PersonImpl : IPerson 
{
    private String firstName, lastName;

    public String FirstName 
    {
        get { return firstName; }
        set { firstName = value; }
    }

    public String LastName 
    {
        get { return lastName; }
        set { lastName = value; }
    }
}

class Factory 
{
    public IPerson MakePerson() 
    {
        PersonImpl person = new PersonImpl();
        person.FirstName = 'Joe';
        person.LastName = 'Schmoe';
        return person;
    }
}

Le seul inconvénient de cette approche est que l'on peut simplement la convertir en classe d'implémentation. S'il s'agissait d'une question de sécurité, alors simplement utiliser cette approche est insuffisant. Une solution de contournement à cela est que vous pouvez créer une classe de façade pour encapsuler la classe mutable, qui présente simplement une interface avec laquelle l'appelant travaille et ne peut pas avoir accès à l'objet interne.

De cette façon, même le casting ne vous aidera pas. Les deux peuvent dériver de la même interface en lecture seule, mais la conversion de l'objet renvoyé ne vous donnera que la classe Facade, qui est immuable car elle ne change pas l'état sous-jacent de la classe mutable enveloppée.

Il convient de mentionner que cela ne suit pas la tendance typique dans laquelle un objet immuable est construit une fois pour toutes à travers son constructeur. Naturellement, vous devrez peut-être traiter de nombreux paramètres, mais vous devez vous demander si tous ces paramètres doivent être définis à l'avance ou si certains peuvent être introduits plus tard. Dans ce cas, un constructeur simple avec uniquement les paramètres requis doit être utilisé. En d'autres termes, n'utilisez pas ce modèle s'il recouvre un autre problème dans votre programme.

Neil
la source
1
La sécurité n'est pas meilleure en renvoyant un objet "en lecture seule", car le code qui obtient l'objet peut toujours modifier l'objet en utilisant la réflexion. Même une chaîne peut être modifiée (non copiée, modifiée sur place) en utilisant la réflexion.
MTilsted
Le problème de sécurité peut être parfaitement résolu avec une classe privée
Esben Skov Pedersen
@Esben: Vous devez toujours faire face à MS07-052: L'exécution de code entraîne l'exécution de code . Votre code s'exécute dans le même contexte de sécurité que leur code, ils peuvent donc simplement attacher un débogueur et faire ce qu'ils souhaitent.
Kevin
Kevin1, vous pouvez dire cela à propos de toutes les encapsulations. Je n'essaye pas de me protéger contre la réflexion.
Esben Skov Pedersen
1
Le problème avec l'utilisation du mot «sécurité» est qu'immédiatement quelqu'un suppose que ce que je dis être l'option la plus sécurisée équivaut à une sécurité maximale et aux meilleures pratiques. Je n'ai jamais dit non plus. Si vous remettez la bibliothèque à quelqu'un, à moins qu'elle ne soit obscurcie (et parfois même lorsqu'elle est obscurcie), vous pouvez oublier de garantir la sécurité. Cependant, je pense que nous pouvons tous être d'accord sur le fait que si vous trafiquez un objet de façade retourné en utilisant la réflexion afin de récupérer l'objet interne qu'il contient, vous n'utilisez pas exactement la bibliothèque comme elle devrait être utilisée.
Neil
8

Vous pouvez utiliser le modèle Builder comme le dit @JacquesB, ou bien penser pourquoi est-ce réellement que vos objets doivent être modifiables lors de la création?

En d'autres termes, pourquoi le processus de création doit-il être réparti dans le temps, au lieu de transmettre toutes les valeurs requises au constructeur et de créer des instances en une seule fois?

Parce que le constructeur peut être une bonne solution pour le mauvais problème.

Si le problème est que vous vous retrouvez avec un constructeur de 10 paramètres de long et que vous souhaitez l'atténuer en construisant l'objet petit à petit, cela pourrait indiquer que la conception est foirée, et ces 10 valeurs devraient être " ensachés "/ regroupés en quelques objets ... ou l'objet principal divisé en quelques plus petits ...

Dans ce cas - respectez l'immuabilité tout le temps, améliorez simplement la conception.

Konrad Morawski
la source
Il serait difficile de tout créer à la fois car le code effectue plusieurs requêtes de base de données pour obtenir les données; il compare les données de différentes tables et de différentes bases de données.
Paul Richards