如何編寫一個簡單的依賴註入容器

来源:http://www.cnblogs.com/zkweb/archive/2016/09/13/5867820.html
-Advertisement-
Play Games

隨著大規模的項目越來越多,許多項目都引入了依賴註入框架,其中最流行的有Castle Windsor, Autofac和Unity Container。 微軟在最新版的Asp.Net Core中自帶了依賴註入的功能,有興趣可以 "查看這裡" 。 關於什麼是依賴註入容器網上已經有很多的文章介紹,這裡我將 ...


隨著大規模的項目越來越多,許多項目都引入了依賴註入框架,其中最流行的有Castle Windsor, Autofac和Unity Container。
微軟在最新版的Asp.Net Core中自帶了依賴註入的功能,有興趣可以查看這裡
關於什麼是依賴註入容器網上已經有很多的文章介紹,這裡我將重點講述如何實現一個自己的容器,可以幫助你理解依賴註入的原理。

容器的構想

在編寫容器之前,應該先想好這個容器如何使用。
容器允許註冊服務和實現類型,允許從服務類型得出服務的實例,它的使用代碼應該像

var container = new Container();

container.Register<MyLogger, ILogger>();

var logger = container.Resolve<ILogger>();

最基礎的容器

在上面的構想中,Container類有兩個函數,一個是Register,一個是Resolve
容器需要在Register時關聯ILogger介面到MyLogger實現,並且需要在Resolve時知道應該為ILogger生成MyLogger的實例。
以下是實現這兩個函數最基礎的代碼

public class Container
{
    // service => implementation
    private IDictionary<Type, Type> TypeMapping { get; set; }

    public Container()
    {
        TypeMapping = new Dictionary<Type, Type>();
    }

    public void Register<TImplementation, TService>()
        where TImplementation : TService
    {
        TypeMapping[typeof(TService)] = typeof(TImplementation);
    }

    public TService Resolve<TService>()
    {
        var implementationType = TypeMapping[typeof(TService)];
        return (TService)Activator.CreateInstance(implementationType);
    }
}

Container在內部創建了一個服務類型(介面類型)到實現類型的索引,Resolve時使用索引找到實現類型並創建實例。
這個實現很簡單,但是有很多問題,例如

  • 一個服務類型不能對應多個實現類型
  • 沒有對實例進行生命周期管理
  • 沒有實現構造函數註入

改進容器的構想 - 類型索引類型

要讓一個服務類型對應多個實現類型,可以把TypeMapping改為

IDictionary<Type, IList<Type>> TypeMapping { get; set; }

如果另外提供一個保存實例的變數,也能實現生命周期管理,但顯得稍微複雜了。
這裡可以轉換一下思路,把{服務類型=>實現類型}改為{服務類型=>工廠函數},讓生命周期的管理在工廠函數中實現。

IDictionary<Type, IList<Func<object>>> Factories { get; set; }

有時候我們會想讓用戶在配置文件中切換實現類型,這時如果把鍵類型改成服務類型+字元串,實現起來會簡單很多。
Resolve可以這樣用: Resolve<Service>(serviceKey: Configuration["ImplementationName"])

IDictionary<Tuple<Type, string>, IList<Func<object>>> Factories { get; set; }

改進容器的構想 - Register和Resolve的處理

在確定了索引類型後,RegisterResolve的處理都應該隨之改變。
Register註冊時應該首先根據實現類型生成工廠函數,再把工廠函數加到服務類型對應的列表中。
Resolve解決時應該根據服務類型找到工廠函數,然後執行工廠函數返回實例。

改進後的容器

這個容器新增了一個ResolveMany函數,用於解決多個實例。
另外還用了Expression.Lambda編譯工廠函數,生成效率會比Activator.CreateInstance快數十倍。

public class Container
{
    private IDictionary<Tuple<Type, string>, IList<Func<object>>> Factories { get; set; }

    public Container()
    {
        Factories = new Dictionary<Tuple<Type, string>, IList<Func<object>>>();
    }

    public void Register<TImplementation, TService>(string serviceKey = null)
        where TImplementation : TService
    {
        var key = Tuple.Create(typeof(TService), serviceKey);
        IList<Func<object>> factories;
        if (!Factories.TryGetValue(key, out factories))
        {
            factories = new List<Func<object>>();
            Factories[key] = factories;
        }
        var factory = Expression.Lambda<Func<object>>(Expression.New(typeof(TImplementation))).Compile();
        factories.Add(factory);
    }

    public TService Resolve<TService>(string serviceKey = null)
    {
        var key = Tuple.Create(typeof(TService), serviceKey);
        var factory = Factories[key].Single();
        return (TService)factory();
    }

    public IEnumerable<TService> ResolveMany<TService>(string serviceKey = null)
    {
        var key = Tuple.Create(typeof(TService), serviceKey);
        IList<Func<object>> factories;
        if (!Factories.TryGetValue(key, out factories))
        {
            yield break;
        }
        foreach (var factory in factories)
        {
            yield return (TService)factory();
        }
    }
}

