[IOC] StructureMap的疑惑

 

關於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裏找。

最後跑下所有測試。

還有一些沒有涉及到的。以後有時間會慢慢深入。看出文章存在問題的朋友請給一些反饋。持續改進。

測試代碼下載。

發佈了37 篇原創文章 · 獲贊 9 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章