可变类型的不可变视图

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

我正在使用的模式的一个例子:

public interface IConfiguration { string Version { get; } string VersionTag { get; } IEnumerable Devices { get; } IEnumerable Commands { get; } } [DataContract] public sealed class Configuration : IConfiguration { [DataMember] public string Version { get; set; } [DataMember] public string VersionTag { get; set; } [DataMember] public List Devices { get; private set; } [DataMember] public List Commands { get; private set; } IEnumerable IConfiguration.Devices { get { return Devices.Cast(); } } IEnumerable IConfiguration.Commands { get { return Commands.Cast(); } } public Configuration() { Devices = new List(); Commands = new List(); } } 

编辑

基于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 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 _devices = new FreezableList(); public string Version { get; set; } public FreezableList Devices { get { return _devices; } } IEnumerable IConfiguration.Devices { get { return _devices.Select(d => d.Freeze()); } } } } 

正如您所期望的那样, FreezableListIList的可冻结实现。 这会带来一些额外的复杂性,从而获得绝缘效益。

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

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

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

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

 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的唯一方法是通过建造者。

这将起作用,但“恶意”方法可能会尝试将IConfiguration转换为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; } } 

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

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

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

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

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

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

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

怎么样:

 struct Readonly { 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 _version; [DataMember] public string Version { get { return _version.Value; } set { _version.Value = value; } } } 

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