關於IoC,以及StructureMap的一些簡單應用(基本用法、自動裝配、生命週期、延遲加載)。
Q:什麼是IoC
IoC:Inversion of Control。中文名:控制反轉。是一種思想、一種模式。
從字面意思理解:控制權被反轉。
舉個例子:某知名企業經理有一批商品要存入倉庫,但是現在沒有倉庫,所以必須建一個倉庫,然後將商品存入。
場景A:經理去找一座倉庫。
場景B:經理告訴手下去找一座倉庫。
場景A,找倉庫的控制權一直在主管手上,經理必須自己找。
場景B,經理將找倉庫的控制權交給手下。這種將控制權轉移的過程就是Inversion of Control。
Q:爲什麼需要IoC?
爲了更好的理解,下面用代碼實現上面提到的需求 ,這裏可以把SqlServer理解爲上文倉庫的一種。
邏輯:(1)創建一個SqlServerHelper對象。(2)存入到SqlServer數據庫中。
場景A:
class ProductManager { private readonly IDBHelper dbHelper; public ProductManager () { dbHelper = new SqlServerHelper (); } public void SaveProduct (Product product) { dbHelper.Save (product); } } internal interface IDBHelper { void Save (Product product); } class SqlServerHelper : IDBHelper { public void Save (Product product) { //Save in SqlServer } }
場景A:
1、創建SqlServerHelper的控制權在經理手裏。
2、ProductManager與SqlServerHelper組件存在依賴。當依賴變化時,也就是更換SqlServerHelper組件的時候,我們需要改ProductManager代碼。
3、當ProductManager調用端需要save到Oracle的時候。ProductManager代碼無法複用。
4、給ProductManager添加測試。如果測試掛掉。無法確定問題是出在ProductManager還是SqlServerHelper。(比如鼠標不Work了,不能確定到底是鼠標本身的問題,還是電腦USB的問題)
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
場景B:
class ProductManager { private readonly IDBHelper dbHelper; public ProductManager () { dbHelper = Employee.Find (); } public void SaveProduct (Product product) { dbHelper.Save (product); } } internal interface IDBHelper { void Save (Product product); } class SqlServerHelper : IDBHelper { public void Save (Product product) { //Save in SqlServer } } static class Employee { public static SqlServerHelper Find () { return new SqlServerHelper (); } }
場景B:
1、創建組件的控制權由經理交給了第三方Employee。(控制權轉移)
2、ProductManager與SqlServerHelper組件之間沒有直接的依賴,而是通過第三方Employee去控制它們之間的依賴(鬆散耦合)。
3、ProductManager可複用。當ProductManager調用方需要存入Oralce的時候,只需要改變Employee中代碼即可。(可複用)
4、組件相對獨立、可以分別測試ProductManager組件和SqlServerHelper組件。(比如電腦中各個部件都需要獨立的測試。測試通過後在組裝一起)
這裏並不是IoC的具體實現。可以將第三方Employee理解爲一個容器。只需要知道把控制權轉移到第三方是爲了削弱ProductManager與SqlServerHelper的依賴即可。
那麼爲什麼需要IoC就很清晰了:降低組件之間的耦合、提高軟件可測試性、代碼複用等。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Q:怎麼實現IoC?
IoC容器:一個負責組件管理的容器,可以在容器中註冊組件,也可從容器中取出組件。
現在我們可以把Employee對象理解爲一個容器。我們在這個容器裏管理SqlServerHelper組件。
1、首先在容器裏註冊ProductManager的依賴組件SqlServerHelper(用一個XML文件當做容器)
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="StructureMap" type="StructureMap.Configuration.StructureMapConfigurationSection,StructureMap"/> </configSections> <StructureMap MementoStyle="Attribute"> <DefaultInstance PluginType="IoC_SMTest.IDBHelper,IoC_SMTest" PluggedType="IoC_SMTest.SqlServerHelper,IoC_SMTest" /> </StructureMap> </configuration>
PluginType:"IoC_SMTest. IDBHelper " IDBHelper爲ProductManager下的接口。
pluggedType:"IoC_SMTest. SqlServerHelper " SqlServerHelper爲IDBHelper的實現
此時容器裏定義了:IDBHelper的實現是SqlServerHelper。(ProductManager所依賴的組件是SqlServerHelper)
2、獲取該容器裏的依賴關係
ObjectFactory.Initialize(x =>
{
x.PullConfigurationFromAppConfig = true;
});
執行這步之後。程序把Config裏定義的依賴關係pull下來,已經知道IDBHelper依賴SqlServerHelper組件
3、從容器中獲取組件ObjectFactory.GetInstance<IDBHelper>();
在給產品代碼加一個OracleDB。實現SqlServer和Oracle之間的切換。
class ProductManager { private readonly IDBHelper dbHelper; public ProductManager () { dbHelper = ObjectFactory.GetInstance<IDBHelper> (); } public void SaveProduct (Product product) { dbHelper.Save (product); } } interface IDBHelper { void Save (Product product); } class SqlServerHelper : IDBHelper { public void Save (Product product) { //Save in SqlServer } } class OracleHelper : IDBHelper { public void Save (Product product) { //Save in Oracle } }
如果ProductManager需要Oracle組件。可以直接修改容器中的依賴關係即可,產品代碼不需要改變。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="StructureMap" type="StructureMap.Configuration.StructureMapConfigurationSection,StructureMap"/> </configSections> <StructureMap MementoStyle="Attribute"> <DefaultInstance PluginType="IoC_SMTest.IDBHelper,IoC_SMTest" PluggedType="IoC_SMTest.OracleHelper,IoC_SMTest" /> </StructureMap> </configuration>
在談依賴注入(被動) :
依賴注入(Dependency Injection):這裏指的將ProductManager的依賴組件注入到ProductManager中。
常用的幾種方式:
1、構造函數注入(Constructor Injection)
class ProductManager
{
private readonly IDBHelper dbHelper;
public ProductManager(IDBHelper dbHelper)
{
this.dbHelper = dbHelper;
}
public void SaveProduct(Product product)
{
dbHelper.Save(product);
}
}
使用:
var dbhelper = ObjectFactory.GetInstance<IDBHelper>();//這塊可以StructureMap自動裝配,直接獲取ProductManager實例,自動查找依賴,後面會提。 var productManager = new ProductManager(dbhelper);
2、屬性注入(Setter Injection)
class ProductManager { public IDBHelper dbHelper { get; set; } public void SaveProduct(Product product) { dbHelper.Save(product); } }使用:
var dbhelper = ObjectFactory.GetInstance<IDBHelper>(); var productManager = new ProductManager {dbHelper = dbhelper};
推薦使用Constructor Injection,這樣看起來很清晰,一個類需要多少個組件。在構造函數裏一目瞭然。
再談依賴查找(主動)
class ProductManager
{
private readonly IDBHelper dbHelper;
public ProductManager ()
{
dbHelper = ObjectFactory.GetInstance<IDBHelper> ();
}
public void SaveProduct (Product product)
{
dbHelper.Save (product);
}
}
當這段代碼運行的時候。ProductManager會回自動從容器中找IDBHelpr的依賴組件,並且實例化。這個過程是主動的。
個人認爲這就是依賴查找。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
DI和IoC之間是什麼關係呢?
IoC是一種思想、一種模式。它把調用者(ProductManager)和被調用者(SqlServerHelper)之間的依賴。轉移到第三方容器當中。從而實現解耦。
DI都做了些什麼。有兩個相對獨立的組件ProductManager與SqlServerHelper。在系統運行時候把SqlServerHelper組件注入到ProductManager中。建立了2個組件的關係。
這個關係從哪兒來?IoC容器裏。
個人認爲DI是IoC的一種實現。
StructureMap的應用
StructureMap是.NET下的IoC框架。 官方傳送門 下載地址新建一測試工程引用 StructureMap.dll。開始 StructureMap的第一個測試。
場景:一個商品存入到數據庫中。先驗證,通過驗證存入數據庫。
準備一些接口、類供測試使用。
public class Product { public string Name { get; set; } public string BeSavedDataBase { get; set; } public bool IsValidate { get; set; } public bool IsSave { get; set; } public string UpdateBy { get; set; } } public interface IRepository { Product Save(Product product); } public class Repository : IRepository { public string UpdateBy { get; set; } private readonly string dataBase; public Repository(string dataBase) { this.dataBase = dataBase; } public Product Save(Product product) { if (string.IsNullOrEmpty(dataBase)) return product; product.BeSavedDataBase = dataBase; product.IsSave = true; product.UpdateBy = UpdateBy; return product; } } public interface IValidator { bool Validate(Product product); } public class Validator : IValidator { public bool Validate(Product product) { if (string.IsNullOrEmpty(product.Name)) return false; product.IsValidate = true; return true; } } public class ProductController { private readonly IValidator validator; private readonly IRepository repository; public ProductController(IValidator validator, IRepository repository) { this.validator = validator; this.repository = repository; } public Product Save(Product product) { var flag = validator.Validate(product); if (!flag) return product; repository.Save(product); return product; } }
基本用法(Usage)&自動裝配(AutoErect)
除了上面Demo中將依賴關係定義在XML中之外也可以用 Registry DSL來註冊依賴組件。
1、註冊依賴組件到容器中(IRepository與Repository、IValidator與Validator)。
x.For<IT>().Use<T>();
把IT與T之間的依賴關係注入到容器中
ObjectFactory.Initialize( x => { x.For<IValidator>().Use<Validator>(); x.For<IRepository>().Use<Repository>() .Ctor<string>().Is(TestProfile.SqlServerDB) .SetProperty(rep => rep.UpdateBy = TestProfile.UpdateByAdmin); });
(1).Ctor<string>().is(TestProfile.SqlServerDB) 給Repository類裏的構造函數提供實參。
(2).SetProperty(rep => rep.UpdateBy = TestProfile.UpdateByAdmin) 給Repository類裏的UpdateBy屬性賦值。也有另外的屬性賦值方法比如.WithProperty("UpdateBy").EqualTo(TestProfile.UpdateByAdmin);
2、獲取ProductController
var controller = ObjectFactory.GetInstance<ProductController>();
通過上面準備測試類的代碼可以觀察到ProductController依賴IRepository和IValidator兩個組件。
因爲IRepository和IValidator的依賴關係已經註冊在容器裏。
當獲取ProductController的時候。SM會自動查找ProductController的依賴(IRepository、IValidator),並且實例化它們注入到ProductController中。這就是 自動裝配 的過程。
PS:如果依賴沒有提前註冊到容器中。SM會拋“202”異常No Default Instance defined for PluginFamily.....
3、應用
controller.Save(product);
測試代碼:
[TestClass] public class AutoErectTest { [TestInitialize] public void SetUp() { ObjectFactory.Initialize( x => { x.For<IValidator>().Use<Validator>(); x.For<IRepository>().Use<Repository>() .Ctor<string>().Is(TestProfile.SqlServerDB) .WithProperty("UpdateBy") .EqualTo(TestProfile.UpdateByAdmin); ; }); } [TestMethod] public void should_auto_erect_IContactValidator_and_IContactRepository_instance_when_get_ContactController_instance() { var controller = ObjectFactory.GetInstance<ProductController>(); var entityResult = controller.Save(TestProfile.ProductWithName); Assert.AreEqual(true, entityResult.IsValidate); Assert.AreEqual(true, entityResult.IsSave); } }
生命週期(Life Cycle)
SM默認的對象生命週期是PerRequest.每次都是創建一個新的實例。我們可以通過註冊依賴組件的時候指定組件的生命週期。
下面以IValidator組件爲例:
該測試類下的2個私有方法:獲取組件實例,檢查組件是否爲Null.
private static void GetObjectsPerOneThread(ref IValidator V1, ref IValidator V2) { V1 = ObjectFactory.GetInstance<IValidator>(); V2 = ObjectFactory.GetInstance<IValidator>(); CheckIsNull(V1, V2); Assert.AreSame(V1, V2); } private static void CheckIsNull(params IValidator[] values) { foreach (var contactValidator in values) { Assert.IsNotNull(contactValidator); } }
Singleton:
[TestMethod] public void objects_should_be_same_when_LifeCycal_is_Singleton() { ObjectFactory.Initialize(x => x.For<IValidator>().Singleton().Use<Validator>()); IValidator v1 = null, v2 = null, v3 = null, v4 = null; ThreadStart starterFirst = () => GetObjectsPerOneThread(ref v1, ref v2); new Thread(starterFirst).Start(); Thread.Sleep(500); ThreadStart starterSecond = () => GetObjectsPerOneThread(ref v3, ref v4); new Thread(starterSecond).Start(); Thread.Sleep(500); CheckIsNull(v1, v2,v3,v4); Assert.AreSame(v1, v2); Assert.AreSame(v1, v3); }場景
x.For<ISessionFactory>().Singleton().Use(y => NHibernateHelper.SessionFactory);
爲了確保IValidator實例唯一,創建兩個線程來獲取4個IValidator。
Singleton適用ISessionFactory(NHibernate)場景。因爲ISessionFactory是線程安全的。允許多個線程同時訪問它來創建或者Open Session實例。所以我們註冊ISessionFactory的時候應該指定爲Singleton.
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
HybridHttpOrThreadLocalScoped:
[TestMethod] public void objects_should_be_not_same_when_a_instance_LifeCycal_is_HybridHttpOrThreadLocalScoped() { ObjectFactory.Initialize( x => x.For<IValidator>() .HybridHttpOrThreadLocalScoped() .Use<Validator>()); IValidator v1 = null, v2 = null, v3 = null, v4 = null, v5 = null, v6 = null; ThreadStart starterFirst = () => GetObjectsPerOneThread(ref v1, ref v2); new Thread(starterFirst).Start(); Thread.Sleep(500); ThreadStart starterSecond = () => GetObjectsPerOneThread(ref v3, ref v4); new Thread(starterSecond).Start(); Thread.Sleep(500); ThreadStart starterThird = () => GetObjectsPerOneThread(ref v5, ref v6); new Thread(starterThird).Start(); Thread.Sleep(500); Assert.AreSame(v1, v2); Assert.AreSame(v3, v4); Assert.AreSame(v5, v6); Assert.AreNotSame(v1, v3); Assert.AreNotSame(v3, v5); Assert.AreNotSame(v1, v5); }
場景:
x.For<Session>().HybridHttpOrThreadLocalScoped().Use<Session>()
如果存在HttpContext,那麼在HttpContext上唯一。否則在TreadLocal上唯一。HttpContext和TreadLocal銷燬,組件也隨之銷燬。
應用場景:註冊(NHibernate)Session的時候。應該讓一個Session在一個請求中唯一。因爲Session不是線程安全。一個Session是操作數據庫的一個工作單元。如果多個請求共享一個Session那麼就會出現併發、髒數據等情況。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Transient:
[TestMethod] public void objects_should_be_not_same_when_LifeCycal_is_Transient() { ObjectFactory.Initialize(x => x.For<IValidator>().Transient().Use<Validator>()); var v1 = ObjectFactory.GetInstance<IValidator>(); var v2 = ObjectFactory.GetInstance<IValidator>(); CheckIsNull(v1, v2); Assert.AreNotSame(v1, v2); }
每次請求都會創建一個新的實例。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------HttpContextScoped:
[TestMethod]
public void objects_should_be_same_when_LifeCycal_is_HttpContextScoped()
{
ObjectFactory.Initialize(x => x.For<IValidator>().HttpContextScoped().Use<Validator>());
IValidator v1 = null, v2 = null;
ThreadStart starterFirst = () => GetObjectsPerOneThread(ref v1, ref v2);
var thread = new Thread(starterFirst);
thread.Start();
Thread.Sleep(500);
Assert.AreSame(v1, v2);
thread.Abort();
Assert.IsNull(v1);
Assert.IsNull(v2);
}
HttpContext銷燬(一個請求結束),組件也隨之銷燬。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------想想還是提一下延遲加載吧,因爲我沒有想到在什麼場景下應用,其實延遲加載就是應用到MS的Lazy關鍵字。Lazy<T>有2個屬性,第一個是IsValueCreate,第二個是Value.
沒有調用Value的時候,IsValueCreate爲false。也就是該對象沒有被創建。如果調用value屬性,T就會被創建,那麼IsValueCreate就會變爲true。
延遲加載(Lazy)
[TestClass] public class LazyTest { [TestMethod] public void lazy_object_should_not_be_created_when_it_does_not_be_use() { ObjectFactory.Initialize( x => { x.For<Lazy<ProductController>>() .Use(y => new Lazy<ProductController>(y.GetInstance<ProductController>)); x.For<IValidator>().Use<Validator>(); x.For<IRepository>().Use<Repository>() .Ctor<string>().Is("admin") .WithProperty("DataBase").EqualTo("SqlServer"); }); var controller = ObjectFactory.GetInstance<Lazy<ProductController>>(); Assert.IsFalse(controller.IsValueCreated); controller.Value.Save(new Product { Name = "ipad" }); Assert.IsTrue(controller.IsValueCreated); } [TestMethod] public void exceptionCode_should_be_202_when_lazy_object_do_not_to_be_created() { ObjectFactory.Initialize( x => { x.For<Lazy<IValidator>>() .Use(y => new Lazy<IValidator>(y.GetInstance<Validator>)); x.For<Lazy<IRepository>>() .Use(y => new Lazy<IRepository>(y.GetInstance<Repository>)); }); const string expectExceptionMsg = "No Default Instance defined for PluginFamily "; const int expectExceptionCode = 202; try { ObjectFactory.GetInstance<ProductController>(); Assert.Fail(); } catch (StructureMapException exception) { Assert.AreEqual(expectExceptionCode, exception.ErrorCode); Assert.IsTrue(exception.Message.Contains(expectExceptionMsg)); } } }
到這裏一些簡單的StructureMap的應用就告一段落了。當然還有一些其他的應用,可以從官方API裏找。
最後跑下所有測試。
還有一些沒有涉及到的。以後有時間會慢慢深入。看出文章存在問題的朋友請給一些反饋。持續改進。
完