1. 程式人生 > >如何用C#實現依賴注入?

如何用C#實現依賴注入?

1. 問題的提出

開發中,尤其是大型專案的開發中,為了降低模組間、類間的耦合關係,比較提倡基於介面開發,但在實現中也必須面臨最終是“誰”提供實體類的問題。Martin Fowler在《Inversion of Control Containers and the Dependency Injection pattern》中也提到了標準的三種實現方式——Constructor Injection、Setter Injection和Interface Injection,很全面的闡釋了這個問題。

對於C#而言,由於語法元素上本身要比Java豐富,如何實施注入還有些技巧和特色之處。這方面微軟的ObjectBuilder是個不錯的教科書,對三種標準方式的實現也都很到位,但就是有些龐大了。

本文中,筆者借鑑Martin Fowler的撰文,也通過一些精簡的程式碼片斷向讀者介紹C#實現依賴注入的基本技巧。

我有個習慣,每天晚上要看天氣預報,就以這個開始好了,先定義待注入物件的抽象行為描述,然後增加一個假的實體類,相關程式碼和單元測試如下:

C#
using System;
namespace VisionLogic.Training.DependencyInjection.Scenario
{
/// <summary>
/// 抽象注入物件介面
/// </summary>
public interface IWeatherReader
{
string Current { get;}
}
}

C#
using System;
namespace VisionLogic.Training.DependencyInjection.Scenario.Raw
{
/// <summary>
/// 偽造的一個實現類
/// </summary>
class FakeWeatherReader : IWeatherReader
{
public string Current { get { return string.Empty; } }
}

/// <summary>
/// 客戶程式
/// </summary>
public class Client
{
protected IWeatherReader reader = new FakeWeatherReader();

public virtual string Weather
{
get
{
string current = reader.Current;
switch (current)
{
case "s": return "sunny";
case "r": return "rainy";
case "c": return "cloudy";
default:
return "unknown";
}
}
}
}
}
Unit Test
using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario;
using VisionLogic.Training.DependencyInjection.Scenario.Raw;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest.Raw
{
[TestClass]
public class WeatherReaderTest
{
[TestMethod]
public void Test()
{
Client client = new Client();
Assert.AreEqual<string>("unknown", client.Weather);
}
}
}

    問題就出現了,雖然美好的願望是Client僅僅依賴抽象的IWeatherReader,但之前總要和一個實體類“軋”一道,那麼實際的效果就是實體類作了修改、重新編譯了,Client也要處理,沒有真正達到隔離的目的。依賴注入通過引入第三方責任者的方法,相對好的梳理了這個關係,這位重要的角色就是一個Assembler類,他和實體型別打交道,對Client而言他總是可以根據約定,加工出需要的IWeatherReader。

2.進一步的分析

    看上去,Client被解放了,但又套住了Assembler,為了儘量讓他與實體類間鬆散些需要做什麼呢?

 首先要完成自己的職責:可以找到合適的實現類例項,不管是重新構造一個還是找個現成的。
 既要根據需要加工介面IWeatherReader,又要讓自己儘量不與大量的實體類糾纏在一起,最好的辦法就是從.Net Framework中再找到一個“第三方”,這裡選中了System.Activator。

 還有就是當客戶程式呼叫Assembler的時候,它需要知道需要通過哪個實現類的例項返回,該項工作一方面可以通過一個字典完成,也可以通過配置解決,兩者應用都很普遍,怎麼選擇呢——抽象,提取一個介面,然後都實現。
由於本文主要介紹依賴注入的實現,為了簡單起見,採用一個偽造的記憶體字典方式,而非基於System.Configuration的配置系統實現一個Assembler的協同類。

    C# 新增一個用於管理抽象型別——實體型別對映關係的型別ITypeMap

