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のような初期化子ではなく)真のコンストラクターを定義できる言語では発生しません。これは、コンストラクターが呼び出される前にサブクラスが存在しないためです...しかし、ここでこの問題が発生します。

では、これが起こらないようにするための最良/適切な設計/実践は何ですか?

編集:

必ずしも循環参照が必要だと言っているわけではありませんが、実際には、両方が相互KernelFileManager依存しています。循環参照を使用せずにこの問題を軽減する方法について提案がある場合は、それも素晴らしいことです。

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

オブジェクトのペアを相互に参照して保持する必要がある場合は、それらを正しく構築するためのユーティリティを提供する必要があります。ファクトリパターンを使用し、ファクトリパターンメソッドの背後に構築とアセンブリを隠すことで、構築の複雑さを軽減します。

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++でも実現できます。

于 2012-05-18T20:53:05.397 に答える
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 に答える
0

このような構造を強制するための宣言型のサポートはありませんが、次の制約に従って、リークセーフなコンストラクターとパラメーターのカテゴリーを(コメントを介して)定義することをお勧めします。

  1. リークセーフコンストラクターは、リークセーフパラメーターを使用して、ネストされたリークセーフコンストラクター(パラメーターもリークセーフとして渡す必要がある)を呼び出すか、独自のフィールドに格納することができます。
  2. リークセーフコンストラクターは、リークセーフパラメーターやリークセーフパラメーターからロードされるフィールドを逆参照することはできません。
  3. リークセーフコンストラクターは、ネストされたリークセーフコンストラクターへのリークセーフパラメーターとして以外は、構築中のオブジェクトをどこにも渡さない場合があります。
  4. リークセーフパラメータの受け渡しを呼び出して構築されたオブジェクトまたは構築中のオブジェクトをリークセーフコンストラクタに渡すと、リークセーフパラメータのすべての制約が適用されます。

このような制約を順守する場合、部分的に構築されたオブジェクトがコンストラクター(のコンストラクターFoo構築中のオブジェクトをのコンストラクターに渡すことができますBarが、そのコンストラクターが渡されたオブジェクトを逆参照したり、そうする可能性のあるコードに公開したり、それ自体の外部に永続化したりしない場合、逆参照を取得できる唯一の方法は次のとおりです。新しく作成されたBar;のインスタンスを逆参照します。 Foo'コンストラクターがそれを行わない場合、'Fooコンストラクターが戻るまで'逆参照されます)。

于 2012-08-19T17:27:33.140 に答える