改進後的容器仍然有以下的問題

  • 沒有對實例進行生命周期管理
  • 沒有實現構造函數註入

實現實例的單例

以下麵代碼為例

var logger_a = container.Resolve<ILogger>();
var logger_b = container.Resolve<ILogger>();

使用上面的容器執行這段代碼時,logger_alogger_b是兩個不同的對象,如果想要每次Resolve都返回同樣的對象呢?
我們可以對工廠函數進行包裝,藉助閉包(Closure)的力量可以非常簡單的實現。

private Func<object> WrapFactory(Func<object> originalFactory, bool singleton)
{
    if (!singleton)
        return originalFactory;
    object value = null;
    return () =>
    {
        if (value == null)
            value = originalFactory();
        return value;
    };
}

添加這個函數後在Register中調用factory = WrapFactory(factory, singleton);即可。
完整代碼將在後面放出,接下來再看如何實現構造函數註入。

實現構造函數註入

以下麵代碼為例

public class MyLogWriter : ILogWriter
{
    public void Write(string str)
    {
        Console.WriteLine(str);
    }
}

public class MyLogger : ILogger
{
    ILogWriter _writer;
    
    public MyLogger(ILogWriter writer)
    {
        _writer = writer;
    }
    
    public void Log(string message)
    {
        _writer.Write("[ Log ] " + message);
    }
}

static void Main(string[] args)
{
    var container = new Container();
    container.Register<MyLogWriter, ILogWriter>();
    container.Register<MyLogger, ILogger>();
    
    var logger = container.Resolve<ILogger>();
    logger.Log("Example Message");
}

在這段代碼中,MyLogger構造時需要一個ILogWriter的實例,但是這個實例我們不能直接傳給它。
這樣就要求容器可以自動生成ILogWriter的實例,再傳給MyLogger以生成MyLogger的實例。
要實現這個功能需要使用c#中的反射機制。

把上面代碼中的

var factory = Expression.Lambda<Func<object>>(Expression.New(typeof(TImplementation))).Compile();

換成

