低カップリングと緊密な凝集


11

もちろん状況にもよります。しかし、より低いレベルのオブジェクトまたはシステムがより高いレベルのシステムと通信する場合、より高いレベルのオブジェクトへのポインターを保持するよりも、コールバックまたはイベントを優先する必要がありますか?

たとえばworld、メンバー変数を持つクラスがありますvector<monster> monstersmonsterクラスがと通信するとき、worldコールバック関数を使用する方がいいですかworldmonsterそれともクラス内のクラスへのポインターが必要ですか?


質問のタイトルのスペルは別として、それは実際には質問の形式で表現されていません。現在の形でこの質問に対する有用な回答を得ることができないと思うので、これを言い換えることは、ここで尋ねていることを固めるのに役立つと思います。また、これはゲームデザインの問題でもありません。プログラミング構造に関する問題です(デザインタグが適切であるかどうかがわからないため、「ソフトウェアデザイン」とタグでどこに到達したか思い出せません)
MrCranky

回答:


10

あるクラスが別のクラスと密接に結合せずに別のクラスと通信するには、主に3つの方法があります。

  1. コールバック関数を通じて。
  2. イベントシステムを通じて。
  3. インターフェイスを介して。

3つは互いに密接に関連しています。多くの点でイベントシステムは、コールバックの単なるリストです。コールバックは多かれ少なかれ単一のメソッドを持つインターフェースです。

C ++では、コールバックを使用することはほとんどありません。

  1. C ++は、thisポインターを保持するコールバックを適切にサポートしていないため、オブジェクト指向コードでコールバックを使用することは困難です。

  2. コールバックは基本的に、拡張できない1メソッドのインターフェースです。時間が経つにつれ、ほとんどの場合、そのインターフェースを定義するために複数のメソッドが必要になり、単一のコールバックで十分になることはほとんどありません。

この場合、おそらくインターフェースを使用します。あなたの質問では、monster実際に通信する必要があるものを実際には詳しく述べていませんworld。推測して、私は次のようなことをします:

class IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) = 0;
  virtual Item*    getItemAt(const Position & position) = 0;
};

class Monster {
public:
  void update(IWorld * world) {
    // Do stuff...
  }
}

class World : public IWorld {
public:
  virtual Monster* getNearbyMonster(const Position & position) {
    // ...
  }

  virtual Item*    getItemAt(const Position & position) {
    // ...
  }

  // Lots of other stuff that Monster should not have access to...
}

ここでの考え方IWorldは、Monsterアクセスする必要のある最低限必要なもの(これはくだらない名前です)のみを入れるということです。その世界観は可能な限り狭くすべきです。


1
時間が経過するにつれて、+ 1のデリゲート(コールバック)は通常、より多くなります。モンスターが何かに取り掛かれるようにモンスターにインターフェースを与えることは、私の意見では良い方法です。
Michael Coleman、

12

コールバック関数を使用して、呼び出す関数を非表示にしないでください。コールバック関数を配置し、そのコールバック関数に割り当てられている関数が1つだけの場合、結合を実際に壊すことはありません。あなたはそれを抽象化の別のレイヤーでマスクしているだけです。コンパイル時間以外は何も得られませんが、明確さが失われます。

私はそれをベストプラクティスとは厳密には言いませんが、親へのポインタを持つために何かに含まれるエンティティを持つことは一般的なパターンです。

そうは言っても、インターフェースパターンを使用して、モンスターが世界で呼び出すことができる機能の限られたサブセットを与えることは、あなたの価値があるかもしれません。


+1モンスターに限定された方法で親を呼び出すことは、私の考えでは良い方法です。
マイケルコールマン

7

一般的に私は双方向リンクを試して回避しますが、それらが必要な場合は、リンクを作成する方法とそれらを解除する方法の1つがあることを絶対に確認します。

