12

警告: この質問では、例として RPG との類似性を使用しています。

C# を使用して、私が夢見ていたこの RPG を作成しているとしましょう。プレイヤーが戦闘に入ると、戦場で利用可能なさまざまな対戦相手やアイテムなど、戦闘に関連するすべての要素への参照を保持する、ある種の戦場が表示されます。

現在、これらすべての要素には 1 つではなくいくつかの役割があります。たとえば、味方 (直接継承によるファイター) は、戦場を移動したり、コマンドを発行したり、敵の標的になったりすることができます。

今では、石の中の強力な剣も戦闘でいくつかの役割を果たしています. 明らかに、移動したりコマンドを発行したりすることはできませんが、それでもターゲットにすることができ、(うまくいけば)持ち上げたり使用したりできます.

これらの動作はすべて、コード内のインターフェイスによって表されるため、実装するオブジェクトに関係なく、同じ動作を持つすべてのオブジェクトを使用できます。

コード例:

public class Ally : Fighter, IMovable, IActor, ITarget
{
    // IMovable implementation
    public void Move(...) { ... }

    // IActor implementation
    public bool HasInitiative { get { ... } }

    public ICommand IssueCommand(...) { ... }

    // ITarget implementation
    public void OnTarget(...) { ... }

    // other code ...
}

public class MightySword : ITarget, ILiftable, IUsable
{
    // ITarget implementation
    public void OnTarget(...) { ... }

    // ... other interface implementation

    // other code ...
}

ここで私の質問は次のとおりです。Battlefield クラスは、これらすべてのオブジェクトへの参照をどのように保持する必要がありますか? 考えられる解決策を 2 つ思いつきましたが、現時点では、どちらが最適か、さらに優れた解決策が見つかるかどうかはわかりません。

ソリューション A:

複数回参照し、直接アクセスする: このソリューションを使用すると、各オブジェクトは実装するインターフェイスごとに 1 回参照されるため、私の戦場クラスは次のようになります。

public class Battlefield : ...
{
   IList<IMovable> movables;
   IList<ITarget> targets;
   IList<IActor> actors;
   // ... and so on
}

Battlefield.Update メソッドは次のようになります。

Update(...) 
{
  // Say every actor needs to target somebody else
  // it has nothing to do in the Update method,
  // but this is an example
  foreach (IActor actor in actors) 
  {
    if (actor.HasInitiative) 
    {
      ITarget target = targets[actor.TargetIndex];
      target.OnTarget(actor.IssueCommand(...));
    }
  }
}

このソリューションにより、オブジェクトのさまざまな動作に直接アクセスできますが、Ally を可動オブジェクト、ターゲット、およびアクターで参照する必要があるため、冗長性が生じます。この冗長性は、より大きなメモリフットプリントを意味し、バトルフィールドへのオブジェクトの追加と削除をより複雑にします (各タイプは、どのコレクションから追加/削除する必要があります)。

また、新しいインターフェイスを追加するということは、アイテムを保持し、対応するコードを管理するための新しいコレクションを追加することを意味します。

ソリューション B:

1 回の参照、フィルター アクセス: このソリューションを使用すると、すべてのオブジェクトが 1 つのクラスまたはインターフェイス (IBattleElement に似たもの) から派生し、1 つのコレクションに格納されます。要素を使用する場合、それらはそのタイプに従ってフィルタリングされます。

Update(...)
{
    IList<IActor> actors = elements.OfType<Actor>();
    foreach (IActor actor in actors) 
    {
     ITarget target = elements.OfType<Target>()[actor.TargetIndex];
     target.OnTarget(actor.IssueCommand(...));
    }
}

そこには。これ以上の冗長性はありませんが、「typeof」構造を使用するのは嫌いで、通常は避けようとします。実行時のパフォーマンスに関しては、明らかにこれの方が悪いです。

おもちゃのプロジェクトでソリューション A を実装しましたが、このプロジェクトではパフォーマンスが重要になるものは何もありません。答えは、パフォーマンス (メモリに関しても CPU に関しても) だけでなく、BattleElement の新しいクラスや新しい動作のための新しいインターフェイスを追加するなど、私やデザイナーが実行しなければならない操作に最適な設計を提供するものも考慮する必要があります。バトルフィールドからオブジェクトのインスタンスを削除し、より一般的にはゲーム ロジックで使用します。

私の例が一般的にばかげていることをお詫びします。私の英語が十分であることを願っています。また、私の質問が少なくとも少しは興味を引くものであり、設計自体の全体的な愚かさを示していないことを願っています。

4

3 に答える 3

4

解決策 A。ただし、理由がある場合は、インスタンスのリスト全体を一度にクエリすることを軽視しないでください。

一般に (大きな一般化ですが、重要な前例があります)、可能な限り派生型の少ない型、つまり関心のあるインターフェイスのみを使用することで、最大の柔軟性を実現できます。事後にインスタンスを分割するのではなく、事前にインスタンスを操作してグループ化することで、最高のパフォーマンスが得られる可能性があります。

パフォーマンスの向上が重要かどうかは別の問題です。おそらく数十または数百のオブジェクト (数百万ではない) について話しているので、パフォーマンスは私の最初の関心事ではありません。

私は、設計の観点から、リアルタイム型の問い合わせを必要とするものは避けます (ただし、それは意見の問題です)。

パフォーマンスと同様に、メモリを設計する前にメモリをプロファイルします。Visual Studio を使用している場合は、そのための優れたツールを利用できます。そして、おそらくこれらのインスタンスをコピーしていないので、オーバーヘッドはとにかく最小限です。

ハイブリッド アプローチも理にかなっている可能性があります。

  • 単一のコレクションから追加/削除します。
  • コレクションと、特定の種類のフィルター処理された読み取り専用コレクションの両方を公開します。
  • 必要な場合にのみ、これらのフィルタリングされたコレクションを (再) 構築します。

public class Battlefield
{
    private readonly IList<IBattleElement> _all = new List<IBattleElement>();
    private IReadOnlyList<IActor> _actors;
    private bool _isDirty;

    /// <summary>
    ///     Gets a list of all elements.
    /// </summary>
    public IReadOnlyList<IBattleElement> All
    {
        get { return _all; }
    }

    /// <summary>
    ///     Gets a filtered list of elements that are <c>IActor</c>.
    /// </summary>
    public IReadOnlyList<IActor> Actors
    {
        get
        {
            if( _isDirty || _actors == null )
            {
                _actors = All.OfType<IActor>().ToList();
            }

            return _actors;
        }
    }

    /// <summary>
    ///     The one and only way to add new elements. This could be made thread-safe if needed.
    /// </summary>
    /// <param name="element"></param>
    public void AddElement( IBattleElement element )
    {
        _all.Add( element );
        _isDirty = true;
    }
}

...そして、私の質問には少なくとも少しの関心があり、デザイン自体の全体的な愚かさを示しているわけではありません.

まったくばかげていません。

于 2013-09-24T19:09:07.530 に答える