循環参照の何が問題になっていますか?


160

私は今日、プログラミングの議論に参加しました。そこでは、循環参照(モジュール、クラスなど)が一般的に悪いと公理的に仮定したステートメントをいくつか作成しました。ピッチで問題が解決したら、同僚は「循環参照の何が問題になっていますか?」と尋ねました。

これには強い気持ちがありますが、簡潔かつ具体的に言葉で表現するのは難しいです。私が思いつくかもしれない説明は、公理と見なされる他の項目に依存する傾向があります(「単独では使用できないため、テストできません」、「参加オブジェクトの状態が変化するときの不明/未定義の動作」など) )、しかし、私は循環参照が悪い理由の簡潔な理由を聞きたいです。それは私の脳が持っている種類の信仰の飛躍をとらない、何年にもわたってそれらを理解し、修正し、さまざまなコードを拡張します。

編集:二重にリンクされたリストや親へのポインターのような同種の循環参照については尋ねていません。この質問は、libAがlibAをコールバックするlibBを呼び出すなど、「より大きいスコープ」の循環参照について本当に質問しています。必要に応じて、「lib」を「module」に置き換えます。これまでのすべての答えに感謝します!


循環参照はライブラリとヘッダーファイルに関係しますか?ワークフローでは、新しいProjectBコードがレガシーProjectAコードからの出力ファイルを処理します。ProjectAからの出力は、ProjectBによって駆動される新しい要件です。ProjectBには、どのフィールドがどこに行くかなどを一般的に決定するのを容易にするコードがあります。重要なことは、レガシーProjectAは新しいProjectBでコード再利用できることです。レコードの解析、データの検証と変換など)。
Luv2code

1
@ Luv2codeプロジェクト間でコードをカットアンドペーストした場合、またはおそらく両方のプロジェクトが同じコード内でコンパイルおよびリンクされた場合にのみ、愚かになります。このようなリソースを共有している場合は、ライブラリに入れてください。
ダッシュトムバン

回答:


220

循環参照には非常に多くの間違いがあります。

  • 循環クラス参照は、高い結合を作成します。両方のクラスは毎回再コンパイルする必要がありますいずれかのそれらのが変更されています。

  • BはAに依存しますが、Bが完了するまでAはアセンブルできないため、循環アセンブリ参照は静的リンクを防ぎます

  • 循環オブジェクト参照は、スタックオーバーフローで単純な再帰アルゴリズム(シリアライザー、訪問者、プリティプリンターなど)をクラッシュさせる可能性があります。より高度なアルゴリズムにはサイクル検出があり、単に説明的な例外/エラーメッセージで失敗します。

  • 循環オブジェクト参照も依存性注入を不可能にし、システムのテスト容易性を大幅に低下させます。

  • 非常にしたオブジェクトの大循環参照の数が多い神オブジェクト。そうでなくても、スパゲッティコードにつながる傾向があります

  • 循環エンティティ参照(特にデータベース内、ドメインモデル内)は、最終的にデータ破損または少なくとも不整合につながる可能性のある非ヌル性制約の使用を防ぎます。

  • 循環参照一般的には、単純にあり混乱してどのようにプログラムの機能を理解しようとしたときに大幅に認知的負荷を増加させます。

子供たちのことを考えてください。できる限り循環参照を避けてください。


32
最後の点を特に感謝します。「認知的負荷」は私が非常に意識しているものですが、簡潔な用語はありませんでした。
ダッシュトムバング

6
いい答えだ。テストについて何か言った方がいいでしょう。モジュールAとBが相互に依存している場合、一緒にテストする必要があります。つまり、実際には独立したモジュールではありません。一緒に、それらは1つの壊れたモジュールです。
ケビンクライン

5
依存関係の注入は、自動参照であっても循環参照では不可能ではありません。コンストラクターパラメーターとしてではなく、プロパティを挿入する必要があります。
BlueRaja-ダニーPflughoeft 14

3
@ BlueRaja-DannyPflughoeft:アンチパターンは、他の多くのDI実践者と同様に、(a)プロパティが実際に依存関係であることが明確ではないため、(b)「インジェクト」されるオブジェクトが簡単にできないため、独自の不変式を追跡します。さらに悪いことに、Castle Windsorのような最も洗練された/人気のあるフレームワークの多くは、依存関係を解決できない場合に有用なエラーメッセージを出すことができません。どのコンストラクターのどの依存関係を解決できなかったのかを正確に説明する代わりに、迷惑なNULL参照ができてしまいます。できるからといって、そうすべきだという意味ではありません。
アーロンノート14