using System;
using System.Collections.Generic;
namespace VisionLogic.Training.DependencyInjection.Scenario
{
/// <summary>
/// 考慮到某些型別沒有無參的建構函式,增加了描述構造資訊的專門結構
/// </summary>
public class TypeConstructor
{
private Type type;
private object[] constructorParameters;
public TypeConstructor(Type type, params object[] constructorParameters)
{
this.type = type;
this.constructorParameters = constructorParameters;
}
public TypeConstructor(Type type) : this(type, null) { }

public Type Type { get { return type; } }
public object[] ConstructorParameters { get { return constructorParameters; } }
}


/// <summary>
/// 管理抽象型別與實體型別的字典型別
/// </summary>
public interface ITypeMap
{
TypeConstructor this[Type target]{get;}
}
}
C# 實現一個Assembler型別,為了示例方便,同時實現了一個ITypeMap和IWeatherReader
using System;
using System.Collections.Generic;
namespace VisionLogic.Training.DependencyInjection.Scenario
{
/// <summary>
/// 測試用的實體類
/// </summary>
public class WeatherReaderImpl : IWeatherReader
{
private string weather;
public WeatherReaderImpl(string weather)
{
this.weather = weather;
}

public string Current
{
get { return weather; }
}
}

/// <summary>
/// 管理抽象型別與實際實體型別對映關係,實際工程中應該從配置系統、引數系統獲得。
/// 這裡為了示例方便,採用了一個純記憶體字典的方式。
/// </summary>
public class MemoryTypeMap : ITypeMap
{
private Dictionary<Type, TypeConstructor> dictionary =
new Dictionary<Type, TypeConstructor>();
public static readonly ITypeMap Instance;


/// <summary>
/// Singleton
/// </summary>
private MemoryTypeMap(){}
static MemoryTypeMap()
{
MemoryTypeMap singleton = new MemoryTypeMap();
// 註冊抽象型別需要使用的實體型別
// 該型別實體具有構造引數,實際的配置資訊可以從外層機制獲得。
singleton.dictionary.Add(typeof(IWeatherReader), new TypeConstructor(
typeof(WeatherReaderImpl), "s"));
Instance = singleton;
}


/// <summary>
/// 根據註冊的目標抽象型別,返回一個實體型別及其構造引數陣列
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public TypeConstructor this[Type type]
{
get
{
TypeConstructor result;
if (!dictionary.TryGetValue(type, out result))
return null;
else
return result;
}
}
}

public class Assembler<T>
where T : class
{
/// <summary>
/// 其實TypeMap工程上本身就是個需要注入的型別,可以通過訪問配置系統獲得,
/// 這裡為了示例的方便,手工配置了一些型別對映資訊。
/// </summary>
private static ITypeMap map = MemoryTypeMap.Instance;


public T Create()
{
TypeConstructor constructor = map[typeof(T)];
if (constructor != null)
{
if (constructor.ConstructorParameters == null)
return (T)Activator.CreateInstance(constructor.Type);
else
return (T)Activator.CreateInstance(
constructor.Type, constructor.ConstructorParameters);
}
else
return null;
}
}
}
Unit Test
using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest
{
[TestClass()]
public class AssemblerTest
{
[TestMethod]
public void Test()
{
IWeatherReader reader = new Assembler<IWeatherReader>().Create();
Assert.IsNotNull(reader);
Assert.AreEqual<System.Type>(typeof(WeatherReaderImpl), reader.GetType());
}
}

3.經典方式下的注入實現

    在完成了Assembler這個基礎環境後,就是怎麼注入的問題了,下面是對三種方式的經典方法實現:

    3.1 Constructor Injection方式

Unit Test - Constructor
using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest
{
[TestClass]
public class ConstructorInjectionTest
{
class Client
{
private IWeatherReader reader;
public Client(IWeatherReader reader)
{
this.reader = reader;
}
}

[TestMethod]
public void Test()
{
IWeatherReader reader = new Assembler<IWeatherReader>().Create();
Client client = new Client(reader);
Assert.IsNotNull(client);
}
}
}

    3.2 Setter Injection方式

Unit Test - Setter
using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest
{
[TestClass]
public class SetterInjectionTest
{
class Client
{
private IWeatherReader reader;
public IWeatherReader Reader
{
get { return reader; }
set { reader = value; }
}
}

[TestMethod]
public void Test()
{
IWeatherReader reader = new Assembler<IWeatherReader>().Create();
Client client = new Client();
client.Reader = reader;
Assert.IsNotNull(client.Reader);
}
}
}

    3.3 Interface Injection方式

Unit Test - Interface
using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest
{
[TestClass]
public class InterfaceInjectionTest
{
interface IClientWithWeatherReader
{
IWeatherReader Reader { get; set;}
}

class Client : IClientWithWeatherReader
{
private IWeatherReader reader;

#region IClientWithWeatherReader Members
public IWeatherReader Reader
{
get { return reader; }
set { reader = value; }
}
#endregion
}

[TestMethod]
public void Test()
{
IWeatherReader reader = new Assembler<IWeatherReader>().Create();
Client client = new Client();
IClientWithWeatherReader clientWithReader = client;
clientWithReader.Reader = reader;
Assert.IsNotNull(clientWithReader.Reader);
}
}
}

4. 用屬性(Attribute)注入 

  C#還可以通過Attribute注入,Enterprise Library中大量使用這種方式將各種第三方機制加入到類系統中。例如:

 •執行監控需要的Performance Counter。
 •用於構造過程的指標資訊。
 •用於日誌、密碼處理。
 •等等 

  注:Java語言雖然發展比較慢,但在Java 5種也提供了類似的Annotation的機制,換了個名字省去被評估為“抄襲”的嫌疑。) 

  為了演示方便,下面設計一個應用情景: 
    Scenario 
  1、 應用需要一個集中的機制瞭解系統中實際建立過多少個特定型別物件的例項,用於評估系統的Capacity要求。 
  2、 為了防止系統資源被用盡,需要控制每類物件例項數量。 

  怎麼實現呢?如下:

 •增加一個記憶體的註冊器,登記每個類已經建立過的例項例項數量。
 •然後給每個類貼個標籤——Attribute,讓Assembler在生成的物件的時候根據標籤的內容把把登記到註冊器。 

    4.1定義抽象業務實體

