ReadOnlyCollection 类是不良设计的一个很好的例子吗? [关闭]
Posted
技术标签:
【中文标题】ReadOnlyCollection 类是不良设计的一个很好的例子吗? [关闭]【英文标题】:Is the ReadOnlyCollection class a good example of Bad Design? [closed] 【发布时间】:2011-04-23 03:15:05 【问题描述】:看ReadOnlyCollection类的规范,它确实实现了IList接口,对吧。
IList 接口有 Add/Update/Read 方法,我们称之为接口的前置条件。如果我有 IList,我应该能够在我的应用程序中的任何地方执行所有此类操作。
但是,如果我在代码中的某处返回 ReadOnlyCollection 并尝试调用 .Add(...) 方法呢?它抛出一个 NotSupportedException。你认为这是一个糟糕设计的好例子吗?此外,这个类是否违反了Liskov Substitution Principle?
为什么微软采用这种方式?让这个 ReadOnlyCollection 只实现 IEnumerable 接口(顺便说一句,它已经是只读的)是否更容易(更好)?
【问题讨论】:
+1,当然微软这样做是有原因的,而不是使用单独的界面——毕竟,他们是聪明人。我很想知道他们为什么这样做。 【参考方案1】:IList 有一些读取方法和属性,例如 Item 和 IndexOf(..)。如果 ReadOnlyCollection 只实现 IEnumerable 那么你就会错过这些。
还有什么选择?拥有 IList 的只读版本和写入版本?这会使整个 BCL 复杂化(更不用说 LINQ)。
我也不认为它违反了 Liskov 替换原则,因为它是在(IList 的)基础级别定义的,它可以抛出不受支持的异常。
【讨论】:
是的,但这并不与ReadOnlyCollection
实现IList
的糟糕设计的观察结果相矛盾。【参考方案2】:
是的,这确实是糟糕的设计。 .NET 中缺少集合接口:没有只读接口。
您是否知道string[]
实现了IList<string>
(其他类型也是如此)?这有同样的问题:你希望你可以在接口上调用Add
和Remove
,但它会抛出。
不幸的是,如果不破坏向后兼容性,就无法再更改它,但我同意你的观点,这是非常糟糕的设计。更好的设计应该为只读功能提供单独的接口。
【讨论】:
在调用这些方法之前,您可以检查 IsReadOnly 属性。当 index 为负数或大于或等于 Count 属性时,对于同一接口的 Insert 方法抛出 ArgumentOutOfRangeException 也是一个糟糕的设计决定吗?因为,根据你的论证,它确实是。 编译时你不知道你的 IList 变量在哪里是只读的或不是 List。 @Timwi:是吗?他们都有一种方法来检查它是否会引发异常,它们都被记录为被抛出(当它们被抛出时有条件)并且它们都在同一个界面中。有什么区别? @Ivan:一个区别是它有意义并且相当实用具有只读接口并保留可写接口以实际可写的实例。另一个区别是客户端代码在使用索引器之前检查数组边界是相当实用,但必须调用Add
只是为了检查它是否抛出是完全荒谬的,只会导致错误的代码。 (IsReadOnly
属性不可靠:((IList)new byte[0]).IsReadOnly
返回false
,但((IList)new byte[0]).Add((byte)0)
仍然抛出!)
@Ivan:一般来说,你想不出有什么区别,并不意味着没有区别,特别是,这并不意味着某人的论证以某种方式“导致得出结论”。你应该更加小心地对别人所说和暗示的内容发表声明。【参考方案3】:
我认为这是在抽象和专业化之间进行权衡的一个很好的例子。
你想要 IList 的灵活性,但你也想施加一些约束,那你怎么办?它的设计方式有点尴尬,并且可能在技术上违反了一些设计原则,但我不确定什么会更好,并且仍然为您提供相同的功能和简单性。
在这种情况下,最好有一个单独的 IListReadOnly 接口。但是,一旦使用界面扩散地,很容易走上疯狂的道路,让事情变得非常混乱。
【讨论】:
我同意为每个功能(IAddableCollection
、IRemovableCollection
、IIndexableCollection
、...)提供单独的接口是不好的设计,但我会认为 readonlyness 是集合类型的基本属性,足以保证分离。 (此外,它将启用协方差!)
@Timwi:也许是这样。听到有关集合接口的设计决策会很有趣。
我不会说你提到的路径无论如何都是“疯狂的”。一个设计良好的框架会暴露NSArray
以及NSMutableArray
、NSDictionary
和NSMutableDictionary
、NSSet
和NSMutableSet
等...
@Remus:我并不是说公开可变和不可变版本的东西是疯狂的,在这种情况下可能会更好。我是说你可以通过制作专门的接口来做的太过分了,结果很混乱。【参考方案4】:
这不是一个伟大的设计,但在我看来是必要的邪恶。
不幸的是,微软没有在框架中包含 IReadableList<>
和 IWriteableList<>
之类的东西,而是让IList<>
自己实现了这两者(或者甚至完全跳过IList<>
,让IWriteableList<>
实现IReadableList<>
)。问题解决了。
但现在改变为时已晚,如果您遇到需要集合具有列表语义的情况,并且您希望在运行时抛出异常而不是允许突变,那么很遗憾,ReadOnlyCollection<>
是,您的最佳选择。
【讨论】:
+1 。这是个好建议,我很喜欢。 Objective-C 解决方案很好(除了它没有泛型/模板)。在那里,您有 ArrayList(不可变)和 MutableArrayList(可变)。这样一来,您会本能地使用不可变的变体,并主动决定何时真正需要可变的版本,而这种情况通常很少见。 @RickyHelgesson:ArrayList
是不可变的,还是只读的?我认为正确的方法是拥有一个只读接口,以及从它派生的可变和不可变接口。期望仅通过保存引用来封装集合内容的代码可以采用不可变接口类型的参数,而现在需要读取某些内容并且不关心以后可能(或可能不会)更改的代码可以接受可读的界面。
@supercat 我从没想过这种模式,但我喜欢它。我唯一担心的是普通用户可能不理解只读接口和不可变接口之间的重要区别,因为它们看起来相同。我已经写了一篇关于可变/不可变模型数据的文章 btw rickyhelgesson.wordpress.com/2012/07/17/… 如果我能把它足够简单的话,我可能会考虑到你的模式,对我的模型架构进行另一次传递。
@supercat 回复:Objective-C,我有一段时间没用过,但如果我没记错的话,ArrayList 是完全不可变的。【参考方案5】:
虽然IList<T>
接口定义了Add(T)
和Insert(int,T)
方法,它也定义了IsReadOnly
属性,如果你仔细阅读MSDN上IList.Insert(int,T)和IList.Add(T)方法的定义,你可以看到它们都指定如果列表是只读的,那么这些方法可能会抛出 NotSupportedException
。
因为这个原因说它是糟糕的设计就像说它也是糟糕的设计,因为Insert(int, T)
可以在索引为负数或大于集合大小时抛出ArgumentOutOfRangeException
。
【讨论】:
【参考方案6】:我认为如果有一个不好的设计,这是一种在不检查 ReadOnly 属性的情况下添加到 IList 的习惯。程序员忽略接口部分的习惯并不意味着接口很差。
事实上,我们程序员中很少有人会费心去阅读规范。老实说,有很多事情对我来说似乎比坐下来阅读整个规范文档更令人兴奋。 (例如,看看一个人是否真的可以用牙签睁开眼睛。)此外,我有一个限制,我无论如何都不会记住所有内容。
话虽如此,不应该在不查看属性和方法列表的情况下使用接口。您认为名为“ReadOnly”的布尔属性的用途是什么?也许是因为只能出于某种原因阅读该列表。如果您从您自己的代码之外的某个地方传递一个列表,您应该在尝试添加之前检查该列表不是只读的。
【讨论】:
我不同意。界面很差,因为它同时包含 Add 和 ReadOnly。一个好的设计是 IReadOnlyList,ReadOnlyCollection 将实现它并让 IList 从 IReadOnlyList 继承。不再需要 ReadOnly bodge 属性,任何实现 IList 的东西都将支持 Add 等。 我不同意。您希望编译器帮助您发现类和接口的错误用法。 ReadOnlyCollection 并非如此。【参考方案7】:我会说这是一个糟糕的设计。即使人们接受具有能力查询的中等大接口的概念,也应该有其他接口继承自它们,以保证允许某些行为。例如:
-
IEnumerable(很像现有的,但没有重置,并且不承诺如果在枚举期间更改集合会发生什么)
IMultipassEnumerable(添加重置,并保证不断变化的集合的重复枚举将返回相同的数据或抛出异常)
ICountableEnumerable(一个多遍枚举,加上一个“计数”属性,以及一个同时获取枚举数和计数的方法)
IModifiableEnumerable(一个 IEnumerator,如果在枚举期间由执行枚举的线程修改了集合,则不会抛出该 IEnumerator。不会指定精确的行为,但枚举期间未更改的项目必须仅返回一次;那些已修改的项目在枚举过程中,每次添加或修改最多只能返回一次,如果它们在枚举开始时存在,则加一个。此接口本身不提供任何突变,但将与其他提供突变的接口一起使用)。
ICopyableAsEnumerable(包括计数属性和返回表示列表快照的 IEnumerable 的方法;实际上不是 IEnumerable 本身,而是 IEnumerable 提供的有用功能)。
IImmutable(无成员,但可继承以创建保证不可变接口)
IImmutableEnumerable
IImmutableCountableEnumerable
IList(可以是可读、读写或不可变的)
IImmutableList(没有新成员,但继承了 IImmutable)
IWritableList(没有新成员,但保证可写)
这只是一个小样本,但应该传达设计理念。
【讨论】:
【参考方案8】:IMO 的设计很糟糕。可能是由于向后兼容性问题、缺少协方差和逆变等等问题。现在幸运的是,他们在 .NET 4.5 中解决了这个问题:
IReadOnlyList<out T>
IReadOnlyCollection<out T>
IReadOnlyDictionary<TKey, TValue>
但是我缺少带有“bool Contains(T)”的“只读”界面。
【讨论】:
如果您需要该功能,请添加扩展方法。您可以使用接口的其他成员轻松实现它。以上是关于ReadOnlyCollection 类是不良设计的一个很好的例子吗? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章
ReadOnlyCollection 或 IEnumerable 用于公开成员集合?
更新 GridView 中的 ReadOnlyCollection
readOnlycollection 的 .Item 属性不起作用
为啥 List.AsReadOnly 返回 ReadOnlyCollection 而 Dictionary.AsReadOnly 返回 IReadOnlyDictionary?