转移IDisposable对象的所有权和构建器设计模式

3

我习惯于这种方式:当一个对象创建另一个对象时(直接或通过工厂、生成器等),它就“拥有”该对象,从而管理其生命周期。

这个思路在几乎所有情况下都很好用。

但有时候,创建对象的那个对象根本无法管理它,例如在生成器设计模式实现中:

IFoo BuildFoo()
{
    var dep = new Dep();

    return new Foo(dep);
}

因此,在这里构建器无法管理dep对象的生命周期,因为:

  1. 它没有对它的引用;
  2. 它不知道什么时候可以安全地Dispose它。

天真的解决方案是使Foo:IDisposable,并让它管理传递给它构造函数的Dep

但是另一个困境出现了:

using (var dep = new Dep())
{
    using (var foo = new Foo(dep))
    {
        // (1) do something with foo
    }

    // (2) !!! do something with dep !!!
}

上面的代码变得不安全:在点(2)处使用dep是不安全的,因为它已经被foo Dispose了。
并且没有任何语法可以表示谁负责管理对象的生命周期。
那么问题来了:有什么通用解决方案吗?
3个回答

1
在这种情况下,我不会担心它。我想我会被大家抨击,但是“构建器”、“工厂”等都是可以创建对象并将其处理交给请求的事物处理的地方。
然而,有一个规则,即构建器/工厂必须 创建对象,而不能对其进行任何操作。
当然,如果您正在使用new关键字来新建对象,则即使是间接地通过工厂类,也会将自己与该实现耦合起来。根据所创建对象的用途,您可能需要考虑依赖注入,此时DI容器将在正确的时间基于其配置的生命周期为您创建对象并处置它们。

没错,但请看代码片段#2。当一个对象在那些创建模式之外被创建(这是完全合法的),那么就没有任何约定能提示我们,在传递给它的构造函数中,我们正在转移所有权。 - zerkms
不要这样做。保持一致性。你只应该为那些本身难以创建的对象(需要复杂的初始化等)创建工厂。如果是这样,那么就创建工厂并使用它。保持一致性非常重要。 - Neil Barnwell
构建器有助于构建一个复杂的对象(通常带有一些运行时条件),但它的目的纯粹是辅助性的。这与专门设计封装对象构造的工厂略有矛盾。因此,我认为直接使用构建器和构造函数并不矛盾。 - zerkms
这不是我想说的。我的意思是对于任何足够复杂需要使用构建器的对象,都应该使用构建器来创建它们。无论如何,接收对象的代码都要负责其可处理性。将构建器和工厂视为 new 关键字 - 它们只是隐藏了您必须传递的所有依赖项。我还要再次提到 DI,因为我认为如果您在这个领域,这是一个有效的考虑。 - Neil Barnwell
因为我认为如果你在这个领域,这是一个有效的考虑。实际上这不是一个实际问题,而更多是关于研究有哪些习语/工具来表示所有权转移。例如:在Rust语言中,它是显式的,所以我的目的是找出在.NET/C#中是否有任何惯例。 - zerkms
1
很好。这个习语是“如果你创建了一个对象,你就要负责处理它”。在实践中,“创建一个对象”包括使用构建器或工厂,这本质上与new语句几乎相同。DI容器只是更进一步。 - Neil Barnwell

1
非常幼稚的实现。
void Main()
{
    using (var builder = new Builder())
    {
        var foo = builder.Create();
        // do smtg
    }
}

public class Foo
{
    public Foo(Dep instance)
    {
    }
}

public class Dep : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine($"{this.GetType().Name} disposed!");
    }
}

public class Builder : IDisposable
{
    private List<IDisposable> _disposables = new List<System.IDisposable>();

    public Foo Create()
    {
        var dep = new Dep();
        _disposables.Add(dep);

        var foo = new Foo(dep);
        return foo;
    }

    public void Dispose()
    {
        foreach(var d in _disposables)
            d.Dispose();

        Console.WriteLine($"{this.GetType().Name} disposed!");
    }
}

如果某个东西拥有IDisposable,那么它应该实现IDisposable。 当然,这只是一个很简单的实现示例。

是的。我多次阅读过这方面的内容,但现在只记得《框架设计指南》这本书。我认为这本书对你会很有趣。 - tym32167
你应该问,谁拥有Dep实例?如果是Builder,则应该实现IDisposable接口。如果只有Foo,则Foo应该实现它。例如,如果你正在使用StreamWriter - 它将Stream作为参数传递,然后负责关闭/释放流。 - tym32167
这正是我非常想问的问题:哪种写法更为地道。因为我可以想到一些情况,Dep既可以由Foo拥有,也可以由外部作用域拥有。 - zerkms
1
如果你确切地在询问建造者模式,不确定是否有答案适用于你。释放行为应该是可配置的建造者。例如,如果您正在使用 DI 容器,则可以选择哪些实例将受容器控制,哪些实例将由外部控制。 - tym32167
1
这就是我的问题所在。并没有确定的答案,因为它取决于项目中的情况。Dep 是什么意思?如果 Dep 类似于流,则应由 Foo 处置,因为您可能不希望使用它两次。但是,如果它类似于 EventAggregator 或 DbTransaction,则不应由 Foo 处置。 - tym32167
显示剩余3条评论

0

你可以让 Foo 调用构造器来实例化 Dep 并在需要时使用它。这样,Foo 就可以管理 Dep 的生命周期,而不必担心 Foo 的客户端。


那么Foo将会了解这个构建器。控制反转的整个理念在于你需要获得相关依赖。这样一来,整个过程变得更难测试。 - zerkms
那么,你为什么假设Foo的客户端需要管理Dep的生命周期呢?为什么不让Foo在自己被处理时同时处理Dep呢? - Timothy Ghanem
因为没有明确的契约规定,在 Foo 处理完后,其依赖项也处于无效状态。 - zerkms
1
那么,让构建器管理Dep的生命周期,参见@tym32167的回答。 - Timothy Ghanem

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