如果接口的实现调用Dispose方法,这是否是一个泄漏的抽象?

10

考虑以下代码:

public class MyClass()
{
  public MyClass()
  {    
  }

  public DoSomething()
  {
    using (var service = new CustomerCreditServiceClient())
    {
       var creditLimit = service.GetCreditLimit(
         customer.Firstname, customer.Surname, customer.DateOfBirth);       
    }
  }
}

我们现在希望对其进行重构,使其松散耦合。最终得到:

public class MyClass()
{
  private readonly ICustomerCreditService service;

  public MyClass(ICustomerCreditService service)
  {
     this.service= service;
  }

  public DoSomething()
  {
     var creditLimit = service.GetCreditLimit(
       customer.Firstname, customer.Surname, customer.DateOfBirth);       
  }
}

看起来还不错,对吧?现在任何实现都可以使用该接口,一切都很好。

如果我现在说实现是一个WCF类,并且重构之前的using语句是有原因的。即/关闭WCF连接。

那么现在我们的接口必须实现Dispose方法调用,或者我们使用一个工厂接口来获取实现并在周围放置一个using语句。

对我来说(虽然对这个主题还很新),这似乎是一个泄漏的抽象。我们不得不在代码中加入方法调用,只是为了实现处理的方式。

有人能帮我理解这个问题并确认我是对还是错。

谢谢


3
你可以让你的界面继承自Dispose。 - Thomas Levesque
无论如何,必须调用Dispose是有泄漏的,对吧? - Jon
5个回答

7
是的,当您让ICustomerCreditService实现IDisposable时,它是一个泄漏的抽象,因为您现在已经编写了特定实现的ICustomerCreditService。此外,这向接口的消费者传达了它可以处理该服务的处理,这可能不正确,特别是通常情况下,资源应由创建它的人(即具有所有权的人)处置。当您将资源注入类中(例如使用构造函数注入),不清楚消费者是否拥有所有权。

因此,通常负责创建该资源的人应该处置它。

但是,在您的情况下,您可以通过实现非可处置的ICustomerCreditServiceClient实现甚至防止发生这种情况,该实现仅在同一方法调用中创建和处置WCF客户端。这使一切变得更加容易:

public class WcfCustomerCreditServiceClient
    : ICustomerCreditServiceClient
{
    public CreditLimit GetCreditLimit(Customer customer)
    {
        using (var service = new CustomerCreditServiceClient())
        {
            return service.GetCreditLimit(customer.Firstname,
                customer.Surname, customer.DateOfBirth);       
        }
    }
}

有趣的想法。我唯一想说的是,还有另一个抽象层面,但这本身可能并不是坏事。 - Jon
@Jon:不存在新的抽象级别。您已经定义了“ICustomerCreditServiceClient”抽象。不同之处在于您的WCF代理没有实现它,但没有添加新的抽象。这样做可以防止您使IoC配置复杂化。 - Steven
@Jon:没错。但这会成为问题吗? - Steven
1
如果这是在该接口上调用的唯一函数,那么这也是我的第一个想法。然而,当不止一次调用接口,而是多次调用(在原始的 using-block 内),这可能会变得缓慢,因为每次调用都要连接和断开连接... - FrankB
1
@FrankB:没错。当性能出现问题时,您需要将资源的范围向上移动。一步上升是使用工厂(正如您在答案中所写的)。下一步是让 DI/IoC 容器处理它的释放。例如,在 Web 环境中运行时,您可以将其注册为“每个 Web 请求”生命周期,这意味着在单个 Web 请求的生命周期内仅创建一个实例。如果您没有在 Web 环境中运行,则可能需要一些作用域,就像 Dennis Traub 所解释的那样。 - Steven
显示剩余2条评论

1

您应该在实例化ICustomerCreditService的地方调用Dispose,因为MyClass现在不知道ICustomerCreditService的生命周期。


但这将使用一个IOC容器完成,因此您需要在其中指定一些内容。我认为Ninject允许您在配置依赖项实现时调用dispose。 - Jon

1

