7

I have a project where I need to construct a fair amount of configuration data before I can execute a process. During the configuration stage, it's very convenient to have the data as mutable. However, once configuration has been completed, I'd like to pass an immutable view of that data to the functional process, as that process will rely on configuration immutability for many of its computations (for instance, the ability to pre-compute things based on initial configuration.) I've come up with a possible solution using interfaces to expose a read-only view, but I'd like to know if anybody has encountered problems with this type of approach or if there are other recommendations for how to solve this problem.

One example of the pattern I'm 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 put together 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> is, as you would expect, a freezable implementation of IList<T>. This gains insulation benefits, at the cost of some additional complexity.

Dan Bryant
  • 27,329
  • 4
  • 56
  • 102

5 Answers5

13

The approach you describe works great if the "client" (the consumer of the interface) and the "server" (the provider of the class) have a mutual agreement that:

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

If you do not have a good working relationship between the people writing the client and the people writing the server then things go pear-shaped quickly. A rude client can of course "cast away" the immutability by casting to the public Configuration type. A rude server can hand out an immutable view and then mutate the object when the client least expects it.

A nice approach is to prevent the client from ever 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 mutate a Frobber, you can make a Frobber.FrobBuilder. When you're done your mutations, you call Complete and get a read-only interface. (And then the builder becomes invalid.) Since all the mutability implementation details are hidden in a private nested class, you can't "cast away" the IReadOnly interface to RealFrobber, only to Frobber, which has no public methods!

Nor can the hostile client create their own Frobber, because Frobber is abstract and has a private constructor. The only way to make a Frobber is via the builder.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • I like this solution, though it does make the mutation a bit more awkward. At the moment, I own both the client and server portions of the application, but I can see the problems that can occur down the line when a maintenance engineer decides it's convenient to 'cast-away' the immutability or forgets that the server shouldn't modify the config after it's been handed off. My main concern with this approach is that some of the junior devs I've worked with would have a hard time understanding this kind of structure. – Dan Bryant Nov 12 '10 at 21:09
  • I think you meant to have RealFrobber implement IReadOnly, not Frobber? Otherwise Frobber must have public methods or at least explicit interface implementation. I had to mark this as the solution just for the completeness of its paranoia; the 'dummy' outer class means even other classes in the same assembly can't break the immutability contract. – Dan Bryant Nov 12 '10 at 21:58
  • One a side note, couldn't you get rid of Frobber entirely and just have RealFrobber as a nested private class of FrobBuilder? – Dan Bryant Nov 12 '10 at 22:05
  • @Dan: Yes, there are a number of ways to do this. The way I suggested is just one possibility. A common use of this pattern is to have a bunch of different specific kinds of Frobbers that the builder can build, all of which are implementation details, and all of which implement all the interfaces of the base type, but you don't have to do it that way. – Eric Lippert Nov 12 '10 at 23:09
  • @Dan Bryant: An alternative design is to have an abstract type FrobBase, which sports read-only properties for everything of interest, with a sealed child class ImmutableFrob and a possibly-unsealed class MutableFrob. ImmutableFrob should have a constructor that copies its data from a FrobBase. Code which wants something known to be immutable can take an object of type ImmutableFrob. Code which wants something it can mutate would use MutableFrob. Code which won't mutate an object itself, but won't care that something else might, would use FrobBase. – supercat Nov 15 '10 at 18:29
3

This will work, but "malicious" methods may try to cast an IConfiguration to a Configuration and thereby bypass your interface-imposed restrictions. If you're not worried about that then your approach will work fine.

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

Alternatively, you could deep-clone your mutable classes into immutable classes.

