【问题标题】:If security is not an issue, is an immutable interface a good pattern?如果安全不是问题,那么不可变接口是一个好的模式吗?
【发布时间】:2011-11-24 07:48:57
【问题描述】:

通常建议应该密封不可变类,以向消费者强制承诺观察到的类的属性将保持不变。当然,对于将在安全上下文中使用的类来说,这似乎是一个很好的做法。另一方面,在许多情况下,拥有一些具有共同基本特征的不可变类以及这些类的可编辑版本可能会很有用。

例如,一个图形程序可能有一个 DrawnText 对象,它包含一个位置、字体和字符串,以及一个派生的 DrawnFancyText 字符串,它为形状周围的曲线文本添加参数。在某些情况下,拥有这些对象的不可变版本可能很有用(例如,对于诸如撤消缓冲区之类的东西),但在其他情况下,拥有可变版本可能更有用。

在这样的上下文中,有些上下文需要一个可读的 DrawnFancyText 对象,但不关心它是否可变,但有些上下文需要 DrawnText 或 DrawnFancyText 的不可变衍生物,但不关心哪一个。实现前者需要 EditableDrawnFancyText 和 ImmutableDrawnFancyText 有一个共同的基础;实现后者需要 ImmutableDrawnText 和 ImmutableDrawnFancyText 有一个共同的基础。不幸的是,没有多重继承就无法实现这样的模式,因为 ImmutableDrawnText 与 EditableDrawnFancyText 没有关系。幸运的是,即使类不允许,接口也允许多重继承。

似乎实现正确继承关系的最佳方法是定义接口:

  1. IdrawnText
  2. IDrawnFancyText:IDrawnText
  3. IEditableDrawnText:IDrawnText
  4. IEditableDrawnFancyText:IEditableDrawnText、IDrawnFancyText
  5. IImmutableDrawnText:IDrawnText
  6. IImmutableDrawnFancyText:IImmutableDrawnText,IIDrawnFancyText

似乎让类的消费者使用接口而不是类将实现所有正确的对象关系。另一方面,暴露接口意味着消费者必须相信没有人使用允许外部突变的对象实现所谓的“不可变”接口。

对于非安全敏感信息,最好使用接口来允许适当的继承关系,并依靠实现者不违反合同?

理想情况下,可以很好地公开一个公共接口以允许外部实例被传递,而不必允许外部代码定义自己的实现。如果这是可行的,那似乎是最佳方法。不幸的是,虽然可以使用“内部”限定的构造函数公开公共抽象类,但我不知道接口有任何这种能力。不过,我不确定是否有人使用允许外部突变的对象实现“IImmutableDrawnText”一定是一个真正的问题。

编辑 IDrawnText 只会暴露 getter 而不会暴露 setter,但它的文档会明确指出实现 IDrawnText 的对象可能通过其他方式可变,也可能不可变; IImmutableDrawnText 将公开与 IDrawnText 相同的成员,但文档会明确声明允许突变的类禁止实现接口。没有什么可以阻止可变类违反约定实现 IImmutableDrawnText,但任何和所有此类类都将是该接口的破坏实现。

【问题讨论】:

  • 不变性与安全性无关
  • ..and 一切与理智有关

标签: c# .net vb.net interface immutability


【解决方案1】:

没有“不可变接口”这样的东西。有一个接口,不声明方法改变对象,但没有办法禁止改变。允许突变允许随之而来的所有线程安全和“安全”问题。

不可变类应该被密封的原因是,它们承诺一旦创建它们就不能(通常)被修改。子类可以破坏该承诺(以及与之一起的 LSP),并且禁止继承是执行该承诺的唯一方法。