C#
using System;
namespace VisionLogic.Training.DependencyInjection.Scenario.Attributer
{
/// <summary>
/// 抽象的處理物件
/// </summary>
public interface IObjectWithGuid
{
string Guid { get; set;}
}
}
定義需要注入的限制介面,並用一個Attribute管理它
C#
using System;
namespace VisionLogic.Training.DependencyInjection.Scenario.Attributer
{
/// <summary>
/// 需要注入的用以限制最大數量的介面
/// </summary>
public interface ICapacityConstraint
{
int Max { get;}
}

public class CapacityConstraint : ICapacityConstraint
{
private int max;
public CapacityConstraint(){this.max = 0;} // 預設情況下不限制
public CapacityConstraint(int max) { this.max = max; }
public int Max { get { return max; } }
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ConstraintAttribute : Attribute
{
private ICapacityConstraint capacity;

public ConstraintAttribute(int max) { this.capacity = new CapacityConstraint(max); }
public ConstraintAttribute() { this.capacity = null; }

public ICapacityConstraint Capacity { get { return capacity; } }
}
}

Assembler上增加通過Attribute注入限制的響應
C#
using System;
using System.Collections.Generic;
namespace VisionLogic.Training.DependencyInjection.Scenario.Attributer
{
public class Assembler
{
/// <summary>
/// 登記相關型別對“最大容量”屬性的使用情況
/// </summary>
private IDictionary<Type, ConstraintAttribute> attributeRegistry =
new Dictionary<Type, ConstraintAttribute>();
/// <summary>
/// 登記每個型別(如須受到“最大容量”屬性限制的話),實際已經建立的物件數量
/// </summary>
private IDictionary<Type, int> usageRegistry = new Dictionary<Type, int>();


public T Create<T>()
where T : IObjectWithGuid, new()
{
ICapacityConstraint constraint = GetAttributeDefinedMax(typeof(T));
if ((constraint == null) || (constraint.Max <= 0)) // max <= 0 代表是不需要限制數量的。
return InternalCreate<T>();
else
{
if (usageRegistry[typeof(T)] < constraint.Max) // 檢查是否超出容量限制
{
usageRegistry[typeof(T)]++; // 更新使用情況註冊資訊
return InternalCreate<T>();
}
else
return default(T);
}
}

// helper method
// 直接生成特定例項,並setter 方式注入其guid。
private T InternalCreate<T>()
where T : IObjectWithGuid, new()
{
T result = new T();
result.Guid = Guid.NewGuid().ToString();
return result;
}

/// helper method.
// 獲取特定型別所定義的最大數量, 同時視情況維護attributeRegistry 和usageRegistry 的註冊資訊。
private ICapacityConstraint GetAttributeDefinedMax(Type type)
{
ConstraintAttribute attribute = null;
if (!attributeRegistry.TryGetValue(type, out attribute)) //新的待建立的型別
{
// 填充相關型別的“最大容量”屬性註冊資訊
object[] attributes = type.GetCustomAttributes(typeof(ConstraintAttribute), false);
if ((attributes == null) || (attributes.Length <= 0))
attributeRegistry.Add(type, null);
else
{
attribute = (ConstraintAttribute)attributes[0];
attributeRegistry.Add(type, attribute);
usageRegistry.Add(type, 0); // 同時補充該型別的使用情況註冊資訊
}
}
if (attribute == null)
return null;
else
return attribute.Capacity;
}
}
}

