SignalR中的依賴注入


什么是依賴注入?

如果你已經熟悉依賴注入可以跳過此節。

依賴注入 (DI) 模式下,對象並不為自身的依賴負責。 下邊的例子是一個主動 DI. 假設你有個對象需要消息日志。你可能定義了一個日志接口:

C#
interface ILogger { void LogMessage(string message); } 

在你的對象中,你可以創建一個 ILogger來記錄消息。

C#
// 不用依賴注入。 class SomeComponent { ILogger _logger = new FileLogger(@"C:\logs\log.txt"); public void DoSomething() { _logger.LogMessage("DoSomething"); } } 

可以工作,但不是最好的設計。如果你想將FileLogger換成其它的ILogger 實現, 你就得修改 SomeComponent。假如有一堆的對象使用 FileLogger, 你就得將所有的對象都改一遍,或者學決定將 FileLogger形成單例模式,你依舊需要整個程序的修改。

更好的做法是將 ILogger i注入到對象,比如通過構造函數:

C#
// 使用依賴注入. 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 的實現而不更改依賴的哪個對象。

C#
var logger = new TraceLogger(@"C:\logs\log.etl"); var someComponent = new SomeComponent(logger); 

這個模式叫 構造函數注入. 另一種模式是設置注入,在需要的地方可以通過設置器方法或屬性來設置依賴。

SignalR中簡單依賴注入

細看一下聊天程序教程 Getting Started with SignalR. 下邊是這個程序的Hub類:

C#
public class ChatHub : Hub { public void Send(string name, string message) { Clients.All.addMessage(name, message); } } 

假設你想把聊天的信息在發送前先存下來。你可以定義一個接口來抽象這些功能,然后使用 DI 把這個接口注入到ChatHub 類中。

C#
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 應用並不直接創建hub; SignalR 會為你創建。默認情況下,SignalR 期望一個有參數的構造方法。然而你可以很容易的注冊一個函數來創建這個hub 實例,然后用這個函數來實現 DI. 調用GlobalHost.DependencyResolver.Register來注冊這個函數。

