可变类型的不可变视图

发布于 2024-10-02 03:55:20 字数 2941 浏览 2 评论 0原文

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

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

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 是,正如您一样期望的是 IList 的可冻结实现。这获得了绝缘的好处,但代价是增加了一些复杂性。

I have a project where I need to construct a fair amount of configuration data before I can execute a process. During the configuration stage, it's very convenient to have the data as mutable. However, once configuration has been completed, I'd like to pass an immutable view of that data to the functional process, as that process will rely on configuration immutability for many of its computations (for instance, the ability to pre-compute things based on initial configuration.) I've come up with a possible solution using interfaces to expose a read-only view, but I'd like to know if anybody has encountered problems with this type of approach or if there are other recommendations for how to solve this problem.

One example of the pattern I'm currently using:

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

EDIT

Based on input from Mr. Lippert and cdhowie, I put together the following (removed some properties to simplify):

[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> is, as you would expect, a freezable implementation of IList<T>. This gains insulation benefits, at the cost of some additional complexity.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

孤千羽 2024-10-09 03:55:20

如果“客户端”(接口的使用者)和“服务器”(类的提供者)达成以下共识,那么您描述的方法效果很好:

  • 客户端将有礼貌并且不会尝试利用实现服务器的详细信息
  • 服务器将是礼貌的,并且在客户端引用该对象后不会改变该对象。

如果编写客户端的人员和编写服务器的人员之间没有良好的工作关系,那么事情很快就会变得糟糕。粗鲁的客户端当然可以通过转换为公共配置类型来“抛弃”不变性。粗鲁的服务器可以分发一个不可变的视图,然后在客户端最不期望的时候改变对象。

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

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

The approach you describe works great if the "client" (the consumer of the interface) and the "server" (the provider of the class) have a mutual agreement that:

  • the client will be polite and not try to take advantage of the implementation details of the server
  • the server will be polite and not mutate the object after the client has a reference to it.

If you do not have a good working relationship between the people writing the client and the people writing the server then things go pear-shaped quickly. A rude client can of course "cast away" the immutability by casting to the public Configuration type. A rude server can hand out an immutable view and then mutate the object when the client least expects it.

A nice approach is to prevent the client from ever seeing the mutable type:

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

Now if you want to create and mutate a Frobber, you can make a Frobber.FrobBuilder. When you're done your mutations, you call Complete and get a read-only interface. (And then the builder becomes invalid.) Since all the mutability implementation details are hidden in a private nested class, you can't "cast away" the IReadOnly interface to RealFrobber, only to Frobber, which has no public methods!

Nor can the hostile client create their own Frobber, because Frobber is abstract and has a private constructor. The only way to make a Frobber is via the builder.

水波映月 2024-10-09 03:55:20

这可行,但“恶意”方法可能会尝试将 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;
    }
}

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

This will work, but "malicious" methods may try to cast an IConfiguration to a Configuration and thereby bypass your interface-imposed restrictions. If you're not worried about that then your approach will work fine.

I usually do something like this:

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

Alternatively, you could deep-clone your mutable classes into immutable classes.

可是我不能没有你 2024-10-09 03:55:20

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

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

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

Why can't you provide a separate immutable view of the object?

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

or if you don't like the extra typing, make the set members internal rather than public - accessible within the assembly but not by clients of it?

天暗了我发光 2024-10-09 03:55:20

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

该框架相当知名,而且我不知道有任何关于其背后的特定设计决策的广泛抱怨。

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

I'm regularly working with a large, COM-based framework (ESRI's ArcGIS Engine) that handles modifications very similarly in some situations: there are the "default" IFoo interfaces for read-only access, and IFooEdit interfaces (where applicable) for modifications.

That framework is fairly well-known, and I'm not aware of any widespread complaints about this particular design decision behind it.

Finally, I think it's definitely worth some additional thought in deciding which "perspective" gets to be the default one: the read-only perspective or the full-access one. I would personally make the read-only view the default.

感情洁癖 2024-10-09 03:55:20

怎么样:

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

我将其称为“只读”,但我不确定这是否是最好的名称。

How about:

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

I called it Readonly but I'm not sure that's the best name for it though.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文