如何设计类以防止循环依赖项在构造之前调用派生成员

本文关键字:调用 成员 派生 设计类 依赖 循环 | 更新日期: 2023-09-27 18:34:49

(我把它标记为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(的语言中不会出现此问题,因为这样子类在调用它们的构造函数之前甚至不存在......但在这里,这个问题发生了。

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

编辑:

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

如何设计类以防止循环依赖项在构造之前调用派生成员

对我来说,这种对象之间有循环依赖关系闻起来很糟糕。

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

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

如果你选择构图,那么当主要对象被破坏时,只需销毁次要对象即可。

下面是我的意思的一个例子:

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 );
    }
}

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

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. */ }
}  

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

如果需要保留对象对,并相互引用,则应提供一个实用程序来正确构建它们。 使用工厂模式,通过将构造和装配隐藏在工厂模式方法后面来降低构造的复杂性。

在 Java 中,将构造函数放在包中,并使内部组件的构造函数和初始赋值设置方法"包私有">

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;
}

在深思熟虑地使用私人和朋友C++中也可以有类似的想法。

尽管对

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

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

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