aspnetcore 注冊單例方案


一個單例是沒有公共構造函數的,只能通過靜態的 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 的破事。

我們已經完全隱藏了單例模式的實現,這是解決這個問題附帶的收獲。

這個設計是不是徹底實現了 面向接口編程 的規范? 快誇我吧!

 


免責聲明!

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



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