多くの場合、必要に応じてデータを渡すことにより、双方向リンクを完全に回避できます。ささいなリファクタリングとは、モンスターが世界にリンクを維持するのではなく、それを必要とするモンスターメソッドを参照して世界を渡すようにすることです。それでもなお、モンスターが厳密に必要とする世界のビットのインターフェースのみを渡すことは、モンスターが世界の具体的な実装に依存するようにならないことを意味します。これは、インターフェース分離の原則依存関係の逆転の原則に対応しますが、イベント、シグナル+スロットなどで時々得られる過剰な抽象化を導入し始めません。

ある意味で、コールバックの使用は非常に特化したミニインターフェースであり、それは問題ないと主張できます。インターフェイスオブジェクトのメソッドの1つのコレクションを介して、または異なるコールバックのメソッドをいくつか組み合わせて、目標をより有意義に達成できるかどうかを決定する必要があります。


3

含まれているオブジェクトがコンテナーを呼び出すのを避けようとします。混乱を招き、正当化が簡単になりすぎ、使いすぎて、管理できない依存関係が作成されるためです。

私の意見では、理想的な解決策は、高レベルのクラスが低レベルのクラスを管理するのに十分スマートになることです。たとえば、モンスターと騎士の衝突がどちらか一方を知らずに発生したかどうかを判断することを知っている世界は、モンスターが騎士と衝突したかどうかを世界に尋ねるモンスターよりも私にはましです。

あなたの場合の別のオプション、私はおそらくモンスタークラスがワールドクラスについて知る必要がある理由を理解するでしょう、そしておそらくあなたはそれが作り出すそれ自身のクラスに分解することができる何かがワールドクラスにあることに気付くでしょうモンスタークラスが知っている感覚。


2

イベントがなければ、それほど遠くまでは行きませんが、イベントシステムの作成(および設計)を始める前に、本当の質問をする必要があります。なぜモンスターがワールドクラスと通信するのでしょうか。ほんとに?

モンスターがプレイヤーを攻撃する「古典的な」状況を考えてみましょう。

モンスターが攻撃している:ヒーローがモンスターの隣にいる状況を世界は非常によく識別し、モンスターに攻撃するように伝えることができます。したがって、モンスターの機能は次のようになります。

void Monster::attack(LivingCreature l)
{
  // Call to combat system
}

しかし、世界(すでにモンスターを知っている)は、モンスターに知られる必要はありません。実際には、モンスターはクラスWorldの存在そのものを無視できます。

モンスターが動いているときも同じです(サブシステムにクリーチャーを取得させ、そのための移動計算/意図を処理させます。モンスターは単なるデータの袋ですが、多くの人がこれは本当のOOPではないと言っています)。

私のポイントは、イベント(またはコールバック)はもちろん素晴らしいですが、直面するすべての問題に対する唯一の答えではありません。


1

可能な限り、オブジェクト間の通信を要求と応答のモデルに制限しようとしています。2つのオブジェクトAとBの間で、Aが直接または間接的にBのメソッドを呼び出す方法、またはBが直接または間接的にAのメソッドを呼び出す方法が存在する可能性があるという、プログラム内のオブジェクトの暗黙的な部分的な順序があります。ただし、AとBが相互にメソッドを相互に呼び出すことはできません。もちろん、メソッドの呼び出し元への逆方向の通信が必要な場合もあります。これを行うにはいくつかの方法があり、どちらもコールバックではありません。

1つの方法は、メソッド呼び出しの戻り値により多くの情報を含めることです。つまり、プロシージャが制御を返した、クライアントコードはそれをどうするかを決定します。

もう1つの方法は、相互の子オブジェクトを呼び出すことです。つまり、AがBのメソッドを呼び出し、BがAに情報を伝達する必要がある場合、BはCのメソッドを呼び出します。AとBはどちらもCを呼び出すことができますが、CはAまたはBを呼び出すことができません。オブジェクトAはBがAに制御を戻した後、Cから情報を取得する責任があります。これは、私が最初に提案した方法とは根本的に異なっていません。オブジェクトAは、戻り値からのみ情報を取得できます。オブジェクトAのメソッドはいずれもBまたはCによって呼び出されません。このトリックのバリエーションは、Cをパラメーターとしてメソッドに渡すことですが、CとAおよびBの関係に対する制限は依然として適用されます。

