一個單例是沒有公共構造函數的,只能通過靜態的 Instance 屬性獲取,這是單例的標准初衷,一個單例是不想讓別人調用它的構造函數的。但是 aspnetcore 中提供的 AddSingleton<TService, TImplementation>() ,只提供了類型,而無法注入對象實例,單實例對象還是要框架深層構造的,這實際上並不是安全的做法。
如果使用了標准的單例設計方法,則無法由框架直接生成單實例,這就需要使用點小技巧了。
單例設計
public class Singleton<T> where T : class { protected static T instance; protected static object locker = new object(); public static T Instance { get { if (instance == null) { lock (locker) { if (instance == null) //防止線程重入 { IEnumerable<ConstructorInfo> constructors = typeof(T).GetTypeInfo().DeclaredConstructors; ConstructorInfo ci = constructors.ToList()[0]; instance = ci.Invoke(new object[] { }) as T; } } } return instance; } } protected Singleton() { IEnumerable<ConstructorInfo> constructors = typeof(T).GetTypeInfo().DeclaredConstructors; constructors.ActionForeach(c => { if (c.IsPublic) { throw new InvalidOperationException("禁止從public構造函數中實例化!"); } }); } }
以上要點有三個:
1 使用次 if 判斷和 locker 保護,防止線程重入時構造多個實例,確保唯一性;
2 在基類中,使用反射調用子類的構造函數完成實例化;
3 基類的構造函數是受保護的,它會檢查,禁止子類的公共構造函數調用。
第3條隱藏了一個知識點:子類在初始化實例時,默認會調用基類的構造函數。
因為以上三條機制確保了單例的唯一性,所以反射只會在第一次使用時調用,對性能的影響可以忽略不計。
單例的使用
使用起來非常簡單
public class Root:Singleton<Root> { protected Root() { } public void DoSomething() { Console.WriteLine("I feel very happy, cus I'm unique.");
}
}
假設有一個類,叫做Root,由於業務需要,它必須要以單例實現,顧名思義,根只能有一個。
如果不重寫 protected 構造函數,則可能發生以下情況:
Root root = new Root();
這是被禁止的,萬一忘記寫了 protected Root(){} 這一行,就會拋出異常,可見在 Singleton<T> 中進行判斷,是十分有必要的。
在業務需要的地方,就可以用通常使用的單例模式來調用 :
Root.Instance.DoSomething();
至此,單例模式完成。
一個真實的業務場景
假設主模塊是 Main.dll, 它是一個 aspnetcore 工程, 它調用了 A.dll 作為它的類庫。由於某種原因,Root 類必須要在 Main 工程中實現,而不能放到 A 工程中。但是A工程要用到 Root 的方法。如果讓 A 工程來引用 Main 工程,這就是反向引用了,這會形成循環引用,是不被允許的。
所以我們可以把 Root 的方法抽象出接口來,注冊到 aspnet 框架中,我們可以這樣做:
在 A.csproj 中,暴露接口給自己調用:
public interface IRoot { void DoSomething(); }
在 Main.csproj 中實現這個接口:
class Root:Singleton<Root>, IRoot { protected Root() { } public void DoSomething() { Console.WriteLine("I feel very happy, cus I'm unique."); } }
然后就是注冊了,在 Main.csproj 工程的 Startup.cs 文件的 ConfigureServices 方法中進行注冊:
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddSession(); //...其他代碼
services.AddSingleton<IRoot, Root>(); }
這樣就可以了嗎? no no no , 這樣肯定是不行的,因為前面我們設計的單例,是這樣使用的: Root.Instance.DoSomething(); 而不是 Root root = new Root();
這會導致注入失敗,因為框架注冊要求 Root 有一個公共無參構造函數,況且它並不知道Root有個靜態屬性 Instance ,而且只能通過 Instance 來訪問。
單例注入方案
下面說到正題了,既然不能直接注冊單例,我們可以使用一個中間接口來注入,這個中間接口提供了單例的訪問對象,而且它擁有一個沒有寫出來的默認公共構造方法,它的構造函數,與 Root 類的構造函數,毫無關系,所以可以由框架創建。
在 A.csproj 工程中定義:
public interface IRootProvider { IRoot Root { get; } }
在 Main.csproj 中實現:
public class RootProvider : IRootProvider { public IRoot Root { get => SomeNamespace.Root.Instance; } }
然后再注冊:
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddSession(); //...其他代碼
services.AddScoped<IRootProvider, RootProvider>();
services.AddScoped<ISomeService, SomeService>();
}
look, 我們已經不需要使用 AddSingleton 來注入了,因為 RootProvider 不必是單實例的。
在 A.csproj 中愉快地使用:
public class SomeService:ISomeService { private IRoot root; public SomeService(IRootProvider rootProvider) { this.root = rootProvider.Root; } public void SomeBusiness() { this.root.DoSomething(); } }
SomeService 是在主模塊中注入的服務,在主模塊中構造,構造的前提是要有一個 IRootProvider 的實例,同時 IRootProvider 要在它的前一行注冊,這個很重要。
到這里,本文就結束了,但我還是想啰嗦一下:
在A中使用的 IRoot root 一點也看不出單例的痕跡,因為 IRoot 只是一個業務接口;同時 IRootProvider 也只提供了 IRoot 的 get 方法, 所以對於 A 模塊的開發者,完全不必知道 Root 的存在,更不必知道什么 Singleton<Root> 跟 Instance 的破事。
我們已經完全隱藏了單例模式的實現,這是解決這個問題附帶的收獲。
這個設計是不是徹底實現了 面向接口編程 的規范? 快誇我吧!