顺便说一句,不变性不是为了安全。允许反射的语言/框架(例如,C#/.net?)可用于随意修改对象,忽略对象为阻止它所做的一切。不变性主要只是让意外很难做到这一点。

【讨论】:

  • 任何接口都构成承诺,任何实现它们的类都会做出承诺,并且任何接口都可能以违反其暗示的承诺的方式实现。如果在枚举时修改了集合,则实现 IEnumerable.GetEnumerator 的集合可能会开始返回随机垃圾,但 IEnumerable 的文档明确表示不应该这样做。同样,如果 IImmutableDrawnShape 的文档明确指定其属性必须是不变的。
  • 任何接口都构成了做出的承诺。它没有提到不会做出的承诺。不变性不仅仅是缺乏变异的能力;这是保证这些方法永远不会存在。而且您无法通过界面强制执行此操作。如果不禁止继承,您根本无法强制执行它。
  • 我不关心 IImmutableDrawnShape 的文档,除非我想知道它是如何工作的。我尤其是不关心它是否扩展了某个类,该类扩展了某个类,该类实现了IImmutableDrawnShape 的某些扩展。接口指定对象必须具有的行为,而不是它不得具有的行为。当然,您可以使用 IEnumerable 和 GetEnumerator 引起类似的问题,但损坏是显而易见的。当你违背了不变性的承诺时,这种违背会更加阴险。
  • 如果在枚举过程中隐含集合,是否有任何东西阻止实现 IEnumerable 的集合返回随机数据?如果一个接口的文档指定那些没有做出不可变承诺的类不能合法地实现接口,这不会阻止某人编写一个声称实现接口但行为不符合接口要求的类,但是所有类型的合同都存在同样的情况。
  • 如果你不关心接口的文档,你为什么要实现它?如果你不关心基类的文档,你为什么要扩展它?
【解决方案2】:

接口没有定义可变性/不变性;相反,它仅用于限制 消费者 可以做什么。仅仅因为一个接口没有暴露变异工具并不意味着你应该假设该对象是不可变的。例如,IEnumerable 没有说明集合的不变性,但它不是可变接口。

基本上:您需要在其他地方定义 - 可能在文档中。

如果这种拆分功能的方式对您的域有帮助,那么:去吧!如果没有帮助,请不要不必要地强迫自己(永远不要发明要求)。

另外:如果一个类型不是sealed,它就不能真正声称是不可变的,因为(基本)类型本身甚至不知道什么是可能的。同样,这可能是也可能不是真正的问题。

【讨论】:

  • 接口的声明语法并不意味着外部不变性,但这并不意味着接口的文档不能指定这些东西。请参阅我上面的编辑,以及我对 cHao 的评论。
  • @supercat 这正是我的观点;在那种情况下,它不是提供该保证的接口。可以通过任何 API 实现相同的目的。
  • 如果方法接受类型为 IImmutableFoo 的参数,则编译器可以拒绝使用任何不实现 IImmutableFoo 的类型调用该方法的任何尝试。如果唯一实现 IImmutableFoo 的类是那些承诺是不可变的,那么这允许编译时检查以确保方法只传递给承诺不可变的对象。只要编译时检查可行,我就喜欢它们,而接口似乎是强制执行此类检查的最实用方法。
  • 至于派生类型可能会做坏事,任何非密封类都是如此。如果基类的约定指定在任何正确编写的派生类中,对 GetHashCode 的每次调用都将始终返回相同的值,这不会阻止派生类以违反该约定的方式实现 GetHashCode。尽管如此,对于像 Dictionary 这样的类来说,假设 GetHashCode 返回一个不可变的值被认为是完全合理的。
【解决方案3】:

如果您想实现此模式,可以查看 WPF Freezable 类以获得灵感。

描述如下:

定义具有可修改状态和只读(冻结)状态的对象。派生自 Freezable 的类提供详细的更改通知,可以使其不可变,并且可以克隆自己。

【讨论】:

  • 我对 Freezeable 的一个问题是,除非我读错了东西,否则想要修改这样一个可能共享或可能不共享的对象的人必须制作一个防御性副本以避免其他人将其冻结在他身上(如果没有其他人确实将对象冻结在他身上,他可以假设其他人不会介意它是否发生变化)。我认为写时复制包装器会是一种更好的模式,尽管我还不能完全弄清楚如何通用地执行它的细节。见stackoverflow.com/questions/7541192/…
  • @supercat - 可能是真的,我不是建议它回答你的问题(其他人已经这样做了),只是它可能会给你一些灵感。 Freezable 是一个 WPF 类,我认为不支持多线程场景。
猜你喜欢
  • 1970-01-01
  • 2019-08-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-16
  • 1970-01-01
相关资源
最近更新 更多