web-dev-qa-db-ja.com

疎結合と依存関係のシャッフル

次のようなクラスがたくさんあります。

public class MyGame()
{
    private Graphics graphics;

    private Player player;

    public MyGame()
    {
        graphics = new Graphics();
        //...
    }

    protected void Initialize()
    {
        player = new Player();
        //...
    }

    protected void Update()
    {
        player.DoStuff();
        //...
    }

私はデザインパターンについて読んでいて、疎結合されていないため、これはあまり良いデザインではないことを理解しました。インターフェースにコーディングする代わりに、実装(グラフィックス、プレーヤーなど)にコーディングしています。

この認識されている問題を修正するために、まずこれらの依存関係をインターフェイスに変更します。

public class MyGame()
{
    private IGraphics graphics;

    private IPlayer player;

    public MyGame()
    {
        graphics = new Graphics();
        //...
    }

    protected void Initialize()
    {
        player = new Player();
        //...
    }

実際にどれほど役立つかはわかりませんが、たとえばグラフィックスフィールドがGraphicsクラスに関連付けられていないのは素晴らしいことです。これらのクラスがIGraphicsを実装している限り、代わりにAwesomeGraphicsクラスまたはSparklyGraphicsクラスを使用できます。

しかし、私はまだグラフィックスとプレーヤーに強い依存関係があるので、すべてのリファクタリングのポイントは何でしたか? (私の実際のコードははるかに複雑です。)また、依存関係の注入が良いアイデアであることを理解するようになりました。

public class MyGame()
{
    private IGraphics graphics;

    private IPlayer player;