C#
public void Configuration(IAppBuilder app) { GlobalHost.DependencyResolver.Register( typeof(ChatHub), () => new ChatHub(new ChatMessageRepository())); App.MapSignalR(); // ... } 

現在SignalR就會在你需要創建 ChatHub 實例的時候來調用這個匿名函數。

IoC 容器

上邊的代碼在簡單的場合下已經不錯了,但你還是得這樣寫:

C#
... new ChatHub(new ChatMessageRepository()) ... 

在一個復雜的應用有很多的依賴,你可能要寫大量的“裝配”代碼。這個代碼很難維護,特別是嵌套的依賴。另外單元測試也很難。

有個解決方案就是使用IoC 容器。IoC容器是一個軟件組件,用於負責管理依賴。你在容器中注冊類型,然后使用容器創建對象。容器自動找出依賴關系。很多IoC容器也可以讓你控制對象的生存期及生存域等。

"IoC"代表 "控制反轉",這是框架進入程序代碼的一個常規模式。IoC容器為你構造對象,它“反轉”了常規的流程控制。

SignalR中使用IoC容器

聊天應用可能太過簡單而不能體現IoC窗口的好處。我們換一個 StockTicker 的例子來看看。

StockTicker 示例定義了兩個主要的類:

  • StockTickerHub: hub 類,管理客戶端連接。
  • StockTicker: 一個單例用於存放股票價格並定時更新。

StockTickerHub 放了一個 StockTicker 單例的引用,同時 StockTicker 放了一個 StockTickerHub的IHubConnectionContext引用。使用接口與StockTickerHub實例進行通訊。 (更多信息見: Server Broadcast with ASP.NET SignalR.)

我們用 IoC容器來解開一點依賴。首先,我們簡化StockTickerHubStockTicker類。在下邊的代碼中,我注釋了部分我們用不到的代碼。

刪除StockTickerHub沒有參數的構造器。取而代之,我們一般DI來創建hub。

C#
[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。

C#
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<dynamic> clients) { if (clients == null) { throw new ArgumentNullException("clients"); } Clients = clients; LoadDefaultStocks(); } //public static StockTicker Instance //{ // get // { // return _instance.Value; // } //} 

下一步,我們重構代碼來創建StockTicker的接口。 我們使用接口解除 StockTicker中StockTickerHub 類的耦合。

Visual Studio 做這種重構很容易,打開StockTicker.cs文件,右擊 StockTicker 類申明,然后選擇 重構 ...提取接口。

在提取接口對話框中, 點擊選中所有。其它默認,點擊確定。

Visual Studio創建了一個IStockTicker接口,同時更改 StockTicker 繼承IStockTicker.

打開IStockTicker.cs 文件,把接口申明為public.

C#
public interface IStockTicker { void CloseMarket(); IEnumerable<Stock> GetAllStocks(); MarketState MarketState { get; } void OpenMarket(); void Reset(); } 

StockTickerHub 中, 將StockTicker 的兩個實例改為 IStockTicker:

C#
[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 WindsorSpring.Net,AutofacUnity, 和StructureMap.)

使用NuGet 包管理器安裝 Ninject 庫. 在Visual Studio中, 打開工具菜單選擇庫包管理器 | 包管理器命令行。在包管理器命令行窗口,輸入以下命令:

PowerShell
Install-Package Ninject -Version 3.0.1.10 

替換SignalR 依賴處理器

要讓 Ninject 同 SignalR一起工作,創建一個類繼承於DefaultDependencyResolver。

C#
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 在運行時調用這些方法創建各種對象,包括hub 實例,以及SignalR內部的各類服務。

  • GetService創建類型的單個實例。重寫這個方法調用Ninject內核的TryGet方法。如果這個方法返回null, 則回到默認的處理器。
  • GetServices 方法創建特定類型的對象集合。重寫這個方法將Ninject的結果和默認處理器的結果聯系起來。

配置Ninject 綁定

現在我們使用 Ninject來申明類型綁定

打開應用程序的 Startup.cs 類文件(that you either created manually as per the package instructions in readme.txt, or that was created by adding authentication to your project). 在 Startup.Configuration 方法中, 創建 Ninject 容器, Ninject 叫做 kernel.

C#
var kernel = new StandardKernel(); 

創建自定義依賴處理器的實例:

C#
var resolver = new NinjectSignalRDependencyResolver(kernel); 

創建IStockTicker 的綁定,如下:

C#
kernel.Bind<IStockTicker>()
    .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()  // Bind to StockTicker. .InSingletonScope(); // Make it a singleton object. 

這個代碼說了兩件事。首先,程序什么時候需要IStockTicker, kernel需要創建一個StockTicker的實例。其次, StockTicker類需要創建為單例對象。 Ninject創建對象的一個實例,並返回每個請求相同的實例。

創建IHubConnectionContext的綁定如下:

C#
kernel.Bind(typeof(IHubConnectionContext<dynamic>)).ToMethod(context => resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients ).WhenInjectedInto<IStockTicker>(); 

代碼創建一個匿名函數返回一個 IHubConnection。WhenInjectedInto 方法告訴 Ninject只在創建IStockTicker實例的時候使用這個函數。理由是SignalR 內部創建 IHubConnectionContext實例,我們並不想重寫SignalR是如何創建他們的。這個函數只用於我們的 StockTicker 類。

增加一個hub配置將依賴處理器傳給 MapSignalR 方法:

C#
var config = new HubConfiguration(); config.Resolver = resolver; Microsoft.AspNet.SignalR.StockTicker.Startup.ConfigureSignalR(app, config); 

更新示例的Startup類的Startup.ConfigureSignalR方法參數:

C#
public static void ConfigureSignalR(IAppBuilder app, HubConfiguration config) { app.MapSignalR(config); } 

現在SignalR將會使用MapSignalR中指定的處理器,替代默認的處理器。

這里列出了 Startup.Configuration的完整代碼:

C#
public class Startup { public void Configuration(IAppBuilder app) { // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888 var kernel = new StandardKernel(); var resolver = new NinjectSignalRDependencyResolver(kernel); kernel.Bind<IStockTicker>() .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>() // Bind to StockTicker. .InSingletonScope(); // Make it a singleton object. kernel.Bind(typeof(IHubConnectionContext<dynamic>)).ToMethod(context => resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients ).WhenInjectedInto<IStockTicker>(); var config = new HubConfiguration(); config.Resolver = resolver; Microsoft.AspNet.SignalR.StockTicker.Startup.ConfigureSignalR(app, config); } } 

 在 Visual Studio中 安 F5運行StockTicker程序。在瀏覽器窗口中,導航到 http://localhost:*port*/SignalR.Sample/StockTicker.html.

這個程序的功能和前邊完全一樣。 (描述內容見: Server Broadcast with ASP.NET SignalR.) 我們沒有改變行為,只是將代碼變得容易測試、維護和進化。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM