23

これは確かに主観的なことですが、議論になることは避けたいと思います。人々がそれを適切に扱うならば、それは興味深い質問になるかもしれないと思います。

最近のいくつかのプロジェクトでは、長い委任チェーンが一般的なアーキテクチャを実装していました。

二重の委任チェーンは非常に頻繁に発生する可能性があります。

bool Exists = Env->FileSystem->FileExists( "foo.txt" );

そして、トリプル委任はまったく珍しいことではありません。

Env->Renderer->GetCanvas()->TextStr( ... );

高次の委任チェーンは存在しますが、実際には不足しています。

上記の例では、使用されるオブジェクトは常に存在し、プログラムの機能に不可欠であり、実行開始時に明示的に構築されるため、NULLランタイムチェックは実行されません。基本的に、私はこれらの場合に委任チェーンを分割するために使用しました:

1)委任チェーンを通じて取得したオブジェクトを再利用します。

{ // make C invisible to the parent scope
   clCanvas* C = Env->Renderer->GetCanvas();
   C->TextStr( ... );
   C->TextStr( ... );
   C->TextStr( ... );
}

2)委任チェーンの途中にある中間オブジェクトは、使用する前にNULLをチェックする必要があります。例えば。

clCanvas* C = Env->Renderer->GetCanvas();

if ( C ) C->TextStr( ... );

私は以前、プロキシオブジェクトを提供することでケース(2)と戦っていました。これにより、NULL以外のオブジェクトでメソッドを呼び出して、empty結果を得ることができます。

私の質問は次のとおりです。

  1. ケース(1)または(2)のどちらかがパターンまたはアンチパターンですか?
  2. C ++で長い委任チェーンを処理するためのより良い方法はありますか?

これが私の選択をする際に私が考慮したいくつかの賛否両論です:

長所:

  • それは非常に説明的です:オブジェクトがどこから来たのかは1行のコードから明らかです
  • 長い委任チェーンは見栄えがします

短所:

  • 委任チェーン内の複数の一時オブジェクトを検査するのは難しいため、インタラクティブなデバッグは面倒です。

長い委任チェーンの他の長所と短所を知りたいです。あなたの意見を提示し、あなたがそれにどれだけ同意するかではなく、どれほどよく議論された意見に基づいて投票してください。

4

8 に答える 8

14

どちらもアンチパターンとは言いません。Cただし、最初の方法には、変数が論理的に関連した後でも変数が表示されるという欠点があります(あまりにも無償のスコープ)。

これは、次の構文を使用して回避できます。

if (clCanvas* C = Env->Renderer->GetCanvas()) {
  C->TextStr( ... );
  /* some more things with C */
}

これは C++ で許可され (C ではありませんが)、適切なスコープを維持し (C条件ブロック内にあるかのようにスコープされます)、NULL をチェックできます。

何かが NULL ではないことを主張することは、SegFault によって強制終了されるよりもはるかに優れています。そのため、ポインターが NULL になることは決してないという 100% の確信がない限り、これらのチェックを単純にスキップすることはお勧めしません。


さらに、特にダンディーだと感じる場合は、追加の無料関数でチェックをカプセル化できます。

template <typename T>
T notNULL(T value) {
  assert(value);
  return value;
}

// e.g.
notNULL(notNULL(Env)->Renderer->GetCanvas())->TextStr();
于 2012-07-24T13:17:40.217 に答える
6

私の経験では、そのようなチェーンには、些細なことではないゲッターが含まれていることが多く、非効率につながります。(1)は合理的なアプローチだと思います。プロキシオブジェクトを使用するのはやり過ぎのようです。プロキシオブジェクトを使用するのではなく、NULLポインタでクラッシュが発生することを望んでいます。

于 2012-07-24T13:11:26.227 に答える
6

デメテルの法則に従えば、このような長い委任の連鎖は起こらないはずです。私はしばしば、その支持者の何人かに対して、彼らは良心的すぎると主張してきましたが、長い委任チェーンを処理する最善の方法を考えるようになった場合は、おそらくその推奨事項にもう少し準拠する必要があります.

于 2012-08-25T17:39:22.060 に答える
4

興味深い質問です。これは解釈の余地があると思いますが、

マイ ツー セント

デザインパターンは、一般的な問題に対する再利用可能なソリューションであり、オブジェクト指向 (通常は) プログラミングに広く適用できるほど一般的です。多くの一般的なパターンは、インターフェイス、継承チェーン、および/または包含関係から始まり、チェーンを使用して物事をある程度呼び出すことになります。ただし、パターンはこのようなプログラミングの問題を解決しようとしているわけではありません。連鎖は、目の前の機能上の問題を解決することの副作用にすぎません。だから、私は本当にそれをパターンとは見なしません。

同様に、アンチパターンは (私の考えでは) デザインパターンの目的に反するアプローチです。たとえば、デザイン パターンはコードの構造と適応性がすべてです。人々はシングルトンをアンチパターンと考えています。なぜなら、シングルトンは本質的にグローバルを作成し、多くの場合、設計が急速に劣化するという事実のために (常にではありませんが) 蜘蛛の巣のようなコードになるからです。

繰り返しになりますが、連鎖の問題は必ずしも設計の良し悪しを示すものではありません。パターンの機能上の目的やアンチパターンの欠点とは関係ありません。一部のデザインでは、うまく設計されていても、ネストされたオブジェクトがたくさんあります。


それについて何をすべきか:

長い委譲チェーンは、しばらくすると間違いなく厄介な問題になる可能性があります。これらのチェーンのポインターが再割り当てされないように設計されている限り、興味のあるチェーンのポイントへの一時的なポインターを保存すると思いますin は完全に問題ありません (関数スコープまたはあまり好ましくない)。

個人的には、チェーンの一部への永続的なポインターをクラスメンバーとして保存することに反対しています。これは、サブオブジェクトへの30個のポインターが永続的に保存され、オブジェクトがどのように保存されているかについてのすべての概念を失うことになるのを見たからです。作業しているパターンまたはアーキテクチャに配置されています。

もう1つの考え-これが好きかどうかはわかりませんが、チェーンをナビゲートするプライベート(あなたの正気のために)関数を作成して、それを思い出して、かどうかの問題に対処しないようにする人を見てきましたカバーの下でポインターが変化するか、null があるかどうかに関係なく。すべてのロジックを一度ラップし、チェーンのどの部分からポインターを取得するかを示す適切なコメントを関数の先頭に置き、委譲を使用する代わりに関数の結果をコードで直接使用するのは良いことです。毎回チェーン。

パフォーマンス

私の最後のメモは、このラップイン関数アプローチとデリゲーション チェーン アプローチの両方がパフォーマンスの欠点に悩まされていることです。一時ポインターを保存すると、これらのオブジェクトをループで使用している場合に、余分な 2 つの間接参照を何度も回避できます。同様に、関数呼び出しからポインターを格納すると、ループ サイクルごとに余分な関数呼び出しのオーバーヘッドが回避されます。

于 2012-07-24T13:24:58.740 に答える
3

チェーンのさらに詳細bool Exists = Env->FileSystem->FileExists( "foo.txt" );な内訳を知りたいので、私の理想的な世界では、次のコード行があります。

Environment* env = GetEnv();
FileSystem* fs = env->FileSystem;
bool exists = fs->FileExists( "foo.txt" );

なぜ?いくつかの理由:

  1. 読みやすさ: 行末まで読まないと気がbool Exists = Env->FileSystem->FileExists( "foo.txt" );済まない。長すぎる。
  2. 有効性: あなたがオブジェクトについて言及したにもかかわらず、あなたの会社が明日新しいプログラマーを雇い、彼がコードを書き始めた場合、明後日にはオブジェクトが存在しない可能性があります。これらの長い行はかなり友好的ではなく、新しい人はそれらを怖がって最適化などの興味深いことをする可能性があります...これは、経験豊富なプログラマーが修正するのに余分な時間がかかります.
  3. デバッグ: 万が一 (そして新しいプログラマーを雇った後)、アプリケーションがチェーンの長いリストでセグメンテーション違反をスローした場合、どのオブジェクトが問題のあるオブジェクトであったかを見つけるのは非常に困難です。内訳が詳細になるほど、バグの場所を見つけやすくなります。
  4. 速度: 同じチェーン要素を取得するために多数の呼び出しを行う必要がある場合は、「適切な」ゲッター関数を呼び出す代わりに、チェーンからローカル変数を「引き出す」方が速い場合があります。あなたのコードが本番用かどうかはわかりませんが、「適切な」ゲッター関数が欠落しているようで、代わりに属性のみを使用しているようです。
于 2012-08-27T11:44:19.000 に答える
3

長いデリゲート チェーンは、私にとってはデザインの匂いがします。

委譲チェーンが教えてくれるのは、1 つのコードが無関係なコードに深いアクセスを持っているということです。これは、 SOLIDの設計原則に反する高結合を思い起こさせます。

これに関する主な問題は保守性です。深さが 2 レベルに達している場合、それは 2 つの独立したコード部分であり、独自に進化し、あなたの下で壊れる可能性があります。チェーン内に関数がある場合、これはすぐに悪化します。関数には独自のチェーンが含まれる可能性があるためです。たとえば、Renderer->GetCanvas()オブジェクトの別の階層からの情報に基づいてキャンバスを選択する可能性があり、終了しないコード パスを適用することは困難です。コードベースの存続期間にわたってオブジェクトに深く到達します。

より良い方法は、SOLID の原則に従い、依存関係の挿入や制御の反転などの手法を使用して、オブジェクトがその職務を実行するために必要なものに常にアクセスできるようにするアーキテクチャを作成することです。このようなアプローチは、自動テストや単体テストにも適しています。

ちょうど私の2セント。

于 2012-08-31T00:07:41.583 に答える
2

可能であれば、ポインターの代わりに参照を使用します。したがって、デリゲートは有効なオブジェクトを返すか、例外をスローすることが保証されます。

clCanvas & C = Env.Renderer().GetCanvas();

存在できないオブジェクトについては、has、is などの追加のメソッドを提供します。

if ( Env.HasRenderer() ) clCanvas* C = Env.Renderer().GetCanvas();
于 2012-07-24T17:33:20.950 に答える
1

すべてのオブジェクトが存在することを保証できれば、あなたがしていることに問題はないと思います。他の人が述べたように、NULL は決して起こらないと思っていても、とにかく起こるかもしれません。

そうは言っても、どこでも裸のポインターを使用していることがわかります。私がお勧めするのは、代わりにスマート ポインターの使用を開始することです。-> 演算子を使用すると、ポインターが NULL の場合、スマート ポインターは通常スローします。したがって、SegFault を回避できます。それだけでなく、スマート ポインターを使用すると、コピーを保持でき、オブジェクトが足元から消えることはありません。ポインターが NULL になる前に、各スマート ポインターを明示的にリセットする必要があります。

そうは言っても、 -> 演算子が時々スローするのを防ぐことはできません。

それ以外の場合は、AProgrammer によって提案されたアプローチを使用したいと思います。オブジェクト A が、オブジェクト B が指すオブジェクト C へのポインターを必要とする場合、オブジェクト A が行っている作業は、おそらくオブジェクト B が実際に行うべきことです。したがって、A は常に B へのポインターを持っていることを保証でき (B への共有ポインターを保持しているため、NULL になることはないため)、常に B で関数を呼び出して、オブジェクト C でアクション Z を実行できます。 Z, B は常に C へのポインタを持っているかどうかを知っています。これは、B の実装の一部です。

C++11 では std::smart_ptr<> があることに注意してください。

于 2012-08-30T23:56:55.117 に答える