11

概要

過去数か月間、私はAPI抽象化とエンティティ/コンポーネント/スクリプトシステムを備えた軽量のC#ベースのゲームエンジンをプログラミングしてきました。Unityエンジンと同様のアーキテクチャを提供することで、XNA、SlimDXなどのゲーム開発プロセスを容易にすることが全体的なアイデアです。

設計上の課題

ほとんどのゲーム開発者が知っているように、コード全体でアクセスする必要のあるさまざまなサービスがあります。多くの開発者は、レンダリングマネージャー(またはコンポーザー)、シーン、グラフィックスデバイス(DX)、ロガー、入力状態、ビューポート、ウィンドウなどのグローバル静的インスタンスを使用することに頼っています。グローバル静的インスタンス/シングルトンにはいくつかの代替アプローチがあります。1つは、コンストラクターまたはコンストラクター/プロパティの依存性注入(DI)を介して、アクセスする必要のあるクラスのインスタンスを各クラスに与えることです。もう1つは、StructureMapのObjectFactoryのようなグローバルサービスロケーターを使用することです。 IoCコンテナ。

依存性注入

私は多くの理由でDIの道を選ぶことにしました。最も明白なものは、インターフェースに対してプログラミングし、コンストラクターを介して提供されるすべてのクラスのすべての依存関係を持つテスト容易性です。テストコンテナーは必要なサービスまたはそれらのモックをインスタンス化し、フィードできるため、これらのクラスは簡単にテストされます。テストするすべてのクラス。DI / IoCを実行するもう1つの理由は、信じられないかもしれませんが、コードの可読性を高めるためです。すべての異なるサービスをインスタンス化し、必要なサービスへの参照を使用してクラスを手動でインスタンス化するという、これ以上の巨大な初期化プロセスはありません。Kernel(NInject)/ Registry(StructureMap)を構成すると、エンジン/ゲームの単一の構成ポイントが提供され、サービスの実装が選択されて構成されます。

私の問題

  • インターフェイスのためにインターフェイスを作成しているような気がすることがよくあります
  • 迅速で単純なグローバルな静的な方法ではなく、DIの方法で物事を行う方法を心配するだけなので、私の生産性は劇的に低下しました。
  • 場合によっては、たとえば実行時に新しいエンティティをインスタンス化するときに、インスタンスを作成するためにIoCコンテナ/カーネルにアクセスする必要があります。これにより、IoCコンテナ自体(SMのObjectFactory、Ninjectのカーネルのインスタンス)への依存関係が作成されます。これは、そもそもIoCコンテナを使用する理由に反します。これはどのように解決できますか?抽象ファクトリが思い浮かびますが、それはコードをさらに複雑にします。
  • サービス要件によっては、一部のクラスのコンストラクターが非常に大きくなる可能性があります。これにより、IoCが使用されていない他のコンテキストでは、クラスが完全に役に立たなくなります。

基本的に、DI / IoCを実行すると、生産性が大幅に低下し、場合によってはコードとアーキテクチャがさらに複雑になります。したがって、それが私がたどるべき道なのか、それとも単に諦めて昔ながらのやり方で物事を行うのか、私にはわかりません。私は何をすべきか、すべきでないかを示す単一の答えを探していませんが、グローバルな静的/シングルトンの方法を使用するのではなく、DIを使用する価値があるかどうかについての議論、私が見落としている可能性のある長所と短所、 DIを扱うとき、上記の私の問題に対する可能な解決策。

4

1 に答える 1

20

昔ながらのやり方に戻るべきですか?要するに私の答えはノーです。あなたが述べたすべての理由から、DIには多くの利点があります。

インターフェイスのためにインターフェイスを作成しているような気がすることがよくあります

これを行っている場合は、 再利用された抽象化の原則(RAP)に違反している可能性があります

サービス要件によっては、一部のクラスのコンストラクターが非常に大きくなる可能性があります。これにより、IoCが使用されていない他のコンテキストでは、クラスが完全に役に立たなくなります。

