-
09-10-2019 - |
题
我有一个项目,需要在执行过程之前构建大量的配置数据。在配置阶段,将数据称为可变非常方便。但是,一旦配置完成,我想将该数据的不变视图传递给功能过程,因为该过程将依赖于其许多计算的配置不变性(例如,可以预先计算事物的能力在初始配置时。)我想出了一个可能使用接口来揭示仅阅读视图的解决方案,但是我想知道是否有人遇到了这种方法的问题,或者是否还有其他有关如何如何处理的建议解决这个问题。
我目前正在使用的模式的一个示例:
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>
. 。这获得了隔热效果,以额外的复杂性为代价。
解决方案
如果“客户端”(界面的消费者)和“服务器”(班级提供商)具有相互协议,则您描述的方法非常有效:
- 客户将是有礼貌的,不要尝试利用服务器的实现详细信息
- 服务器将有礼貌,并且在客户端具有参考后不会突变对象。
如果您在编写客户和编写服务器的人之间没有良好的工作关系,那么事情会很快变成梨形。粗鲁的客户当然可以通过投射到公共配置类型来“抛弃”不变性。粗鲁的服务器可以分发不变的视图,然后在客户最不期望的情况下突变对象。
一种不错的方法是防止客户看到可变类型:
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。完成突变后,您致电完整并获得仅读取的界面。 (然后构建器变得无效。)由于所有突变性实现细节都隐藏在私人嵌套类中,因此您不能“抛弃”到Realfrobber的Ireadonly界面,而不是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引擎),该框架在某些情况下处理的修改非常相似:有“默认” IFoo
仅阅读访问的接口, IFooEdit
用于修改的接口(如果适用)。
该框架是相当众所周知的,我不知道对其背后的这一特殊设计决定有任何广泛的抱怨。
最后,我认为在确定哪种“视角”成为默认情况下,绝对值得一些其他想法:仅阅读视角或完整的观点。我个人会使仅阅读的视图默认值。
怎么样:
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,但我不确定这是最好的名字。