    public MyGame( IGraphics graphics, IPlayer player )
    {
        this.graphics = graphics;
        this.player = player;
        //...
    }

MyGameにハードな依存関係がなくなったので、これは本当に素敵に見えます。ただし、依存関係はまだ私のコードにあります-それらはMyGameをインスタンス化するクラスに移動されました:

static class Program
{
    static void Main( string[] args )
    {
        using ( var game = new Game( new Graphics(), new Player() )
        {
            game.Run();
        }
    }
}

今、私は私が始めたところに戻ってきました!このクラスは、疎結合ではありません!それで、再び、すべてのリファクタリングのポイントは何でしたか? IoCコンテナーを使用してこれらの依存関係を何らかの方法で処理できると確信していますが、すでに複雑なプロジェクトにかなりの複雑さを追加するだけです。さらに、それらの依存関係をもう一度別のクラスに移動するだけではありませんか?

そもそも自分が持っていたものに固執すべきでしょうか、それともまだ目に見えていない具体的なメリットはありますか?

7
Big McLargeHuge

リファクタリングで疎結合の利点を得ました。正確な実装の知識をMyGameクラスの外に移動し、より高いレベルのクラスに移動しました。これは良いことです。正確な実装が何であるかに関するすべての知識を維持することにより、コードの構造を改善し、高レベルの変更を簡単に行えるようにし、たとえば、次のような変更を簡単に行うことができます。 Playerクラスの複数の実装を使用する(おそらくNetworkPlayerまたはAIPlayerを後で導入することにより)。 MyGameを変更する理由が1つ減ったため、単一の責任を持つことに近づき、単一の責任の原則は、多くのプログラマーによってオブジェクト指向設計で最も重要なものの1つと見なされるようになりました。

6
Jules

最終的に、コードで指定されているか、DIフレームワーク構成ファイルの種類で指定されているかにかかわらず、すべてのコードは具体的な実装に依存する必要があります。インターフェースに依存することの利点は、状況に応じてmultiple具体的な実装に依存しやすくなることです。

ただし、これらの利点は、実際に複数の具体的な実装を行うまで実際には実現されません。最も頻繁に、2番目の具体的な実装は、単体テストで使用されるモックオブジェクトです。また、複数のターゲットプラットフォームのようなものもあります。たとえば、UIのインターフェイスに依存するプロジェクトがあるので、SwingまたはAndroidのどちらかを具体的な実装に置き換えることができます。複数のデータベースまたはオペレーティングシステム固有の機能のインターフェイスを持つことは一般的です。

その2番目の実装がない場合、実際には具体的なメリットは得られません。 2番目の実装が必要になった場合に、統合が容易になるという将来の保証があります。それは非配当株を所有しているようなものです。紙の上で何か価値がありますが、実際に販売するまで、実際の利益は得られません。

2
Karl Bielefeldt

programレベルでは、IGraphicsIPlayerの具体的な実装をGame。これがここに表示されているものです。

作業のポイントは、決定をGame自体から外したため、特定の実装に密接に結合されておらず、より高いレベルのプログラムコードに任せて決定することです。

つまり、Gameの方がはるかに柔軟であり(IGraphicsIPlayerと同様)、プログラムは1つの柔軟性のない階層ユニットではなく、自由に構成可能なユニットで構成されています。

そのような単純な工夫された例では大したことではないように思えるかもしれませんが、少しでも複雑になったら、構成可能性ははるかに有益になります。

2
Eric King

あなたは確かに構造を変えることからかなりの利益を得ました。

新しいシステムまたはかなり安定したシステムをコーディングする場合、設計パターンを実装することは、現在の生活を楽にしていないため、労力と時間の無駄のように思えるかもしれません。

元々使用していたGraphicsオブジェクトは、2Dグラフィックスをレンダリングするために開発されたものであり、開発を容易にするためにあらゆる種類のロジックを隠蔽していると考えてください。ゲームの後で成長し、3Dグラフィックの実行を開始する必要があります。通常は、グラフィックスから継承することから始めます。2Dグラフィックスのみですが、Render()、Refresh()、またはInvalidate()などのいくつかの類似のメソッドには何もありません。 3Dグラフィックスで行うこと。

グラフィック用のいくつかのSIMPLEインターフェイスを開くことにより、コードの柔軟性を大幅に高めることができます。

戦略パターン(依存関係の逆転)およびインターフェースへのコーディングの例:

MyGameが機能するには、いくつかのものが必要です。

Render()、Refresh()、Invalidate()が必要です。 (IGraphics)Transform()、Rotate()などの追加の3Dメソッドが必要です。 (IGraphicsManipulations3D)

そして、それらの複数のインターフェースを一緒にまとめて何かにする意味がある場合は、組み合わせたインターフェースを作成できます

public interface IGraphics3D : IGraphics, IGraphicsManipulations3D

したがって、SPECIFICグラフィックスの実装が必要であると言う代わりに、基本的なことを実行するオブジェクトが必要です。必要に応じて、完全に異なるレンダリングエンジンでスワップすることもできます。

public class UnityEngineGraphics : IGraphics3D

または

public class HavokEngineGraphics : IGraphics3D

しかし、あなたのゲームは少しも気にしません。

public class MyGame(IGraphics3D, IPlayer)

注意すべき重要なこと、そして価値を確認するのが難しいと思われる場合は、これらの状況がより早くではなく後で発生する傾向があり、MyGameを構築するときに、グラフィックスオブジェクトの1つの実装の使用に完全に専念していることです。要件が変化することを忘れないでください。それは、場合によっては、場合によっては頭痛の種であり、デザインパターンが先制になる方法です。

2
Ovan Crone

この例では:

public MyGame( IGraphics graphics, IPlayer player )

ステートメント:

ただし、依存関係はまだ私のコードにあります

不正解です。クラスは実装ではなくコントラクトに関連付けられているため、依存関係はありません。

場合によっては、疎結合である必要はありません。たとえば、プレーヤクラスは、プロパティまたはコアロジックのみが含まれている場合、疎結合の候補として適していない場合があります。

より良い例を見てみましょう。たとえば、ゲームでサードパーティのランキングエンジンを使用してプレーヤーをランク付けするとします。

したがって、コンストラクタは次のようになります。

public MyGame( IRankingProvider Provider )

ランキングプロバイダーのさまざまな実装を簡単に入れ替えることができるようになりました。コードは、ゲームが実行されているプラ​​ットフォームに応じて、3つまたは4つの実装を持つ場合があります。

また、ユニットテストでは、外部のランキングプロバイダーに依存せずにMyGameのロジックをユニットテストするための偽のプロバイダーを作成できます。これにより、プロバイダーをインストール/セットアップすることなく、開発/ビルドマシンですべての単体テストを簡単に実行できます。

最終的に開発/配備サーバーで、実際のプロバイダーを使用して統合テストを実行し、実装が正しいことを確認する必要があります。その場合、テストフィクスチャが実際の実装を注入します。

したがって、主な利点は次のとおりです。

  1. テスト性の向上
  2. 実装を切り替える、または複数の実装を使用する機能
  3. 依存関係の削除

システムのどの部分が疎結合であるかを注意深く選択する必要があります。場合によっては不要であり、採用すると不必要な抽象化レイヤーが発生します。

1
Jon Raynor