如何考虑不同的实现需求
本文关键字:实现需求 何考虑 | 更新日期: 2023-09-27 18:13:28
假设我有一个包含两个具体类的接口。具体需要实现IDisposable
。为了一个类的利益,应该修改接口以实现IDisposable
,还是接口的使用者必须执行可处置性的运行时检查?
我认为应该修改接口,因为它是一个简单的更改(特别是如果它是一个新接口),但我也可以看到在更改设计以适应特定实现时可能违反liskov(特别是如果其他类或类必须抛出不支持的异常)
如果框架本身有任何指示,那么让接口实现IDisposable
的适当性取决于可处置性是否是履行接口定义的契约的必要属性。少数框架接口确实实现了IDisposable
,包括:
System.Collections.Generic.IEnumerator<T>
System.Deployment.Internal.Isolation.Store
System.Resources.IResourceReader
System.Resources.IResourceWriter
System.Security.Cryptography.ICryptoTransform
System.ComponentModel.IComponent
System.ComponentModel.IContainer
从本质上讲,这些接口通常定义了消耗资源的构造,因此需要释放资源。从这个意义上说,资源的处理可以被认为是实现契约的一个组成部分,而不是实现接口的具体类的实现细节。例如,IResourceReader
将从资源中读取,并且关闭资源是实现契约的必要部分。
相反,它在框架中非常常见,具体类直接实现IDisposable
(而不是通过另一个接口)。对于框架类,这可以通过反射来查询:
foreach (var v in typeof(/*any type*/)
.Assembly.GetTypes()
.Where(a => a.IsClass
&& typeof(IDisposable).IsAssignableFrom(a)
&& a.GetInterfaces().Where(
i=>i!=typeof(IDisposable)
).All(i=>!typeof(IDisposable).IsAssignableFrom(i))))
{
foreach (var s in v.GetInterfaces())
Console.WriteLine(v.FullName + ":" + s.Name);
}
一般来说,这些类的实现需要消耗资源,这是实现接口契约的附带条件。例如,System.Data.SqlClient.SqlDataAdapter
分别实现了IDbDataAdapter
和IDisposable
;IDbDataAdapter
完全有可能不需要处置,但是SqlDataAdapter
的实现需要消耗和释放资源。
在您的例子中,您指出有两个类实现您的接口,一个需要实现IDisposable
,另一个不需要。如果不这样做,根据定义,处理资源的能力不是满足接口需求的必要条件;因此,接口本身不应该实现IDisposable
。
Dispose()
不应该抛出异常(参见CA1065:不要在意外的位置引发异常)。如果实现IDisposable
的类实例没有资源可以释放,它可以简单地返回;满足释放所有资源的后置条件。没有必要抛出NotSupportedException
.
附录
第二个潜在的考虑因素是接口的预期使用。例如,在数据库场景中通常使用以下模式:
System.Data.IDbCommand cmd = ...;
using (var rdr = cmd.ExecuteReader()) // returns IDataReader (IDisposable)
{
while (rdr.Read()) {...}
} // dispose
如果IDataReader
不实现IDisposable
,等效代码将需要明显更复杂:
System.Data.IDbCommand cmd = ...;
System.Data.IDataReader rdr;
try
{
rdr = cmd.ExecuteReader();
while (rdr.Read()) {...};
} finally {
if (rdr is IDisposable) ((IDisposable)rdr).Dispose();
}
IDisposable
,也可以将接口IDisposable
作为特殊情况。我在阅读Mark Seemann关于依赖注入的书时找到了答案。接口上的IDisposable自动是一个有泄漏的抽象,因为IDisposable只是一个实现细节。也就是说,并不是所有的接口都是抽象的,因此——以严格按照接口编程的名义——会出现接口必须实现IDisposable的情况。
虽然具体实现比接口更可取,但在这两种情况下,解决方案都是在资源上创建粗粒度抽象。然后,抽象的每个方法实现将创建和处置资源,从而减轻了使用者做同样事情的负担。我喜欢这种方法,因为它降低了使用者生命周期管理的复杂性(实际上应该没有,特别是在DI中)。
为了在上述场景中实现DI,您可能需要注入一个工厂,允许每个方法临时实例化依赖项。