web-dev-qa-db-ja.com

わずかに異なるインターフェースを持つオブジェクトを反復する

私はオブジェクトのモジュールロジックを構築しています。個別の機能部分が個別のモジュールになります。オンデマンドでモジュールを接続および切断することができます。

ある時点で、オブジェクトは接続されたモジュールのコレクションを反復処理します。

_class MyObject {
    private List<IModule> modules = new List<IModule>();
    void Update()
    {
        modules.ForEach(a=>a.Update());
    }
    void Destroy()
    {
        modules.ForEach(a=>a.Destroy());
    }
}
_

そして、ここは興味深いものになっています。すべてのモジュールは破棄可能ですが、すべてが更新可能である必要はありません。それらのいくつかは、例えばThrow()のような別のメソッドを持つかもしれません。

1。次のインターフェイスを作成できます。

_interface IModule {
    void Update();
    void Destroy();
    void Throw();
}
_

そして、それから私のすべてのモジュールを継承します。しかし、ほとんどのモジュールでは空のThrow()が発生します。いくつかのThrowロジックを必要とするのは2、3のモジュールだけです。それから私のシステムはうまくいきます、唯一の欠点-空のメソッド呼び出し:modules.ForEach(a=>a.Throw());

2。複数のインターフェースを作成できます。

_interface IModule {
    void Destroy();
}
interface IUpdateable : IModule {
    void Update();
}
interface IThrowable : IModule {
    void Throw();
}
_

その場合、私のモジュールは次のようになります。

_class SimpleModule: IModule {...}
class ModuleWithUpdate: IUpdateable {...}
class ComplexModule: IUpdateable, IThrowable {...}

class MyObject {
    private List<IModule> modules = new List<IModule>();
    void Update()
    {
        foreach (var module in modules)
            if (module is IUpdateable updateable)
                updateable.Update();    // It will call ModuleWithUpdate and ComplexModule.
    }
}
_

2番目のオプションは望ましいようですが、それが問題に対する一般的なアプローチかどうか疑問に思わずにはいられません。オブジェクトのコレクションを作成するためのより良い方法はありますか?それらのいくつかは追加のメソッドを持ち、いくつかは持っていませんか?

最終的には数十のモジュールが発生する可能性があり、Updateは1秒あたり50〜100回起動しますが、両方のアプローチでパフォーマンスをカバーしていると思います。私が知る限り、ゴミや高価な操作はありません。

2
Xamtos

一般的な良い習慣

悪魔の擁護者を演じるとき、私はどちらの場合でも良い例の引数を使用して、なぜそれが悪い考えであるかを説明できます。基本的に:

  1. インターフェース分離原理 、SOLIDのIに違反しています。
  2. オブジェクトをアップキャストする必要があるのは、ポリモーフィズムの乱用です。

そうは言っても、2番目の議論はここではより弱いものです(この回答でさらに詳しく説明します)。ただし、この乱用を検討する正当な理由があります。例:

_public void LetThisDuckSwim(Animal a)
{
     if(a is Duck d)
         d.Swim();
}
_

これは誇張された例ですが、メソッドパラメータはDuckではなくAnimal型である必要があるということを理解してもらいます。

同じ問題とまったく同じではありませんが、 Liskov Substitution Principle の古典的な悪い例との重複が見られ始めます。

_void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck educk)
        educk.TurnOn();

    duck.Swim();
}
_

ElectricDuckIElectricDuckを使用する際の小さな違いを除いて、基本的に、達成したいことと同じシナリオがあり、これは悪い習慣の主な例として使用されます。


あなたのシナリオ

特定の実装クラスでインターフェースの一部を意図的に実装することを拒否するインターフェースについて話しているため、LSP違反やISP違反を回避するために、インターフェースを分離する必要があります。

ただし、すでにご存じのとおり、実際には次のアップキャストロジック(または同様のもの)が必要になります。

_void Update()
{
    foreach (var module in modules)
        if (module is IUpdateable updateable)
            updateable.Update();
}
_

オブジェクトのコレクションを作成するためのより良い方法はありますか?それらのいくつかは追加のメソッドを持ち、いくつかは持っていませんか?

あなたは少し間違った質問をしています。中心的な問題は、その集中化された「オブジェクトのコレクション」を使用する必要があるかどうかです。すべてを1つの大きなリストにまとめないことを検討してください。

_// 1
private List<IModule> modules = new List<IModule>();

// 2
private List<IUpdateableModule> updateableModules = new List<IUpdateableModule>();
private List<IDestroyableModule> destroyableModules = new List<IDestroyableModule>();
private List<IThrowableModule> throwableModules = new List<IThrowableModule>();
_

ここでは、どちらの場合にも妥当な議論があります。あなたの質問は、2番目のアプローチがあなたにとって等しく実行可能であるかどうかについての光を十分に照らしていません。

