.NET中使用了一种相当常见的模式来测试类的功能。这里我将使用Stream类作为示例,但该问题适用于使用此模式的所有类。
模式是提供一个名为CanXXX的布尔属性,以指示该类上的功能XXX可用。例如,Stream类具有CanRead,CanWrite和CanSeek属性,以指示可以调用Read,Write和Seek方法。如果属性值为false,则调用相应的方法将导致抛出NotSupportedException。
来自流类的MSDN文档:
根据基础数据源或存储库,流可能仅支持其中一些功能。应用程序可以使用CanRead,CanWrite和CanSeek属性查询流的功能。
CanRead属性的文档:
在派生类中重写时,获取一个值,指示当前流是否支持读取。
如果从Stream派生的类不支持读取,则对Read,ReadByte和BeginRead方法的调用会抛出NotSupportedException。
我看到很多代码都是按照以下方式编写的:
if (stream.CanRead)
{
stream.Read(…)
}
请注意,没有同步代码,例如,以任何方式锁定流对象 - 其他线程可能正在访问它或它引用的对象。也没有捕获NotSupportedException的代码。
MSDN文档未声明属性值不能随时间更改。实际上,当流关闭时,CanSeek属性会更改为false,从而演示这些属性的动态特性。因此,没有合同保证在上面的代码片段中调用Read()不会抛出NotSupportedException。
我希望有很多代码可以解决这个潜在的问题。我想知道那些发现这个问题的人是如何解决它的。这里适合哪些设计模式?
我也很感谢对这种模式的有效性的评论(CanXXX,XXX()对)。对我来说,至少在Stream类的情况下,这代表了一个试图做太多的类/接口,应该分成更基本的部分。缺乏紧密的,有文件记录的合同使得测试变得不可能,并且实施起来更加困难!
答案 0 :(得分:4)
好的,这是另一次尝试,希望比我的其他答案更有用......
令人遗憾的是,MSDN没有对CanRead
/ CanWrite
/ CanSeek
如何随时间变化提供任何具体保证。我认为假设如果一个流是可读的,它将继续可读,直到它被关闭是合理的 - 对于其他属性也是如此
在某些情况下,我认为以后成为可以查找的流是合理的 - 例如,它可能会缓冲它读取的所有内容,直到它到达底层数据的末尾,然后允许搜索之后让客户重读数据。我认为适配器忽略这种可能性是合理的。
这应该照顾除了最病态的病例。 (Streams几乎被设计为造成严重破坏!)将这些要求添加到现有文档中是理论上的重大变化,即使我怀疑99.9%的实现已经遵守它。不过,可能值得在Connect上提出建议。
现在,至于是否使用“基于功能的”API(如Stream
)和基于接口的API之间的讨论......我看到的根本问题是.NET不提供指定变量必须是对多个接口的实现的引用的能力。例如,我不能写:
public static Foo ReadFoo(IReadable & ISeekable stream)
{
}
如果 允许这样做,那么它可能是合理的 - 但如果不是这样,你最终会出现大量潜在接口:
IReadable
IWritable
ISeekable
IReadWritable
IReadSeekable
IWriteSeekable
IReadWriteSeekable
我认为这比目前的情况更糟糕 - 虽然我认为除了现有的IReadable
之外我 支持IWritable
和Stream
的想法类。这将使客户更容易以声明方式表达他们所需要的内容。
使用Code Contracts,API 可以宣布他们提供的内容及其所需内容,诚然:
public Stream OpenForReading(string name)
{
Contract.Ensures(Contract.Result<Stream>().CanRead);
...
}
public void ReadFrom(Stream stream)
{
Contract.Requires(stream.CanRead);
...
}
我不知道静态检查程序可以提供多少帮助 - 或者它如何处理当流做在关闭时变得不可读/不可写的事实。
答案 1 :(得分:3)
在不知道对象内部的情况下,您必须假设“标志”属性太易于在多个线程中修改对象时无法依赖。
我已经看到这个问题更常见于只读集合而不是流,但我觉得这是相同设计模式的另一个例子,并且适用相同的参数。
为了澄清,.NET中的ICollection接口具有属性IsReadOnly,该属性旨在用作指示集合是否支持修改其内容的方法。就像流一样,此属性可以随时更改,并将导致抛出InvalidOperationException或NotSupportedException。
围绕这个问题的讨论通常归结为:
模式很少是好事,因为你被迫处理不止一套“行为”;有一些可以随时切换模式的东西要糟糕得多,因为你的应用程序现在也必须处理多个“一组”行为。然而,仅仅因为它可能会将某些内容分解为更加谨慎的功能并不一定意味着你总是应该这样做,特别是当它分开时无助于降低手头任务的复杂性。
我个人认为,您必须选择最接近您认为班级消费者会理解的心理模型的模式。如果您是唯一的消费者,请选择您最喜欢的型号。在Stream和ICollection的情况下,我认为对这些进行单一定义更接近于在类似系统中多年开发建立的心理模型。当你谈论流时,你谈论文件流和内存流,而不是它们是可读还是可写。同样,当你谈论集合时,你很少用“可写性”来引用它们。
我对此的经验法则:总是寻找一种方法将行为分解为更具体的界面,而不是拥有“模式”的操作,只要它与简单的心理模型相称。如果很难将单独的行为视为单独的事物,请使用基于模式的模式并清楚地记录非常。
答案 2 :(得分:1)
stream.CanRead只是检查底层流是否有可能读取。它没有说明是否可以实际读取(例如磁盘错误)。
如果您使用任何* Reader类,则无需捕获NotImplementedException,因为它们都支持读取。只有* Writer会有CanRead = False并抛出该异常。如果您知道流支持读取(例如您使用StreamReader),则恕我直言,无需进行额外检查。
您仍然需要捕获异常,因为读取期间的任何错误都会抛出异常(例如磁盘错误)。
另请注意,任何未记录为线程安全的代码都不是线程安全的。通常静态成员是线程安全的,但实例成员不是 - 但是,需要检查每个类的文档。
答案 3 :(得分:1)
从你的问题和随后的所有评论中,我猜你的问题在于所述合同的清晰度和“正确性”。声明的合同是MSDN在线文档中的内容。
你所指出的是文档中缺少一些东西,迫使人们对合同做出假设。更具体地说,因为没有任何关于流的可读性属性的波动性的说法,所以可以做出的唯一假设是,NotSupportedException
被抛出是可能的,无论如何相应的CanRead属性的值是几毫秒(或更多)之前的值。
我认为在这种情况下需要继续使用此接口的 intent ,即:
CanRead
的值是不变的。尽管如此,Read *方法可能可能会抛出NotSupportedException
。
相同的参数可以应用于所有其他Can *属性。
答案 4 :(得分:1)
我也很欣赏有关此模式(CanXXX,XXX()对)的有效性的评论。
当我看到这种模式的一个实例时,我通常会期待这个:
无参数CanXXX
成员将始终返回相同的值,除非......
...存在 CanXXXChanged
事件,其中无参数CanXXX
可能会在该事件发生之前和之后返回不同的值;但如果没有触发事件,它就不会改变。
参数化CanXXX(…)
成员可能会为不同的参数返回不同的值;但对于相同的参数,它可能会返回相同的值。也就是说,CanXXX(constValue)
可能会保持不变。
我在这里要谨慎:如果
stream.CanWriteToDisk(largeConstObject)
现在返回true
,假设它将来总会返回true
是否合理?可能不是,所以也许取决于上下文参数化CanXXX(…)
是否会为相同的参数返回相同的值。
仅当XXX(…)
返回CanXXX
时才能成功调用 true
。
话虽如此,我同意Stream
使用这种模式有些问题。至少在理论上,如果在实践中可能没那么多。
答案 5 :(得分:0)
这听起来更像是一个理论问题,而不是一个实际问题。我真的不能想到任何情况下流会变得不可读/不可写其他而不是因为它被关闭了。
可能存在极端情况,但我不希望它们经常出现。我不认为绝大多数代码都需要担心这一点。
但这是一个有趣的哲学问题。
编辑:解决CanRead等是否有用的问题,我相信它们仍然存在 - 主要用于参数验证。例如,仅仅因为一个方法需要一个它在某个时候想要读取的流并不意味着它想要在方法的开头就读它,但这就是理想情况下应该执行参数验证的地方。这与检查参数是否为空并且抛出ArgumentNullException
而不是在您第一次取消引用它时等待NullReferenceException
被抛出时没有什么不同。
此外,CanSeek
略有不同:在某些情况下,您的代码可以很好地处理可搜索和不可搜索的流,但在可搜索的情况下效率更高。
这确实依赖于“可寻找性”等保持一致 - 但正如我所说,这在现实生活中似乎是真的。
好的,让我们尝试换一种方式......
除非你在内存中阅读/寻找并且你已经确定有足够的数据,或者你正在预先分配的缓冲区内写字,否则始终可能会出错。磁盘发生故障或填满,网络崩溃等。这些事情做在现实生活中发生,所以你总是需要以一种能够在失败中存活的方式进行编码(或者有意识地选择忽略问题而不是真的很重要。)
如果您的代码在磁盘发生故障的情况下可以做正确的事情,那么它很可能在FileStream
从可写转为不可写的情况下继续存在。
如果Stream
确实有合同,那么他们必须非常弱 - 您无法使用静态检查来证明您的代码始终有效。你能做的最好的事情就是证明它在失败时做了正确的事。
我不相信Stream
会很快改变。虽然我当然接受它可以更好地记录,但我不接受它被“彻底打破”的想法。如果我们实际上无法在现实生活中使用它,它会被更多打破......如果它可能比现在更加破碎,那么它在逻辑上不会完全打破
我对框架有更大的问题,例如相对较差的日期/时间API状态。在过去的几个版本中,它们已经变得更好了很多,但它们仍然缺少(例如)Joda Time的许多功能。缺乏内置的不可变集合,对语言不变性的支持不足等 - 这些都是导致我实际头痛的真正问题。我宁愿看到他们在Stream
花费多年时间来解决这些问题,这在我看来是一个有点棘手的理论问题,在现实生活中几乎没有问题。