3
私はそれが良い習慣だと主張していませんでした。答えで主張されているように、それは不可能ではないと指摘していました。
BlueRaja-ダニーPflughoeft 14

22

循環参照は、非循環参照の結合の2倍です。

FooがBarを知っていて、BarがFooを知っている場合、変更が必要な2つのことがあります(FoosとBarsがお互いを知る必要がなくなったとき)。FooがBarを知っているが、BarはFooを知らない場合、Barに触れることなくFooを変更できます。

循環参照は、少なくとも長時間続く環境(デプロイされたサービス、イメージベースの開発環境)でブートストラップの問題を引き起こす可能性があります。負荷。


17

2ビットのコードを結び付けると、1つの大きなコードが効果的にできます。少しのコードを維持することの難しさは、少なくともそのサイズの2乗であり、場合によってはそれ以上です。

多くの場合、人々は単一のクラス(/ function / file / etc)の複雑さを見て、最小の分離可能な(カプセル化可能な)ユニットの複雑さを本当に考慮する必要があることを忘れます。循環依存性があると、おそらくユニット1のサイズが目に見えないほど大きくなります(ファイル1の変更を開始し、ファイル2〜127の変更も必要になるまで)。


14

それらは、それ自体ではなく、設計の不良の可能性を示す指標として悪い場合があります。FooがBarに依存し、BarがFooに依存している場合、それらが一意のFooBarではなく2つである理由を疑問視することは正当化されます。


10

うーん...それはあなたが循環依存によって何を意味するかに依存します。なぜなら、実際には非常に有益だと思ういくつかの循環依存があるからです。

XML DOMを考えてみましょう-すべてのノードが親への参照を持ち、すべての親がその子のリストを持つことは理にかなっています。構造は論理的にはツリーですが、ガベージコレクションアルゴリズムなどの観点から見ると、構造は循環的です。


1
それは木ではないでしょうか?
コンラッドフリックス

@コンラッド:はい、それは木と考えることができると思います。どうして?
ビリーONeal

1
子を下にナビゲートすることができ、(親の参照に関係なく)終了するため、ツリーは循環とは見なしません。ノードに、私の頭の中ではツリーではなくグラフにする祖先でもある子がいない限り。
コンラッドフリックス

5
循環参照は、ノードの子の1つが祖先にループバックする場合です。
マットオレニック

これは、実際には循環依存ではありませ(少なくとも問題を引き起こすような方法ではありません)。たとえばNode、それがクラスであり、それNode自体の内部に子供のための他の参照を持っていると想像してください。クラスはそれ自体を参照しているだけなので、クラスは完全に自己完結型であり、他のものとは結合していません。---この引数を使用すると、再帰関数は循環依存関係であると主張できます。それ(一気に)ですが、悪くはありません。
-byxor

9

鶏または卵の問題のようなものです。

循環参照が避けられず有用な場合が多くありますが、たとえば次の場合は機能しません。

プロジェクトAはプロジェクトBに依存し、BはAに依存します。AはBで使用するためにコンパイルする必要があります。


6

ここでのコメントの大部分には同意しますが、「親」/「子」循環参照の特別なケースをお願いします。

クラスは多くの場合、その親または所有クラス、デフォルトの動作、データの取得元のファイルの名前、列を選択したSQL文、またはログファイルの場所などについて何かを知る必要があります。

包含クラスを使用することにより、循環参照なしでこれを行うことができます。これにより、以前は「親」だったものが兄弟になりますが、これを行うために既存のコードをリファクタリングできるとは限りません。

もう1つの方法は、子がコンストラクターで必要とする可能性のあるすべてのデータを渡すことです。


関連する注意事項として、XがYへの参照を保持する2つの一般的な理由があります。Xは、Yに代わってXに代わって何かをするようYに依頼したい場合があります。Yに存在する唯一の参照が他のオブジェクトがYの代わりに何かをしたい場合、そのような参照の所有者は、Yのサービスはもはや必要ではなく、Yへの参照を放棄すべきであることを伝えられるべきです。彼らの利便性。
supercat

5