最初のアプローチには正当化の余地があります。 Unity(ゲームエンジン)は実際にこの方法でコンポーネントを使用します。コンポーネントを使用する前にコンポーネントをアップキャストしなければならないのは少し汚れていますが、確かにコンポーネントの処理が大幅に容易になります。

ただし、Unityには、正しいタイプのコンポーネントを取得するためのクエリロジックがいくつかあります。これは、2つの可能性の妥当な妥協点だと思います。 LINQは実際にOfTypeを介してすでにこの可能性を提供しています

_private List<IModule> modules = new List<IModule>();

private List<IUpdateableModule> updateableModules => modules.OfType<IUpdateableModule>();
_

これらの個別のプロパティを定義するか、OfTypeを直接使用するかは議論の余地がありますが、プロパティを定義すると、将来のモジュールの格納方法の変更による影響を最小限に抑えることができます。

最終的には数十のモジュールが発生する可能性があり、Updateは1秒あたり50〜100回起動しますが、両方のアプローチでパフォーマンスをカバーしていると思います。私が知る限り、ゴミや高価な操作はありません。

パフォーマンスはここで考慮すべき関連事項です。ただし、実際の問題が発生するまで待つこともできます。

スペクトルの下限を想定すると、12モジュールを1秒あたり50回反復すると、合計で600回の反復になります。これはO(m*n)の複雑さであるため、m(モジュール数)またはn(フレームレート)。

リストを分離してこれを最適化することは、最初に期待するほど簡単ではありません。

  • すべてのモジュールがすべてのインターフェースを実装すると、使用可能なインターフェースごとに1回モジュールリストを反復するため、パフォーマンスが低下します。
  • 平均してモジュールが使用可能なインターフェースの半分未満しか実装していない場合、不必要にそれらを反復しないことでパフォーマンスが向上します(特定のモジュールが実装していないインターフェースを反復する場合)。
  • 平均してモジュールが大部分のインターフェースを実装している場合、それらを別々のリストに配置すると、実際には数回(実装されたインターフェースごとに1回)繰り返され、パフォーマンスに悪影響を及ぼします。

実際の問題がある場合は、最適化をそのままにしておくことをお勧めします。すでにこの情報を記録しておけば、何かに目が離せないので興味深いかもしれませんパフォーマンスの変化(フレームレートの増加やモジュールの開発など)。

注:Unityはすでにここで計算を行っているのではないかと思い、パフォーマンスの考慮事項にかかわらず、中央集中型リストを使用することにしました。しかし、私は疑いの明確な確認をまだ見つけていません。

2
Flater

オブジェクトが異なるアクションをサポートしている場合は、はい、別のインターフェースを使用する必要があります(オプション2)。

アップキャストの問題を回避するには、リストをフィルタリングします(たとえば、各操作に対して OfType<T>() を使用します)。

class MyObject {
    private List<IModule> modules = new List<IModule>();
    void Update()
    {
        modules.OfType<IUpdateable>().ForEach(a=>a.Update());
    }
    void Destroy()
    {
        modules.OfType<IDestroyable>().ForEach(a=>a.Destroy());
    }
}
1
John Wu

これを行う3番目の方法を検討することもできます。 2番目の例の拡張バージョンで、冗長なキャストはありませんが、コレクションが追加されています。

class MyObject {
    private List<IModule> modules = new List<IModule>();
    private List<IUpdateable> updateableModules = new List<IUpdateable>();
    public void Add(IModule module)
    {
        modules.Add(module);
        if (module is IUpdateable updateable)
            updateableModules.Add(updateable);

    }
    public void Update()
    {
        foreach (var updateable in updateableModules)
                updateable.Update();
    }
}
1

単一のインターフェースを選択するか複数のインターフェースを選択するかは、状況と好みによって異なります。どちらにもpres/consがあります。

IModuleUpdate、およびDestroyメソッドでThrowインターフェースを使用することが可能です。そして、それらの操作のために「空の」バージョンの基本クラスを実装します。

interface IModule
{
    void Update();
    void Destroy();
    void Throw();
}

class ModuleBase  : IModule
{
    public virtual void Update() { }

    public virtual void Destroy() { }

    public virtual void Throw() { }
}

このようにして、モジュールは必要に応じてそれらを実装できます。

class Module1 : ModuleBase
{
    public override void Update()
    {
        // do module updating required for module1
    }
}

class Module2 : ModuleBase
{
    public override void Destroy()
    {
        // implementation
    }

    public override void Throw()
    {
        // implementation
    }
}

ところで。 pplが指摘したように。別のインターフェースを使用する2番目の方法は、意図がより明確になるため、より純粋になります。

1
Dbuggy