クラスコンストラクターが大きすぎて複雑すぎる場合、これは、非常に重要な他の原則である 単一責任の原則に違反していることを示すための最良の方法です。この場合、コードを抽出してさまざまなクラスにリファクタリングするときが来ました。提案される依存関係の数は約4です。

DIを実行するために、インターフェースを用意する必要はありません。DIは、依存関係をオブジェクトに取り込む方法にすぎません。インターフェースの作成は、テスト目的で依存関係を置き換えることができるようにするために必要な方法かもしれません。依存関係の対象が次の場合を除きます。

  1. 分離しやすい
  2. 外部サブシステム(ファイルシステムなど)と通信しません

依存関係は、抽象クラス、または置換するメソッドが仮想である任意のクラスとして作成できます。ただし、インターフェースは、依存関係の最良の分離された方法を作成します。

場合によっては、たとえば実行時に新しいエンティティをインスタンス化するときに、インスタンスを作成するためにIoCコンテナ/カーネルにアクセスする必要があります。これにより、IoCコンテナ自体(SMのObjectFactory、Ninjectのカーネルのインスタンス)への依存関係が作成されます。これは、そもそもIoCコンテナを使用する理由に反します。これはどのように解決できますか?抽象ファクトリが思い浮かびますが、それはコードをさらに複雑にします。

IOCコンテナへの依存関係に関しては、クライアントクラスでIOCコンテナへの依存関係を持つべきではありません。そして、彼らはそうする必要はありません。

最初に依存性注入を適切に使用するには、CompositionRootの概念を理解する必要があります。これは、コンテナを参照する必要がある唯一の場所です。この時点で、オブジェクトグラフ全体が作成されます。これを理解すると、クライアントにコンテナが必要ないことに気付くでしょう。各クライアントは依存性を注入するだけです。

構築を容易にするために従うことができる他の多くの作成パターンもあります。次のような多くの依存関係を持つオブジェクトを構築したいとします。

new SomeBusinessObject(
    new SomethingChangedNotificationService(new EmailErrorHandler()),
    new EmailErrorHandler(),
    new MyDao(new EmailErrorHandler()));

これを構築する方法を知っている具体的なファクトリを作成できます。

public static class SomeBusinessObjectFactory
{
    public static SomeBusinessObject Create()
    {
        return new SomeBusinessObject(
            new SomethingChangedNotificationService(new EmailErrorHandler()),
            new EmailErrorHandler(),
            new MyDao(new EmailErrorHandler()));
    }
}

そして、次のように使用します。

 SomeBusinessObject bo = SomeBusinessObjectFactory.Create();

また、poor mans diを使用して、引数をまったくとらないコンストラクターを作成することもできます。

public SomeBusinessObject()
{
    var errorHandler = new EmailErrorHandler();
    var dao = new MyDao(errorHandler);
    var notificationService = new SomethingChangedNotificationService(errorHandler);
    Initialize(notificationService, errorHandler, dao);
}

protected void Initialize(
    INotificationService notifcationService,
    IErrorHandler errorHandler,
    MyDao dao)
{
    this._NotificationService = notifcationService;
    this._ErrorHandler = errorHandler;
    this._Dao = dao;
}

それからそれはちょうどそれが働いていたように見えます:

SomeBusinessObject bo = new SomeBusinessObject();

Poor ManのDIを使用することは、デフォルトの実装が外部のサードパーティライブラリにある場合は悪いと見なされますが、適切なデフォルトの実装がある場合はそれほど悪くありません。

そして、明らかに、すべてのDIコンテナー、オブジェクトビルダー、およびその他のパタ​​ーンがあります。

したがって、必要なのは、オブジェクトの優れた作成パターンを考えることだけです。オブジェクト自体は、依存関係の作成方法を気にする必要はありません。実際、依存関係がより複雑になり、2種類のロジックが混在する原因になります。したがって、DIを使用すると生産性が低下するはずだとは思いません。