private Func<object> BuildFactory(Type type)
{
    // 獲取類型的構造函數
    var constructor = type.GetConstructors().FirstOrDefault();
    // 生成構造函數中的每個參數的表達式
    var argumentExpressions = new List<Expression>();
    foreach (var parameter in constructor.GetParameters())
    {
        var parameterType = parameter.ParameterType;
        if (parameterType.IsGenericType &&
            parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
        {
            // 等於調用this.ResolveMany<TParameter>();
            argumentExpressions.Add(Expression.Call(
                Expression.Constant(this), "ResolveMany",
                parameterType.GetGenericArguments(),
                Expression.Constant(null, typeof(string))));
        }
        else
        {
            // 等於調用this.Resolve<TParameter>();
            argumentExpressions.Add(Expression.Call(
                Expression.Constant(this), "Resolve",
                new [] { parameterType },
                Expression.Constant(null, typeof(string))));
        }
    }
    // 構建new表達式並編譯到委托
    var newExpression = Expression.New(constructor, argumentExpressions);
    return Expression.Lambda<Func<object>>(newExpression).Compile();
}

這段代碼通過反射獲取了構造函數中的所有參數,並對每個參數使用ResolveResolveMany解決。
值得註意的是參數的解決是延遲的,只有在構建MyLogger的時候才會構建MyLogWriter,這樣做的好處是註入的實例不一定需要是單例。
用表達式構建的工廠函數解決的時候的性能會很高。

完整代碼

容器和示例的完整代碼如下

public interface ILogWriter
{
    void Write(string text);
}

public class MyLogWriter : ILogWriter
{
    public void Write(string str)
    {
        Console.WriteLine(str);
    }
}

public interface ILogger
{
    void Log(string message);
}

public class MyLogger : ILogger
{
    ILogWriter _writer;

    public MyLogger(ILogWriter writer)
    {
        _writer = writer;
    }

    public void Log(string message)
    {
        _writer.Write("[ Log ] " + message);
    }
}

static void Main(string[] args)
{
    var container = new Container();
    container.Register<MyLogWriter, ILogWriter>();
    container.Register<MyLogger, ILogger>();
    var logger = container.Resolve<ILogger>();
    logger.Log("asdasdas");
}

public class Container
{
    private IDictionary<Tuple<Type, string>, IList<Func<object>>> Factories { get; set; }

    public Container()
    {
        Factories = new Dictionary<Tuple<Type, string>, IList<Func<object>>>();
    }

    private Func<object> WrapFactory(Func<object> originalFactory, bool singleton)
    {
        if (!singleton)
            return originalFactory;
        object value = null;
        return () =>
        {
            if (value == null)
                value = originalFactory();
            return value;
        };
    }

    private Func<object> BuildFactory(Type type)
    {
        // 獲取類型的構造函數
        var constructor = type.GetConstructors().FirstOrDefault();
        // 生成構造函數中的每個參數的表達式
        var argumentExpressions = new List<Expression>();
        foreach (var parameter in constructor.GetParameters())
        {
            var parameterType = parameter.ParameterType;
            if (parameterType.IsGenericType &&
                parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
            {
                // 等於調用this.ResolveMany<TParameter>();
                argumentExpressions.Add(Expression.Call(
                    Expression.Constant(this), "ResolveMany",
                    parameterType.GetGenericArguments(),
                    Expression.Constant(null, typeof(string))));
            }
            else
            {
                // 等於調用this.Resolve<TParameter>();
                argumentExpressions.Add(Expression.Call(
                    Expression.Constant(this), "Resolve",
                    new [] { parameterType },
                    Expression.Constant(null, typeof(string))));
            }
        }
        // 構建new表達式並編譯到委托
        var newExpression = Expression.New(constructor, argumentExpressions);
        return Expression.Lambda<Func<object>>(newExpression).Compile();
    }

    public void Register<TImplementation, TService>(string serviceKey = null, bool singleton = false)
        where TImplementation : TService
    {
        var key = Tuple.Create(typeof(TService), serviceKey);
        IList<Func<object>> factories;
        if (!Factories.TryGetValue(key, out factories))
        {
            factories = new List<Func<object>>();
            Factories[key] = factories;
        }
        var factory = BuildFactory(typeof(TImplementation));
        WrapFactory(factory, singleton);
        factories.Add(factory);
    }

    public TService Resolve<TService>(string serviceKey = null)
    {
        var key = Tuple.Create(typeof(TService), serviceKey);
        var factory = Factories[key].Single();
        return (TService)factory();
    }

    public IEnumerable<TService> ResolveMany<TService>(string serviceKey = null)
    {
        var key = Tuple.Create(typeof(TService), serviceKey);
        IList<Func<object>> factories;
        if (!Factories.TryGetValue(key, out factories))
        {
            yield break;
        }
        foreach (var factory in factories)
        {
            yield return (TService)factory();
        }
    }
}

寫在最後

這個容器實現了一個依賴註入容器應該有的主要功能,但是還是有很多不足的地方,例如

  • 不支持線程安全
  • 不支持非泛型的註冊和解決
  • 不支持只用於指定範圍內的單例
  • 不支持成員註入
  • 不支持動態代理實現AOP

我在ZKWeb網頁框架中也使用了自己編寫的容器,只有300多行但是可以滿足實際項目的使用。
完整的源代碼可以查看這裡和這裡

微軟從.Net Core開始提供了DependencyInjection的抽象介面,這為依賴註入提供了一個標準。
在將來可能不會再需要學習Castle Windsor, Autofac等,而是直接使用微軟提供的標準介面。
雖然具體的實現方式離我們原來越遠,但是瞭解一下它們的原理總是有好處的。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 ASP.NET 是一個免費的Web開發框架,是由微軟在.NET Framework框架中所提供的,或者說ASP.NET是開發Web應用程式的類庫,封裝在System.Web.dll 文件中。ASP.NET 並不是一種編程語言,它實際上只是一個由 .NET Framework 提供的一種開發平臺 ...
  • 方法: 調用方法 擴展 非原創,原作忘記地址,我是給自己看的 ...
  • 直接使用yield return關鍵字通過類似返回值的方式靈活地構造迭代器 public class EmployeeCollection :IEnumerable<Employee> { private Employee[] employees; #region IEnumerable<Emplo ...
  • 看的代碼越多,寫的代碼越多,就越是享受這些字元,終於漸漸懂得了那種傳聞中的成就感,特別是自己從看不懂然後一步一步學,一個代碼一個代碼地敲,最後哪怕只是完成了一個小功能,也都是特別自豪的!這種自豪不用告訴別人,自己心裡就是特別滿足! 代碼最美的地方就在於所有的不可能都是有可能的...... 顯示的功能 ...
  • String和string的區別 從位置講: 1.String是.NET Framework裡面的String,小寫的string是C#語言中的string 2.如果把using System;刪掉,沒有大寫的String了,System是.NET Framework類庫中的一個函數名. 從性質講: ...
  • 後臺代碼: ...
  • Web.config配置: 在<system.web>節下: 登錄代碼: /// <summary> /// 登錄 /// </summary> public static bool Login(string userName, string userPwd) { MySqlHelper dbHel ...
  • 一、環境及工具 1、伺服器 VirtualBox5.1.4 安裝 Ubuntu Server 16.04 amd64 MySql Ver 14.14 Distrib 5.6.21 Jexus 5.8.1 nginx 1.10.0 dotnet core 1.0.0-preview2-003121 s ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...