    4.2對方案的測試

C#
using Microsoft.VisualStudio.TestTools.UnitTesting;
using VisionLogic.Training.DependencyInjection.Scenario.Attributer;
namespace VisionLogic.Training.DependencyInjection.Scenario.UnitTest.Attributer
{
[TestClass()]
public class AssemblerTest
{
public abstract class ObjectWithGuidBase : IObjectWithGuid
{
protected string guid;
public virtual string Guid
{
get { return guid; }
set { guid = value; }
}
}

[Constraint(2)] // 通過屬性注入限制
public class ObjectWithGuidImplA : ObjectWithGuidBase { }

[Constraint(0)] // 通過屬性注入限制
public class ObjectWithGuidImplB : ObjectWithGuidBase { }

[Constraint(-5)] // 通過屬性注入限制
public class ObjectWithGuidImplC : ObjectWithGuidBase { }

public class ObjectWithGuidImplD : ObjectWithGuidBase { }

[TestMethod]
public void Test()
{
Assembler assembler = new Assembler();
for (int i = 0; i < 2; i++)
Assert.IsNotNull(assembler.Create<ObjectWithGuidImplA>());
Assert.IsNull(assembler.Create<ObjectWithGuidImplA>()); // 最多兩個
for (int i = 0; i < 100; i++)
Assert.IsNotNull(assembler.Create<ObjectWithGuidImplB>()); // 不限制
for (int i = 0; i < 100; i++)
Assert.IsNotNull(assembler.Create<ObjectWithGuidImplC>()); // 不限制
for (int i = 0; i < 100; i++)
Assert.IsNotNull(assembler.Create<ObjectWithGuidImplD>()); // 不限制
}
}
}


    5. 進一步討論

    上面的例子雖然僅僅通過Attribute注入了一個容量限制介面,但完全可以為他增加一個容器,可以把一組限制介面藉助ConstraintAttribute這個通道注入進去,並在Assembler中生效。

    6.其他問題

    實際專案中,為了滿足多核系統的需要,Assembler往往和目標物件分別執行在主程序和具體某個執行緒之中,如何執行緒安全的注入是必須面臨並謹慎設計的問題。