Question

J'ai un projet où je dois construire une quantité juste des données de configuration avant que je puisse exécuter un processus. Au cours de la phase de configuration, il est très pratique d'avoir les données mutable. Cependant, une fois la configuration terminée, je voudrais passer une vue immuable de ces données au processus fonctionnel, car ce processus dépendra de la configuration immuabilité pour un grand nombre de ses calculs (par exemple, la possibilité de pré-calcul des choses à base la configuration initiale.) Je suis venu avec une solution possible en utilisant des interfaces pour exposer une vue en lecture seule, mais je voudrais savoir si quelqu'un a rencontré des problèmes avec ce type d'approche ou s'il y a d'autres recommandations sur la façon de résoudre ce problème.

Un exemple de modèle que je suis actuellement en utilisant:

public interface IConfiguration
{
    string Version { get; }

    string VersionTag { get; }

    IEnumerable<IDeviceDescriptor> Devices { get; }

    IEnumerable<ICommandDescriptor> Commands { get; }
}

[DataContract]
public sealed class Configuration : IConfiguration
{
    [DataMember]
    public string Version { get; set; }

    [DataMember]
    public string VersionTag { get; set; }

    [DataMember]
    public List<DeviceDescriptor> Devices { get; private set; }

    [DataMember]
    public List<CommandDescriptor> Commands { get; private set; }

    IEnumerable<IDeviceDescriptor> IConfiguration.Devices
    {
        get { return Devices.Cast<IDeviceDescriptor>(); }
    }

    IEnumerable<ICommandDescriptor> IConfiguration.Commands
    {
        get { return Commands.Cast<ICommandDescriptor>(); }
    }

    public Configuration()
    {
        Devices = new List<DeviceDescriptor>();
        Commands = new List<CommandDescriptor>();
    }
}

EDIT

D'après les commentaires de M. Lippert et cdhowie, je mets ensemble les éléments suivants (supprimé certaines propriétés à simplifier):

[DataContract]
public sealed class Configuration
{
    private const string InstanceFrozen = "Instance is frozen";

    private Data _data = new Data();
    private bool _frozen;

    [DataMember]
    public string Version
    {
        get { return _data.Version; }
        set
        {
            if (_frozen) throw new InvalidOperationException(InstanceFrozen);
            _data.Version = value;
        }
    }

    [DataMember]
    public IList<DeviceDescriptor> Devices
    {
        get { return _data.Devices; }
        private set { _data.Devices.AddRange(value); }
    }

    public IConfiguration Freeze()
    {
        if (!_frozen)
        {
            _frozen = true;
            _data.Devices.Freeze();
            foreach (var device in _data.Devices)
                device.Freeze();
        }
        return _data;
    }

    [OnDeserializing]
    private void OnDeserializing(StreamingContext context)
    {
        _data = new Data();
    }

    private sealed class Data : IConfiguration
    {
        private readonly FreezableList<DeviceDescriptor> _devices = new FreezableList<DeviceDescriptor>();

        public string Version { get; set; }

        public FreezableList<DeviceDescriptor> Devices
        {
            get { return _devices; }
        }

        IEnumerable<IDeviceDescriptor> IConfiguration.Devices
        {
            get { return _devices.Select(d => d.Freeze()); }
        }
    }
}

FreezableList<T> est, comme on peut s'y attendre, une mise en œuvre congelable de IList<T>. Ce gain avantages d'isolation, au prix d'une certaine complexité supplémentaire.

Était-ce utile?

La solution

L'approche que vous décrivez fonctionne très bien si le « client » (le consommateur de l'interface) et le « serveur » (le fournisseur de la classe) ont un commun accord que:

  • le client sera poli et ne pas essayer de tirer parti des détails d'implémentation du serveur
  • le serveur sera poli et non muter l'objet après que le client a une référence.

Si vous ne disposez pas d'une bonne relation de travail entre les gens qui écrivent le client et les gens qui écrivent le serveur, les choses vont vite en forme de poire. Un bidon client grossier bien sûr « rejettera » l'immuabilité par coulée du type de configuration public. Un serveur grossier peut distribuer une vue immuable puis muter l'objet lorsque le client attend le moins.

Une approche intéressante est d'empêcher le client de voir jamais le type mutable:

public interface IReadOnly { ... }
public abstract class Frobber : IReadOnly
{
    private Frobber() {}
    public class sealed FrobBuilder
    {
        private bool valid = true;
        private RealFrobber real = new RealFrobber();
        public void Mutate(...) { if (!valid) throw ... }
        public IReadOnly Complete { valid = false; return real; }
    }
    private sealed class RealFrobber : Frobber { ... }
}

Maintenant, si vous voulez créer et muter un Frobber, vous pouvez faire un Frobber.FrobBuilder. Lorsque vous avez terminé vos mutations, vous appelez complet et obtenez une interface de lecture seule. (Et le constructeur devient invalide.) Étant donné que tous les détails de mise en œuvre de mutabilité sont cachés dans une classe imbriquée privée, vous ne pouvez pas « Rejetez » l'interface IReadOnly à RealFrobber, pour Frobber, qui n'a pas de méthodes publiques!

Ni le client hostile peuvent créer leur propre Frobber, parce que Frobber est abstrait et a un constructeur privé. La seule façon de faire un Frobber est par le constructeur.

Autres conseils

Cela fonctionne, mais les méthodes « malveillants » peut essayer de jeter un coup IConfiguration à un Configuration et ainsi contourner vos restrictions imposées par l'interface. Si vous n'êtes pas inquiet à ce sujet, votre approche fonctionnera très bien.

Je fais quelque chose comme ceci:

public class Foo {
    private bool frozen = false;

    private string something;

    public string Something {
        get { return something; }
        set {
            if (frozen)
                throw new InvalidOperationException("Object is frozen.");

            // validate value

            something = value;
        }
    }

    public void Freeze() {
        frozen = true;
    }
}

Sinon, vous pourriez profondément clone vos classes mutables en classes immuables.

Pourquoi ne pouvez-vous 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; } }
}

ou si vous ne le faites pas comme la saisie supplémentaire, faire les membres de l'ensemble interne plutôt que du public - accessible dans l'ensemble, mais pas par les clients de celui-ci

Je travaille régulièrement avec un grand cadre (ArcGIS Engine ESRI) basée sur COM que les modifications des poignées de manière très similaire dans certaines situations: il y a les interfaces IFoo « par défaut » pour un accès en lecture seule, et les interfaces IFooEdit (le cas échéant ) des modifications.

Ce cadre est assez bien connu, et je ne suis pas au courant des nombreuses plaintes au sujet de cette décision de conception particulière derrière elle.

Enfin, je pense qu'il vaut vraiment la peine une réflexion supplémentaire pour décider qui « perspective » obtient d'être celui par défaut: la perspective de lecture seule ou un accès complet. Personnellement, je faire de la lecture seule sur la valeur par défaut.

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; }
    }
}

je l'ai appelé mais je Readonly ne suis pas sûr que ce soit le meilleur nom pour ce bien.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top