さて、重要な質問は、なぜ私がこのように物事を主張するのです。主な理由は3つあります。

  • それは私のオブジェクトをより疎結合に保ちます。私のオブジェクトは他のオブジェクトをカプセル化できますが、それらは呼び出し元のコンテキストに依存することはなく、コンテキストはカプセル化されたオブジェクトに依存することはありません。
  • それは私の制御フローを推論しやすくします。selfメソッドの実行中に内部状態を変更できる唯一のコードは、1つのメソッドであり、他のメソッドではないことを想定できるのは素晴らしいことです。これは、ミューテックスを並行オブジェクトに配置するのと同じ理由です。
  • オブジェクトのカプセル化されたデータの不変条件を保護します。パブリックメソッドはインバリアントに依存することが許可されており、別のメソッドが既に実行されているときに1つのメソッドを外部から呼び出すことができる場合、それらのインバリアントに違反することがあります。

コールバックのすべての使用に反対しているわけではありません。「呼び出し元を呼び出さない」という私の方針に従い、オブジェクトAがBのメソッドを呼び出してそれにコールバックを渡した場合、コールバックはAの内部状態を変更せず、AとAのコンテキスト内のオブジェクト。言い換えると、コールバックは、Bによって指定されたオブジェクトのメソッドのみを呼び出すことができます。実際には、コールバックはBと同じ制限の下にあります。

結び付ける最後のゆるい終わりは、私が話していたこの部分的な順序に関係なく、すべての純粋な関数の呼び出しを許可することです。純粋な関数はメソッドとは少し異なり、変更できないか、変更可能な状態や副作用に依存できないため、混乱を招く心配がありません。


0

個人的に?シングルトンを使用します。

ええ、大丈夫、悪いデザイン、オブジェクト指向ではない、など。気にしない。私はテクノロジーのショーケースではなく、ゲームを書いています。誰も私をコードで採点するつもりはありません。目的は楽しいゲームを作ることであり、私の邪魔になるものはすべて、あまり楽しくないゲームになるでしょう。

一度に2つの世界が実行されますか?多分!たぶんそうします。しかし、あなたがその状況を今考えることができなければ、おそらくそうは思わないでしょう。

だから、私の解決策:世界のシングルトンを作る。その上で関数を呼び出します。全体の混乱で行われます。すべての単一の関数に追加のパラメーターを渡すことができます-そして、間違いなく、これがリードするところです。または、機能するコードを作成することもできます。

このように行うには、乱雑になったときに(つまり「if」ではなく「when」の場合)クリーンアップするための少しの規律が必要ですが、コードが乱雑になるのを防ぐ方法はありません。スパゲッティの問題か、何千もの問題があります層の抽象化の問題。少なくともこの方法で、不要なコードを大量に作成することはありません。

シングルトンが不要になった場合は、通常、それを取り除くのは非常に簡単です。いくつかの作業が必要で、10億個のパラメーターを渡す必要がありますが、これらはとにかく渡す必要があるパラメーターです。


2
おそらく、もう少し簡潔に「リファクタリングを恐れないでください」と言います。
Tetrad、2011

シングルトンは悪です!グローバルははるかに優れています。リストしたすべてのシングルトンの美徳は、グローバルでも同じです。私はさまざまなサブシステムへのグローバルポインター(実際には参照を返すグローバル関数)を使用し、メイン関数でそれらを初期化/破棄します。グローバルポインターは、初期化順序の問題、分解中のシングルトンのぶら下がり、シングルトンの自明ではない構築などを回避します。シングルトンを繰り返すのは悪いことです。
deft_code

@Tetrad、非常に同意。それはあなたが持つことができる最高のスキルの一つです。
ZorbaTHut
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.