在调用代码中,您应该处理customerCreditService的生命周期。如果调用方负责清理其资源,那么MyClass如何知道服务是否仍然被调用方所需?如果调用方负责清理其资源,则MyClass不需要是可处理的。

// calling method

using (var service = new CustomerCreditServiceClient()) {
    var myClass = new MyClass(service);
    myClass.DoSomething();
}

更新:在评论中,OP提到了使用像Ninject这样的IoC。那么代码可能看起来像这样:

IKernel kernel = ...;

using (var block = kernel.BeginBlock())
{
    var service = block.Get<ICustomerCreditService>();
    var myClass = new MyClass(service);
    myClass.DoSomething();
}

kernel.BeginBlock() 创建一个激活块。它确保在块结束时已处理解析的实例。


但是这将全部使用IOC容器完成,因此您需要在其中指定一些内容。我认为Ninject允许您在配置时调用依赖项实现的dispose。 - Jon
然后,您可以让IoC容器解析和处理CustomerCreditServiceClient。在MyClass中仍然不需要实现IDisposable.Dispose() - Dennis Traub
你不应该那样传递内核。 - Jon
这只是一个简化的示例。你一开始从未提到过IoC容器,我只是根据你的评论尝试扩展我的答案。如果使用容器如此重要,请更新你的问题。另一方面:使用IoC容器本身就被视为反模式。 - Dennis Traub

1
重新开始第一次实现时,我会尝试向类添加一个getInterface-Request,以便实现可以保持更或多或少相同。然后它可以安全地调用Dispose(实际上只是推迟了接口实现的创建,但仍然控制其生命周期):(C#代码未经验证...)
public class MyClass()
{
  public delegate ICustomerCreditService InterfaceGetter;
  private InterfceGetter getInterface;
  public MyClass(InterfaceGetter iget)
  {
    getInterface = iget;
  }
  public DoSomething()
  {
    using (var customerCreditService = getInterface())
    {
       var creditLimit = customerCreditService.GetCreditLimit(customer.Firstname, customer.Surname, customer.DateOfBirth);       
    }
  }
}

1
我觉得你的答案有点含糊,但你的基本建议是注入一个工厂来创建该实例(对此给予加分)。通常情况下,工厂是很好的选择,因为它清楚地表明调用者拥有所创建实例的所有权,并且需要负责释放该实例。不过,在这种情况下,你仍然需要在 ICustomerCreditService 上实现 IDisposable 接口,尽管这并不是必需的。 - Steven

0

是的,它确实如此。但这是必要的恶。 IDisposable 接口的存在本身就是一个泄漏的抽象。泄漏的抽象在编程中只是一个日常事实。尽可能避免它们,但当你无法避免时不要担心——它们无处不在。


@FrankB 嗯...你能详细说明一下吗?毕竟,这不是一个可以同意或不同意的观点,而是一个事实 - 事实不能被反驳,只能对它们进行评估。如果您认为我在事实或其评估方面犯了错误,请告诉我,我会进行更正。 - Konrad Rudolph
@Steven 同样适用于你。请看前面的评论。顺便说一下,我刚刚看到了你的答案,这确实是一个处理这个特定情况的不泄漏的方法。尽管如此,我的答案更为通用。 - Konrad Rudolph
@KonradRudolph,IDisposable的存在正是为了避免内存泄漏。如果你的意思是使用不当会导致问题,那么是的,这很糟糕。但这并不是IDisposable的错...它是一种清晰的方式来控制未受管理的内容。顺便说一下:您没有详细说明什么会导致内存泄漏? - FrankB
@KonradRudolph 好的,我明白了。然而,我仍然不能完全同意你的总体陈述...(但这不是这个问题的主题)。 - FrankB
IDisposable本身并不是一个泄漏的抽象。 IDisposable是一个明确的契约,它传达了实现它的类型具有可以被确定性清理的资源。然而,当您将IDisposable应用于另一个接口时,该接口可能会成为一个泄漏的抽象。原因是无法预见接口的所有可能实现,因此您始终可以想出几乎任何接口的可处理实现。 - Steven
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接