データベース用語では、適切なPK / FK関係を持つ循環参照により、データの挿入または削除が不可能になります。レコードがテーブルbから削除されない限りテーブルaから削除できず、テーブルAからレコードが削除されない限りテーブルbから削除できない場合は、削除できません。挿入についても同じです。多くのデータベースでは、循環参照がある場合、カスケード更新または削除を設定できない理由があります。これは、ある時点で不可能になるためです。はい、正式に宣言されているPK / Fkを使用せずにこのような関係を設定できますが、データの整合性の問題が発生します(私の経験では100%)。それはただの悪いデザインです。


4

モデリングの観点からこの質問を取り上げます。

実際に存在しない関係を追加しない限り、安全です。それらを追加すると、データの整合性が低くなり(冗長性があるため)、より密結合されたコードになります。

具体的には、循環参照は、自己参照という1つを除いて、実際に必要になる場合を見たことがないということです。ツリーまたはグラフをモデル化する場合、それが必要です。コード品質の観点から自己参照は無害なので、完全に問題ありません(依存関係は追加されません)。

非自己参照が必要になった瞬間に、すぐにそれをグラフとしてモデル化できないかどうか尋ねる必要があると思います(複数のエンティティを1つのノードに折りたたむ)。循環参照を作成する場合もあるかもしれませんが、グラフとしてモデル化することは適切ではありませんが、私はそれを非常に疑います。

人々は循環参照が必要であると考えるが、実際には必要ではないという危険があります。最も一般的なケースは「1対多のケース」です。たとえば、複数のアドレスを持つ顧客がいて、そこから1つをプライマリアドレスとしてマークする必要があります。has_addressis_primary_address_ofの 2つの別個の関係としてこの状況をモデル化することは非常に魅力的ですが、正しくありません。理由は、プライマリアドレスであることはユーザーとアドレスの間の別個の関係ではなく、代わりにアドレスを持つ関係の属性であるためです。。何故ですか?そのドメインはユーザーのアドレスに限定されており、存在するすべてのアドレスに限定されないためです。リンクの1つを選択し、それを最強(プライマリ)としてマークします。

(データベースについて説明します)多くの人は、「プライマリ」を一意のポインターであり、外部キーは一種のポインターであると理解しているため、2関係ソリューションを選択します。だから、外部キーを使用する必要がありますよね?違う。外部キーは関係を表しますが、「プライマリ」は関係ではありません。これは、1つの要素が何よりも優先され、残りの要素が順序付けされていない順序の縮退したケースです。全体の順序をモデル化する必要がある場合、基本的に他の選択肢はないため、もちろんそれを関係の属性と見なします。しかし、あなたがそれを縮退させた瞬間には、リレーションシップではない何かをリレーションシップとしてモデル化するという選択肢があり、非常に恐ろしいものがあります。だからここに来る-関係の冗長性は確かに過小評価されるものではありません。

そのため、モデリングしているものに由来するものであることが完全に明らかでない限り、循環参照を許可しません。

(注:これはデータベース設計にわずかに偏っていますが、他の領域にもかなり適用できると思います)


2

私は別の質問でその質問に答えます:

円形の参照モデルを保持することが、構築しようとしているものに最適なモデルであるという状況を教えてください。

私の経験から、最良のモデルは、私があなたが言っているように循環参照を含むことはほとんどないでしょう。そうは言っても、常に循環参照を使用するモデルはたくさんありますが、これは非常に基本的なものです。親->子関係、グラフモデルなど。ただし、これらはよく知られたモデルであり、他の何かを完全に参照していると思います。


1
循環リンクリスト(シングルリンクまたはダブルリンク)は、「決して停止しない」(キューの重要なN個の要素をキューに貼り付ける)プログラムの中央イベントキューの優れたデータ構造になる可能性があります。 「削除しない」フラグを設定し、空になるまで単純にキューを走査します。新しいタスク(一時的または永続的)が必要な場合は、キューの適切な場所に固定します。 、その後、キューから削除します)。
バティーン

1

データ構造内の循環参照は、データモデルを表現する自然な方法である場合があります。コーディングに関しては、これは間違いなく理想的ではなく、依存性注入によって(ある程度)解決でき、問題をコードからデータにプッシュします。


1

循環参照構造は、設計の観点からだけでなく、エラー検出の観点からも問題があります。

