Immutable Variable Types

I have a project where I need to build enough configuration data before I can execute the process. At the configuration stage, it is very convenient to have data as volatile. However, as soon as the configuration is completed, I would like to transfer the constant representation of this data to the functional process, since this process will rely on the invariance of the configuration for many of its calculations (for example, the ability to pre-calculate things based on the initial setup.) I came up with a possible solution with using interfaces to display a read-only view, but I would like to know if anyone has encountered problems with this approach or if there are other recommendations regarding how to solve this problem.

One example of the template that I am currently using:

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

Based on input from Mr. Lippert and cdhowie, I compiled the following (removed some properties to simplify):

 [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> , as you would expect, is a frozen implementation of IList<T> . This increases the benefits of isolation due to some added complexity.

+7
immutability c # interface mutability
source share
5 answers

The approach you describe works fine if the “client” (interface consumer) and “server” (class provider) have a mutual agreement that:

  • the client will be polite and will not try to use the details of the server implementation
  • the server will be polite and will not mutate the object after the client has a link to it.

If you do not have a good working relationship between people writing a client and people writing a server, then everything becomes pear-shaped. A rude client can, of course, “discard” immutability by casting to a public configuration type. A rude server can produce an immutable representation, and then mutate an object when the client is less than that.

A good approach is to prevent the client from seeing the mutable type:

 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 { ... } } 

Now, if you want to create and modify Frobber, you can create Frobber.FrobBuilder. When you finish your mutations, you call Complete and get a read-only interface. (And then the builder becomes invalid.) Since all the details of the mutation implementation are hidden in a closed nested class, you cannot "drop" the IReadOnly interface in RealFrobber, only for Frobber, which does not have public methods!

And the hostile client cannot create his own Frobber, because Frobber is abstract and has a private constructor. The only way to make Frobber is through the constructor.

+13
source share

This will work, but the “malicious” methods may try to apply IConfiguration to the Configuration and thereby bypass the restrictions imposed by the interface. If this does not bother you, your approach will work fine.

Usually I do something like this:

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

In addition, you can deeply clone your mutable classes into immutable classes.

+3
source share

Why can't you provide a separate, unaltered view of an object?

 public class ImmutableConfiguration { private Configuration _config; public ImmutableConfiguration(Configuration config) { _config = config; } public string Version { get { return _config.Version; } } } 

or if you don’t like the additional text input, make the typing elements internal, and not public - accessible inside the assembly, but not their clients?

+2
source share

I regularly work with a large, COM-based platform (ESRI ArcGIS Engine) that handles modifications very similar in some situations: there are IFoo interfaces for read-only access, and IFooEdit interfaces (if applicable) for modifications.

This structure is pretty well known, and I don’t know of any common complaints about this particular design decision.

Finally, I think it's definitely worth considering which “perspective” is becoming standard by default: a read-only perspective or full access. I would personally do a read-only scan by default.

+1
source share

What about:

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

I called him Readonly, but I'm not sure what the best name is for him, though.

0
source share