8

我将其标记为 C# 和 Java,因为这两种语言都是同一个问题。

说我有这些课程

interface IKernel
{
    // Useful members, e.g. AvailableMemory, TotalMemory, etc.
}

class Kernel : IKernel
{
    private /*readonly*/ FileManager fileManager;  // Every kernel has 1 file manager
    public Kernel() { this.fileManager = new FileManager(this); /* etc. */ }

    // implements the interface; members are overridable
}

class FileManager
{
    private /*readonly*/ IKernel kernel;  // Every file manager belongs to 1 kernel
    public FileManager(IKernel kernel) { this.kernel = kernel; /* etc. */ }
}

这种设计的问题在于,一旦FileManager尝试在其构造函数中做任何事情kernel(它可能合理地需要这样做),它将调用尚未调用构造函数的潜在子类实例上的虚拟方法。

在可以定义真正的构造函数(而不是初始化程序,如 C#/Java)的语言中,不会出现此问题,因为在调用其构造函数之前,子类甚至不存在……但在这里,这个问题发生了。

那么什么是最好/正确的设计/实践,以确保不会发生这种情况?

编辑:

我不一定说我需要循环引用,但事实是两者都Kernel相互依赖FileManager。如果您有关于如何在使用循环引用的情况下缓解此问题的建议,那也很棒!

4

4 回答 4

5

对我来说,这种对象之间的循环依赖很难闻。

我认为您应该决定哪个对象是主要对象,哪个对象是聚合甚至组合的主题。然后在主要对象内部构造次要对象,或者将其作为主要对象的依赖项注入。然后让主对象在辅助对象中注册它的回调方法,它会在需要与“外部世界”通信时调用它们。

如果您决定关系类型是聚合,那么一旦主对象被销毁,它将取消注册所有回调。

如果你使用组合,那么在主要对象被破坏时只破坏次要对象。

这是我的意思的一个例子:

class Program
{
    static void Main( )
    {
        FileManager fm = new FileManager( );
        Kernel k = new Kernel( fm );
        fm.DoSomething( 10 );
    }
}

class Kernel
{
    private readonly FileManager fileManager;
    public Kernel( FileManager fileManager )
    {
        this.fileManager = fileManager;
        this.fileManager.OnDoSomething += OnFileManagerDidSomething;
    }

    ~Kernel()
    {
        this.fileManager.OnDoSomething -= OnFileManagerDidSomething;
    }

    protected virtual void OnFileManagerDidSomething( int i )
    {
        Console.WriteLine( i );
    }
}

class FileManager
{
    public event Action<int> OnDoSomething;

    public void DoSomething( int i )
    {
        // ...

        OnDoSomething.Invoke( i );
    }
}
于 2012-05-18T20:33:48.113 回答
2

就个人而言,我不喜欢循环引用。但是如果你决定离开它们,你可能会增加一些懒惰:

interface IKernel
{
    // Useful members, e.g. AvailableMemory, TotalMemory, etc.
}

class Kernel : IKernel
{
    private readonly Lazy<FileManager> fileManager;  // Every kernel has 1 file manager
    public Kernel() { this.fileManager = new Lazy<FileManager>(() => new FileManager(this)); /* etc. */ }

    // implements the interface; members are overridable
}

class FileManager
{
    private /*readonly*/ IKernel kernel;  // Every file manager belongs to 1 kernel
    public FileManager(IKernel kernel) { this.kernel = kernel; /* etc. */ }
}  

这里的懒惰让我们确保当查询 FileManager 实例时,IKernel 实现将被完全初始化。

于 2012-05-18T20:41:30.590 回答
2

If you need to keep pairs of objects, with references to each other, you should provide a utility to build them correctly. Use the Factory pattern, and reduce the complexity of construction by hiding the construction and assembly behind the Factory pattern methods.

In Java, put the constructor in the package, and make the internal component's constructors and initial assignment set up methods "package private"

public Kernel newKernel() {
  Kernel kernel = new Kernel();
  Filesystem filesystem = new Filesystem();
  kernel.setFilesystem(filesystem);
  filesystem.setKernel(kernel);
  return kernel;
}

public Filesystem newFilesystem() {
  Kernel kernel = new Kernel();
  Filesystem filesystem = new Filesystem();
  kernel.setFilesystem(filesystem);
  filesystem.setKernel(kernel);
  return filesystem;
}

A similar idea can be had in C++ with thoughtful use of private and friend.

于 2012-05-18T20:53:05.397 回答
0

虽然没有任何声明性支持强制执行这样的构造,但我建议定义(通过注释)一类受以下约束的泄漏安全构造函数和参数:

  1. 泄漏安全构造函数只能使用泄漏安全参数来调用嵌套的泄漏安全构造函数(其中参数也必须作为泄漏安全传递),或将它们存储在自己的字段中。
  2. 泄漏安全构造函数不能取消引用任何泄漏安全参数,也不能取消引用从泄漏安全参数加载的任何字段。
  3. 泄漏安全构造函数不能将正在构造的对象传递给任何地方,除非作为泄漏安全参数传递给嵌套的泄漏安全构造函数。
  4. 通过调用传递泄漏安全参数构造的对象或正在构造的对象到泄漏安全构造函数将受到泄漏安全参数的所有约束。

如果遵守这样的约束,应该有可能让泄漏安全的构造函数产生相互引用的对象,以一种可以静态证明的方式,永远不会在它们的构造函数之外取消引用任何部分构造的对象(Foo可以将正在构造的对象传递给 的构造函数Bar,但是如果该构造函数既不取消引用传入的对象,也不将其暴露给任何可能这样做的代码,也不将其保留在自身之外的任何地方,那么它可以被取消引用的唯一方法是取消引用 ; 的新创建的实例,Bar如果Foo' 的构造函数不这样做,它将被取消引用,直到Foo' 的构造函数返回)。

于 2012-08-19T17:27:33.140 回答