オブジェクトに単一のインスタンスを注入できない特殊なケースがいくつかあります。一般的にライフタイムが短く、オンザフライのインスタンスが必要な場合。この場合、ファクトリを依存関係としてオブジェクトに注入する必要があります。

public interface IDataAccessFactory
{
    TDao Create<TDao>();
}

お気づきのように、このバージョンはIoCコンテナーを使用してさまざまなタイプを作成できるため、汎用的です(IoCコンテナーはまだクライアントに表示されないことに注意してください)。

public class ConcreteDataAccessFactory : IDataAccessFactory
{
    private readonly IocContainer _Container;

    public ConcreteDataAccessFactory(IocContainer container)
    {
        this._Container = container;
    }

    public TDao Create<TDao>()
    {
        return (TDao)Activator.CreateInstance(typeof(TDao),
            this._Container.Resolve<Dependency1>(), 
            this._Container.Resolve<Dependency2>())
    }
}

Iocコンテナーを持っていてもアクティベーターを使用したことに注意してください。これは、オブジェクトが異なるライフタイムで登録される可能性があるため、コンテナーが新しいインスタンスを提供すると想定するだけでなく、ファクトリがオブジェクトの新しいインスタンスを構築する必要があることに注意することが重要です(シングルトン、ThreadLocalなど)。ただし、使用しているコンテナによっては、これらのファクトリを生成できるものもあります。ただし、オブジェクトが一時的な有効期間に登録されていることが確実な場合は、単純に解決できます。

編集:Abstract Factory依存関係を持つクラスの追加:

public class SomeOtherBusinessObject
{
    private IDataAccessFactory _DataAccessFactory;

    public SomeOtherBusinessObject(
        IDataAccessFactory dataAccessFactory,
        INotificationService notifcationService,
        IErrorHandler errorHandler)
    {
        this._DataAccessFactory = dataAccessFactory;
    }

    public void DoSomething()
    {
        for (int i = 0; i < 10; i++)
        {
            using (var dao = this._DataAccessFactory.Create<MyDao>())
            {
                // work with dao
                // Console.WriteLine(
                //     "Working with dao: " + dao.GetHashCode().ToString());
            }
        }
    }
}

基本的に、DI / IoCを実行すると、生産性が大幅に低下し、場合によってはコードとアーキテクチャがさらに複雑になります。

Mark Seemanはこのテーマについて素晴らしいブログを書き、質問に答えました。その種の質問に対する私の最初の反応は、緩く結合されたコードは理解しにくいと言うことです。何より難しい?

緩い結合と全体像

編集:最後に、すべてのオブジェクトと依存性が依存性注入を必要とする、または注入する必要があるわけではないことを指摘したいと思います。まず、使用しているものが実際に依存性と見なされるかどうかを検討します。

依存関係とは何ですか?

  • アプリケーション構成
  • システムリソース(時計)
  • サードパーティのライブラリ
  • データベース
  • WCF/ネットワークサービス
  • 外部システム(ファイル/メール)

上記のオブジェクトまたは共同作業者は、制御不能になり、副作用や動作の違いを引き起こし、テストを困難にする可能性があります。これらは、抽象化(クラス/インターフェース)を検討し、DIを使用するときです。

依存関係ではないもの、本当にDIを必要としないものは何ですか?

  • List<T>
  • MemoryStream
  • 文字列/プリミティブ
  • リーフオブジェクト/Dto

new上記のようなオブジェクトは、キーワードを使用して必要に応じて簡単にインスタンス化できます。特に理由がない限り、このような単純なオブジェクトにDIを使用することはお勧めしません。オブジェクトが完全に制御されており、動作に追加のオブジェクトグラフや副作用(少なくとも、動作を変更/制御したいもの、またはテストしたいもの)が発生しないかどうかという質問を検討してください。この場合、単にそれらを新しくします。

Mark Seemanの投稿へのリンクをたくさん投稿しましたが、彼の本やブログの投稿を読むことを強くお勧めします。

于 2012-04-25T11:33:04.983 に答える