7

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

我目前正在使用的模式的一个示例:

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>. 这以一些额外的复杂性为代价获得了绝缘优势。

4

5 回答 5

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

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

恶意客户端也不能创建自己的 Frobber,因为 Frobber 是抽象的并且有一个私有的构造函数。制作 Frobber 的唯一方法是通过构建器。

于 2010-11-12T19:43:45.370 回答
3

这会起作用,但“恶意”方法可能会尝试将 an强制IConfiguration转换为 a Configuration,从而绕过您的接口强加限制。如果您对此不担心,那么您的方法将可以正常工作。

我通常会这样做:

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

或者,您可以将可变类深度克隆为不可变类。

于 2010-11-12T19:40:51.630 回答
2

为什么不能提供对象的单独不可变视图?

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

或者,如果您不喜欢额外的输入,请将集合成员设置为内部而不是公共的 - 在程序集中可访问但不能由其客户访问?

于 2010-11-12T19:56:39.770 回答
1

我经常使用基于 COM 的大型框架(ESRI 的 ArcGIS Engine),它在某些情况下处理修改的方式非常相似:有IFoo用于只读访问的“默认”接口和IFooEdit用于修改的接口(如果适用)。

这个框架是相当有名的,我不知道有任何关于它背后的特定设计决策的广泛抱怨。

最后,我认为在决定哪个“透视图”成为默认透视图时绝对值得一些额外的思考:只读透视图或完全访问透视图。我个人会将只读视图设为默认值。

于 2010-11-12T19:54:15.157 回答
0

怎么样:

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

我称它为 Readonly,但我不确定这是否是它的最佳名称。

于 2010-11-12T19:50:51.993 回答