可变类型的不可变视图

Dan*_*ant 7 .net c# interface immutability mutability

我有一个项目,我需要在执行流程之前构建大量的配置数据.在配置阶段,将数据变为可变非常方便.但是,一旦配置完成,我想将该数据的不可变视图传递给功能过程,因为该过程将依赖于其许多计算的配置不变性(例如,基于预先计算事物的能力)初步配置.)我已经提出了一个可能的解决方案,使用接口来公开只读视图,但我想知道是否有人遇到过这种方法的问题,或者是否有其他建议如何解决这个问题.

我目前使用的模式的一个例子:

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>();
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑

基于Lippert先生和cdhowie的输入,我将以下内容放在一起(删除了一些属性以简化):

[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()); }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

FreezableList<T>正如您所料,是一个可冻结的实现IList<T>.这会带来一些额外的复杂性,从而获得绝缘效益.

Eri*_*ert 13

如果"客户端"(接口的使用者)和"服务器"(类的提供者)具有以下共同协议,则您描述的方法很有效:

  • 客户端将是礼貌的,而不是试图利用服务器的实现细节
  • 服务器将是礼貌的,并且在客户端引用它之后不会改变对象.

如果您在编写客户端的人与编写服务器的人之间没有良好的工作关系,那么事情会很快变成梨形.一个粗鲁的客户端当然可以通过强制转换为公共配置类型来"抛弃"不变性.粗鲁的服务器可以分发不可变的视图,然后在客户端最不期望的时候改变对象.

一个不错的方法是防止客户端看到可变类型:

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 { ... }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果你想创建和改变Frobber,你可以制作一个Frobber.FrobBuilder.当你完成突变时,你会调用Complete并获得一个只读界面.(然后构建器变得无效.)由于所有可变性实现细节都隐藏在私有嵌套类中,因此您不能将IReadOnly接口"抛弃"到RealFrobber,而只能"抛弃"Frobber,它没有公共方法!

敌对客户也不能创建他们自己的Frobber,因为Frobber是抽象的并且有私人构造函数.制作Frobber的唯一方法是通过建造者.