如何考虑不同的实现需求

本文关键字:实现需求 何考虑 | 更新日期: 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分别实现了IDbDataAdapterIDisposable;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,您可能需要注入一个工厂,允许每个方法临时实例化依赖项。