我有一个项目,在执行一个过程之前,需要构造大量的配置数据。在配置阶段,使数据可变是非常方便的。但是,一旦配置完成,我想将数据的不变 View 传递给功能过程,因为该过程的许多计算都将依赖于配置不变性(例如,基于对象进行预计算的能力)。关于初始配置。)我想出了一个可能的解决方案,使用接口(interface)公开只读 View ,但是我想知道是否有人在使用这种方法时遇到问题,或者是否还有其他建议来解决该问题。解决这个问题。

我当前使用的模式的一个示例:

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

编辑

根据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()); }
        }
    }
}

如您所料,FreezableList<T>IList<T>的可卡住实现。以一些额外的复杂性为代价,这获得了绝缘益处。

最佳答案

如果“客户端”(接口(interface)的使用者)和“服务器”(类的提供者)达成以下共识,则您描述的方法非常有用:

  • 客户端将保持礼貌,并且不会尝试利用服务器
  • 的实现细节
  • 服务器将是礼貌的,并且在客户端引用该对象后不会对该对象进行变异。

  • 如果编写客户端的人和编写服务器的人之间没有良好的工作关系,那么事情就会变得很麻烦。粗鲁的客户当然可以通过强制转换为公共(public)Configuration类型来“放弃”不变性。粗鲁的服务器可以分发不可变的 View ,然后在客户端最不希望的时候对对象进行突变。

    一种不错的方法是防止客户端看到可变类型:
    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 { ... }
    }
    

    现在,如果要创建和更改Frobber,可以制作Frobber.FrobBuilder。完成变异后,您可以调用Complete并获得一个只读界面。 (然后,构建器将变得无效。)由于所有可变性实现细节都隐藏在私有(private)嵌套类中,因此您不能将IReadOnly接口(interface)“广播”到RealFrobber,只能“广播”到没有公共(public)方法的Frobber!

    敌对的客户端也无法创建自己的Frobber,因为Frobber是抽象的并且具有私有(private)构造函数。制作Frobber的唯一方法是通过构建器。

    10-08 11:28