SignalR 1.x の依存関係挿入
投稿者: Patrick Fletcher
警告
このドキュメントは、最新版の SignalR を対象としていません。 ASP.NET Core SignalR に関する記事を参照してください。
依存関係の挿入は、オブジェクト間のハードコーディングされた依存関係を解除する方法であり、(モック オブジェクトを使用して) テストしたり、実行時の動作を変更したりするために、オブジェクトの依存関係を簡単に置き換えることができます。 このチュートリアルでは、SignalR ハブで依存関係の挿入を実行する方法について示します。 また、SignalR で IoC コンテナーを使用する方法についても示します。 IoC コンテナーは、依存関係の挿入のための一般的なフレームワークです。
依存関係の挿入とは
依存関係の挿入に既に慣れている場合は、このセクションをスキップしてください。
依存関係の挿入 (DI) とは、オブジェクトによって独自の依存関係が作成されないパターンです。 DI の動機付けとなる簡単な例を次に示します。 メッセージをログに記録する必要があるオブジェクトがあるとします。 次のようなログ インターフェイスを定義できます。
interface ILogger
{
void LogMessage(string message);
}
オブジェクトでは、ILogger
を作成してメッセージをログに記録できます。
// Without dependency injection.
class SomeComponent
{
ILogger _logger = new FileLogger(@"C:\logs\log.txt");
public void DoSomething()
{
_logger.LogMessage("DoSomething");
}
}
これは機能しますが、最適な設計ではありません。 FileLogger
を別の ILogger
実装に置き換える場合は、SomeComponent
を変更する必要があります。 他の多くのオブジェクトで FileLogger
が使用されていると仮定すると、それらすべてを変更する必要があります。 あるいは、FileLogger
をシングルトンにすると、この場合もアプリケーション全体で変更を行う必要があります。
より適切な方法は、たとえばコンストラクター引数を使用して ILogger
をオブジェクトに "挿入" することです。
// With dependency injection.
class SomeComponent
{
ILogger _logger;
// Inject ILogger into the object.
public SomeComponent(ILogger logger)
{
if (logger == null)
{
throw new NullReferenceException("logger");
}
_logger = logger;
}
public void DoSomething()
{
_logger.LogMessage("DoSomething");
}
}
これで、使用する ILogger
の選択がオブジェクトによって行われないようになりました。 実装に依存するオブジェクトを変更せずに、ILogger
実装を切り替えることができます。
var logger = new TraceLogger(@"C:\logs\log.etl");
var someComponent = new SomeComponent(logger);
このパターンはコンストラクター挿入と呼ばれます。 もう 1 つのパターンはセッター挿入です。セッター メソッドまたはプロパティを使用して依存関係を設定します。
SignalR での単純な依存関係の挿入
チュートリアル「SignalR の概要」のチャット アプリケーションについて考えてみましょう。 そのアプリケーションのハブ クラスを次に示します。
public class ChatHub : Hub
{
public void Send(string name, string message)
{
Clients.All.addMessage(name, message);
}
}
チャット メッセージを送信する前にサーバーに保存するとします。 この機能を抽象化するインターフェイスを定義し、DI を使用してインターフェイスを ChatHub
クラスに挿入できます。
public interface IChatRepository
{
void Add(string name, string message);
// Other methods not shown.
}
public class ChatHub : Hub
{
private IChatRepository _repository;
public ChatHub(IChatRepository repository)
{
_repository = repository;
}
public void Send(string name, string message)
{
_repository.Add(name, message);
Clients.All.addMessage(name, message);
}
唯一の問題は、SignalR アプリケーションによってハブが直接作成されるのではないということです。ユーザーに代わって SignalR が作成します。 既定では、SignalR では、ハブ クラスにパラメーターなしのコンストラクターが必要です。 一方、ハブ インスタンスを作成する関数を簡単に登録でき、この関数を使用して DI を実行できます。 GlobalHost.DependencyResolver.Register を呼び出して関数を登録します。
protected void Application_Start()
{
GlobalHost.DependencyResolver.Register(
typeof(ChatHub),
() => new ChatHub(new ChatMessageRepository()));
RouteTable.Routes.MapHubs();
// ...
}
これで、SignalR は ChatHub
インスタンスを作成する必要があるときに常にこの匿名関数を呼び出すようになりました。
IoC コンテナー
前のコードは、単純なケースでは問題ありません。 しかし、依然として次を記述しなければなりませんでした。
... new ChatHub(new ChatMessageRepository()) ...
依存関係が多い複雑なアプリケーションでは、この "配線" コードの多くを記述することが必要な場合があります。 このコードは、特に依存関係が入れ子になっている場合、保守が難しい場合があります。 また、単体テストも困難です。
1 つの解決策は、IoC コンテナーを使用することです。 IoC コンテナーは、依存関係を管理するソフトウェア コンポーネントです。コンテナーに型を登録し、コンテナーを使用してオブジェクトを作成します。 コンテナーは、依存関係を自動的に把握します。 多くの IoC コンテナーでは、オブジェクトの有効期間やスコープなどを制御することもできます。
Note
"IoC" とは、フレームワークがアプリケーション コードを呼び出す一般的なパターンである "Inversion of Control" (制御の反転) の略です。 IoC コンテナーによってオブジェクトが作成され、通常の制御フローが "反転" されます。
SignalR での IoC コンテナーの使用
チャット アプリケーションは、IoC コンテナーを利用するには単純すぎる可能性があります。 代わりに、StockTicker サンプルを見てみましょう。
StockTicker サンプルでは、次の 2 つのメイン クラスを定義します。
StockTickerHub
: クライアント接続を管理するハブ クラス。StockTicker
: 株価を保持し、定期的に更新するシングルトン。
StockTickerHub
は StockTicker
シングルトンへの参照を保持し、一方、StockTicker
は IHubConnectionContext の StockTickerHub
への参照を保持します。 このインターフェイスを使用して StockTickerHub
インスタンスと通信します。 (詳細については、「ASP.NET SignalR を使用したサーバー ブロードキャスト」を参照してください。)
IoC コンテナーを使用して、これらの依存関係を少し簡潔にすることができます。 まず、StockTickerHub
クラスと StockTicker
クラスを簡略化しましょう。 次のコードでは、不要な部分をコメントアウトしました。
パラメーターなしのコンストラクターを StockTicker
から削除します。 代わりに、常に DI を使用してハブを作成します。
[HubName("stockTicker")]
public class StockTickerHub : Hub
{
private readonly StockTicker _stockTicker;
//public StockTickerHub() : this(StockTicker.Instance) { }
public StockTickerHub(StockTicker stockTicker)
{
if (stockTicker == null)
{
throw new ArgumentNullException("stockTicker");
}
_stockTicker = stockTicker;
}
// ...
StockTicker の場合は、シングルトン インスタンスを削除します。 後に、IoC コンテナーを使用して StockTicker の有効期間を制御します。 また、コンストラクターをパブリックにします。
public class StockTicker
{
//private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(
// () => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
// Important! Make this constructor public.
public StockTicker(IHubConnectionContext clients)
{
if (clients == null)
{
throw new ArgumentNullException("clients");
}
Clients = clients;
LoadDefaultStocks();
}
//public static StockTicker Instance
//{
// get
// {
// return _instance.Value;
// }
//}
次に、StockTicker
のインターフェイスを作成してコードをリファクタリングできます。 このインターフェイスを使用して、StockTickerHub
を StockTicker
クラスから分離します。
Visual Studio を使用すると、この種のリファクタリングが簡単になります。 ファイル StockTicker.cs を開き、StockTicker
クラス宣言を右クリックし、[リファクター] を選択し...[インターフェイスの抽出]。
[インターフェイスの抽出] ダイアログで、[すべて選択] をクリックします。 他の既定値はそのままにします。 OK をクリックします。
Visual Studio によって、IStockTicker
という新しいインターフェイスが作成されます。また、IStockTicker
から派生するように StockTicker
が変更されます。
ファイル IStockTicker.cs を開き、インターフェイスをパブリックに変更します。
public interface IStockTicker
{
void CloseMarket();
IEnumerable<Stock> GetAllStocks();
MarketState MarketState { get; }
void OpenMarket();
void Reset();
}
StockTickerHub
クラスで、StockTicker
の 2 つのインスタンスを IStockTicker
に変更します。
[HubName("stockTicker")]
public class StockTickerHub : Hub
{
private readonly IStockTicker _stockTicker;
public StockTickerHub(IStockTicker stockTicker)
{
if (stockTicker == null)
{
throw new ArgumentNullException("stockTicker");
}
_stockTicker = stockTicker;
}
IStockTicker
インターフェイスの作成は厳密には必要ではありませんが、アプリケーション内のコンポーネント間の結合を減らすために DI がどのように役立つかを示したいと思いました。
Ninject ライブラリを追加する
.NET 用のオープン ソース IoC コンテナーは多数存在します。 このチュートリアルでは、Ninject を使用します。 (その他の一般的なライブラリには、Castle Windsor、Spring.Net、Autofac、Unity、StructureMap などがあります。)
NuGet パッケージ マネージャーを使用して、Ninject ライブラリをインストールします。 Visual Studio の [ツール] メニューで、[NuGet パッケージ マネージャー]>[パッケージ マネージャー コンソール] を選択します。 [パッケージ マネージャー コンソール] ウィンドウで、次のコマンドを入力します。
Install-Package Ninject -Version 3.0.1.10
SignalR 依存関係リゾルバーを置き換える
SignalR 内で Ninject を使用するには、DefaultDependencyResolver から派生するクラスを作成します。
internal class NinjectSignalRDependencyResolver : DefaultDependencyResolver
{
private readonly IKernel _kernel;
public NinjectSignalRDependencyResolver(IKernel kernel)
{
_kernel = kernel;
}
public override object GetService(Type serviceType)
{
return _kernel.TryGet(serviceType) ?? base.GetService(serviceType);
}
public override IEnumerable<object> GetServices(Type serviceType)
{
return _kernel.GetAll(serviceType).Concat(base.GetServices(serviceType));
}
}
このクラスは、DefaultDependencyResolver の GetService と GetServices メソッドをオーバーライドします。 SignalR は、これらのメソッドを呼び出して、ハブ インスタンスや、SignalR によって内部的に使用されるさまざまなサービスを含むさまざまなオブジェクトを実行時に作成します。
- GetService メソッドは、型の 1 つのインスタンスを作成します。 このメソッドをオーバーライドして、Ninject カーネルの TryGet メソッドを呼び出します。 そのメソッドによって null が返された場合は、既定のリゾルバーにフォールバックします。
- GetServices メソッドは、指定した型のオブジェクトのコレクションを作成します。 このメソッドをオーバーライドして、Ninject の結果を既定のリゾルバーの結果と連結します。
Ninject バインドを構成する
次に、Ninject を使用して型バインドを宣言します。
ファイル RegisterHubs.csを開きます。 RegisterHubs.Start
メソッドで、Ninject コンテナーを作成します。このコンテナーでは、Ninject によってカーネルが呼び出されます。
var kernel = new StandardKernel();
カスタム依存関係リゾルバーのインスタンスを作成します。
var resolver = new NinjectSignalRDependencyResolver(kernel);
IStockTicker
のバインドを作成します。
kernel.Bind<IStockTicker>()
.To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>() // Bind to StockTicker.
.InSingletonScope(); // Make it a singleton object.
このコードは 2 つのことを言っています。 1 つ目は、アプリケーションで IStockTicker
が必要な場合は常に、カーネルで StockTicker
のインスタンスを作成する必要があるということ。 2 つ目は、StockTicker
クラスはシングルトン オブジェクトとして作成する必要があるということです。 Ninject はオブジェクトの 1 つのインスタンスを作成し、要求ごとに同じインスタンスを返します。
IHubConnectionContext のバインドを次のように作成します。
kernel.Bind<IHubConnectionContext>().ToMethod(context =>
resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
).WhenInjectedInto<IStockTicker>();
このコードは、IHubConnection を返す匿名関数を作成します。 WhenInjectedInto メソッドは、IStockTicker
インスタンスの作成時にのみこの関数を使用するように Ninject に指示します。 その理由は、SignalR によって IHubConnectionContext インスタンスが内部的に作成されるため、SignalR での作成方法をオーバーライドしないようにするためです。 この関数は StockTicker
クラスにのみ適用されます。
依存関係リゾルバーを MapHubs メソッドに渡します。
RouteTable.Routes.MapHubs(config);
SignalR では、既定のリゾルバーではなく、MapHubsで指定されたリゾルバーが使用されます。
RegisterHubs.Start
の完成したコードは次のようになります。
public static class RegisterHubs
{
public static void Start()
{
var kernel = new StandardKernel();
var resolver = new NinjectSignalRDependencyResolver(kernel);
kernel.Bind<IStockTicker>()
.To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()
.InSingletonScope();
kernel.Bind<IHubConnectionContext>().ToMethod(context =>
resolver.Resolve<IConnectionManager>().
GetHubContext<StockTickerHub>().Clients
).WhenInjectedInto<IStockTicker>();
var config = new HubConfiguration()
{
Resolver = resolver
};
// Register the default hubs route: ~/signalr/hubs
RouteTable.Routes.MapHubs(config);
}
}
Visual Studio で StockTicker アプリケーションを実行するには、F5 キーを押します。 ブラウザー ウィンドウで、http://localhost:*port*/SignalR.Sample/StockTicker.html
に移動します。
アプリケーションの機能は以前とまったく同じです。 (説明については、「ASP.NET SignalR を使用したサーバー ブロードキャスト」を参照してください。)動作は変更されていません。変更点は、コードのテスト、保守、発展が容易になっただけです。