工厂模式和注入依赖的生命周期困境

3
这段时间以来一直困扰着我,但我找不到正确的答案。
问题:
假设你有一个工厂接口(C#示例):
interface IFooFactory
{
    IFoo Create();
}

它的实现取决于一个服务:

class FooFactory : IFooFactory
{
    private readonly IBarService _barService;
    public FooFactory(IBarService barService)
    {
        _barService = barService;
    }
}

当服务接口实现 IDisposable 接口时,可优雅地关闭服务:

interface IBarService : IDisposable
{
    ...
}

现在由工厂创建的实际类有两个依赖项 - 服务本身(通过工厂传递),以及由工厂创建的另一个对象:

class Foo : IFoo
{
    public Foo(IBarService barService, IQux qux)
    {
        ...
    }
}

工厂可以这样创建:

class FooFactory : IFooFactory
{
    public IFoo Create()
    {
        IQux qux = new Qux();
        return new Foo(_barService, qux);
    }
}

最后,IFooIQux 都实现了 IDisposable 接口,因此类 Foo 也实现了该接口:

class Foo : IFoo
{
    public void Dispose()
    {
        _qux.Dispose();
    }
}

但是,为什么我们只在Foo.Dispose()中处理Qux?虽然两个依赖项都是注入的,但我们只依赖于工厂确切实现的知识,其中Bar是共享服务(关联关系类型),而Qux仅由Foo使用(组合关系类型)。这样很容易错误地处理它们。在这两种情况下,逻辑上Foo不拥有任何依赖项,因此处理任何一个似乎都是不对的。将Qux的创建放在Foo内部会抵消依赖注入,因此这不是一个选项。

有没有更好的方法来同时拥有这两个依赖项,并清楚地表明它们彼此之间的关系,以正确处理它们的生命周期呢?

可能的解决方案。

所以这里有一种可能的不太完美的解决方案:

class FooFactory : IFooFactory
{
    private readonly IBarService _barService;
    public FooFactory(IBarService barService)
    {
        _barService = barService;
    }
    public IFoo Create()
    {
        // This lambda can capture and use any input argument.
        // Also creation can be complex and involve IO.
        var quxFactory = () => new Qux();
        return new Foo(_barService, quxFactory);
    }
}
class Foo : IFoo
{
    public Foo(IBarService barService, Func<IQux> quxFactory)
    {
        // Injected - don't own.
        _barService = barService;
        // Foo creates - Foo owns.
        _qux = quxFactory();
    }
    public void Dispose()
    {
        // Now it's clear what Foo owns from the code in the constructor.
        _qux.Dispose();
    }
}

我不是很喜欢在构造函数中调用可能复杂的逻辑,特别是如果它是async的话,而且按需(懒加载)调用它也会导致意外的延迟运行时错误(与快速失败相比)。

为了设计而去那么远真的有意义吗?无论如何,我想看看是否有其他可能的优雅解决方案。

2个回答

4
首先是这个:
interface IBarService : IDisposable
{
    ...
}

是一个泄漏的抽象。我们只知道BarService有一个构造函数注入的可释放依赖项。这并不保证IBarService的每个实现都需要是可释放的。为了消除泄漏的抽象,IDisposable应该仅应用于实际需要它的具体实现。

interface IBarService
{
    ...
}

class BarService : IBarService, IDisposable
{
    ...
}

处理依赖注入时会采用register, resolve, release模式。另外,根据MSDN指南,创建可释放对象的一方也负责释放它。
通常情况下,使用DI容器时,DI容器既负责实例化也负责释放实例。
但是,DI模式还允许在没有DI容器的情况下连接组件。因此,为了做到百分之百的全面,我们应该考虑设计具有可释放依赖项的组件。
简而言之,这意味着:
  1. 我们需要使Foo可释放,因为它具有可释放的依赖项
  2. 由于工厂的调用者负责实例化Foo,我们需要为工厂的调用者提供一种方法来释放Foo
正如本文所指出的那样,最优雅的方法是提供一个Release()方法,允许调用者通知工厂它已经完成了对实例的使用。然后,由工厂实现决定是否显式地处理该实例,或者委托给更高级别的机制(DI容器)。
interface IFooFactory
{
     IFoo Create();
     void Release(IFoo foo);
}

class FooFactory : IFooFactory
{
    private readonly IBarService _barService;
    private readonly IQux qux;
    public FooFactory(IBarService barService, IQux qux)
    {
        _barService = barService;
        _qux = qux;
    }
    public IFoo Create()
    {
        return new Foo(_barService, _qux);
    }
    public void Release(IFoo foo)
    {
        // Handle both disposable and non-disposable IFoo implementations
        var disposable = foo as IDisposable;
        if (disposable != null)
            disposable.Dispose();
    }
}

class Foo : IFoo, IDisposable
{
    public Foo(IBarService barService, IQux quxFactory)
    {
        _barService = barService;
        _qux = quxFactory;
    }

    public void Dispose()
    {
        _barService.Dispose();
        _qux.Dispose();
    }
}

然后工厂使用模式看起来像这样:

// Caller creates the instance, the caller owns
var foo = factory.Create();
try
{
    // do something with foo
}
finally
{
    factory.Release(foo);
}
Release 方法确保无论消费应用程序是使用 DI 容器还是纯 DI 进行连接,都能正确处理可处置的内容。
请注意,是 工厂 决定是否处置 IFoo。因此,在使用 DI 容器时,可以省略实现。但是,由于多次调用 Dispose() 应该是安全的,因此我们可以将其保留在原地(并可能使用布尔值来确保在不允许的有缺陷的组件上不会调用多次处置)。

我喜欢泄漏抽象的想法,但我更想专注于Qux依赖项。我不同意上面的例子,因为我不想(1)过早地处理_barService和(2)Foo不拥有任何注入的依赖项,也不应该处理它们——这是一种导致非常危险后果的不良实践。这是我的最大关注点,上面的例子仍然存在这个问题。是的,如果DI框架创建了它,那么它应该被处理。回到Qux——它必须每次都被创建并绑定到Foo的生命周期。 - Serge Semenov

2

每次都要新建 Qux,如果可以将其交由 DI 框架处理,则无需调用 dispose,框架会在必要时进行处理。


让我澄清一下,Qux 必须每次都被创建,并且它的生命周期与 Foo 的生命周期绑定。我不知道依赖注入框架如何能帮助解决这个问题。 - Serge Semenov
在我看来,Qux 是运行时数据。运行时数据不应该由 DI 容器构造,但你也不应该使用运行时数据来构造组件,因为这会导致你现在所处的情况,从而引起工厂抽象的复杂性增加 - Steven
我感谢@Steven的反馈,但不确定那是否可行。我将探索如何将其映射到分布式服务和工作流程(这就是我提出这个SO问题的原因),因为并不存在所谓的“分布式IoC容器”。 - Serge Semenov

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