cdhowie
  • 158,093
  • 24
  • 286
  • 300
  • You're right about the vulnerability to casting, but both suggestions break the link. In order to make changes through a mutable view visible through the immutable view, it is necessary to add read-only proxy/wrapper objects. Using containment instead of inheritance for reuse prevents the cast. – Ben Voigt Nov 12 '10 at 19:43
  • The OP did not indicate in any way that the immutable view could be changed by the client while the server is processing it. – cdhowie Nov 12 '10 at 19:45
  • 1
    Well, a **really** malicious client may even revert back the value of `frozen` with reflection. – Vlad Nov 12 '10 at 19:50
  • Sure, but with reflection you could change the value of any field on any object. I usually don't care about reflection for questions like this, because reflection makes *anything* possible. – cdhowie Nov 12 '10 at 19:53
  • Casting to the derived type is not really better. Both casting and reflection are valid .NET tools, though highly not recommended in such a code. – Vlad Nov 12 '10 at 19:55
  • 4
    @Vlad: cdhowie is right. Attacks that presuppose malicious code that has full trust are not interesting attacks. The fully-trusted code doesn't have to use reflection to change the value of "frozen"; it can use unsafe code to obtain a pointer to memory and go in and trash the bits it wants to change directly. The fully trusted code can start a debugger which attaches to the process, pauses all the threads, completely rewrites internal memory structures, and then resumes the threads. Fully trusted code can do anything that you, the machine owner, can do. – Eric Lippert Nov 12 '10 at 21:08
  • @cdhowie, This does match what I'm after pretty closely. It shares a similar awkwardness to Eric's approach, as it prevents the use of auto-properties (all mutable properties must have the frozen check.) It also only catches usage failures at run-time (the clients have to know not to try to write to the properties, since the configuration will be frozen.) – Dan Bryant Nov 12 '10 at 21:31
  • 1
    @Dan: Nothing says you can't use an interface as well. Clients would then not see the set accessors, but casting around the interface would not yield them any more access; they'd just get greeted with exceptions. – cdhowie Nov 12 '10 at 21:34
  • @cdhowie, yeah, that's true. I'll have to do some additional work to better insulate the collections (which I should be doing anyway.) Your solution is simpler than Eric's, but his does have the additional benefit that you can't get the interface to hand out without freezing the instance first. – Dan Bryant Nov 12 '10 at 21:37
2

Why can't you provide a separate immutable view of the 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 extra typing, make the set members internal rather than public - accessible within the assembly but not by clients of it?

plinth
  • 48,267
  • 11
  • 78
  • 120
  • An immutable wrapper is a good approach and there are good examples out there of its use (ReadOnlyCollection, in particular). This is also easier to understand than Eric's approach, but it only protects against the client performing casting (the 'rude' server can still mutate the Configuration after passing out the view.) – Dan Bryant Nov 12 '10 at 21:15
1

I'm regularly working with a large, COM-based framework (ESRI's ArcGIS Engine) that handles modifications very similarly in some situations: there are the "default" IFoo interfaces for read-only access, and IFooEdit interfaces (where applicable) for modifications.

That framework is fairly well-known, and I'm not aware of any widespread complaints about this particular design decision behind it.

Finally, I think it's definitely worth some additional thought in deciding which "perspective" gets to be the default one: the read-only perspective or the full-access one. I would personally make the read-only view the default.

stakx - no longer contributing
  • 83,039
  • 20
  • 168
  • 268
0

How 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 it Readonly but I'm not sure that's the best name for it though.

Jesper Larsen-Ledet
  • 6,625
  • 3
  • 30
  • 42
  • This works to enforce immutability at runtime by slapping the developer on the wrist with the thrown exceptions, however it makes it difficult to detect at compile-time that the object is truly immutable. The developer (and the end-user if incorrect-usage code is shipped) will not find out until runtime (if ever) that code developed assuming mutability will never work as expected. Also, I would call this `SetOnce` instead of `ReadOnly` since that describes its mechanism more accurately. – James Dunne Feb 13 '11 at 23:15
  • There's no such thing as an immutable struct type; in your example, any code that had access to `_version.Value` and wanted to arbitrarily rewrite it could say `_version = new ReadOnly(); _version.Value = whatever;`. Even if one added a parameterized constructor to a `ReadOnly` struct and offered no other method of mutatation, a field of type `ReadOnly` would offer no more protection than a field of type `T`. – supercat Nov 25 '12 at 18:36