コード障害の可能性を考慮してください。メソッドをまだ開発していないか、怠けているため、どちらのクラスにも適切なエラーキャッチを設定していません。いずれにしても、何が発生したかを伝えるエラーメッセージは表示されないので、デバッグする必要があります。優れたプログラム設計者は、どのメソッドがどのプロセスに関連しているかを知っているため、エラーの原因となったプロセスに関連するメソッドに絞り込むことができます。

循環参照では、問題が倍になりました。プロセスは緊密にバインドされているため、あるクラスが他のクラスに依存しているため、どのクラスのどのメソッドがエラーを引き起こしたのか、またはエラーがどこから発生したのかを知る方法がありません。どちらのクラスが実際にエラーの原因であるかを見つけるために、両方のクラスのテストに時間を費やす必要があります。

もちろん、適切なエラーキャッチはこれを解決しますが、エラーがいつ発生する可能性があるかを知っている場合のみです。また、一般的なエラーメッセージを使用している場合は、まだそれほど良い方法ではありません。


1

一部のガベージコレクターは、各オブジェクトが別のオブジェクトによって参照されているため、クリーンアップに問題があります。

編集:以下のコメントで指摘されているように、これはガベージコレクターでの非常に素朴な試みにのみ当てはまり、実際に遭遇することはありません。


11
うーん、これによってガベージコレクターが作動することは、真のガベージコレクターではありません。
ビリーONeal

11
循環参照に問題がある現代のガベージコレクターは知りません。参照カウントを使用している場合、循環参照は問題になりますが、ほとんどのガベージコレクターはトレーススタイルです(既知の参照のリストから始めて、他のすべてを収集し、他のすべてを収集します)。
ディーンハーディング

4
sct.ethz.ch/teaching/ws2005/semspecver/slides/takano.pdfを参照してください。さまざまなタイプのガベージコレクターの欠点について説明しています。 、循環構造に問題が生じ始めます(循環オブジェクトが異なる世代にある場合)。参照カウントを取得し、循環参照の問題の修正を開始すると、マークとスイープの特性である長い休止時間が導入されることになります。
ケンブルーム

ガベージコレクターがFooを見て、この例ではBarを参照するメモリの割り当てを解除すると、Barの削除を処理する必要があります。したがって、この時点でガベージコレクターが先に進んでbarを削除する必要はありません。または逆に、Fooを参照するBarを削除すると、Fooも削除されるため、Barを削除したときにFooを削除する必要があるため、Fooを削除する必要はありませんか?私が間違っている場合は修正してください。
クリス

1
Objective-Cでは、循環参照によって、リリース時に参照カウントがゼロにならないようにします。これにより、ガベージコレクターが作動します。
DexterW

-2

私の意見では、無制限の参照があるとプログラムの設計が容易になりますが、一部のプログラミング言語ではコンテキストによってはサポートされていないことがわかっています。

モジュールまたはクラス間の参照について言及しました。その場合、それはプログラマーによって事前定義された静的なものであり、プログラマーが循環性に欠ける構造を検索することは明らかに可能ですが、問題にうまく適合しないかもしれません。

実際の問題は、実行時データ構造の循環性にあります。循環性を取り除く方法では、実際にはいくつかの問題を定義できません。結局のところ-それは、プログラマに不必要なパズルを解かせるのを強制し、他のものを必要とする問題です。

それはツールの問題であり、原則の問題ではないと思います。


一文の意見を追加しても、投稿に大きく貢献したり、回答を説明したりすることはありません。これについて詳しく説明していただけますか?

まあ2点、ポスターは実際にモジュールまたはクラス間の参照に言及しました。その場合、それはプログラマーによって事前定義された静的なものであり、プログラマーが循環性に欠ける構造を検索することは明らかに可能ですが、問題にうまく適合しないかもしれません。実際の問題は、実行時データ構造の循環性にあります。循環性を取り除く方法では、実際にはいくつかの問題を定義できません。結局のところ-それは、プログラマに不必要なパズルを解かせるのを強制し、他のものを必要とする問題です。
ジョシュS

プログラムを起動して実行するのが簡単になることがわかりましたが、一般的に言えば、些細な変更がカスケード効果をもたらすため、最終的にはソフトウェアのメンテナンスが難しくなります。AはBを呼び出し、Aを呼び出してBを呼び出します。この性質の変化の影響を真に理解するのは難しいと思います。特にAとBが多態性の場合はそうです。
ダッシュトムバン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.