C# 依賴注入


依賴注入是一個過程,就是當一個類需要調用另一個類來完成某項任務的時候,在調用類里面不要去new被調用的類的對象,而是通過注入的方式來獲取這樣一個對象。具體的實現就是在調用類里面有一個被調用類的接口,然后通過調用接口的函數來完成任務。比如A調用B,而B實現了接口C,那么在A里面用C定義一個變量D,這個變量的實例不在A里面創建,而是通過A的上下文來獲取。這樣做的好處就是將類A和B分開了,他們之間靠接口C來聯系,從而實現對接口編程。

 

依賴注入最常用的兩種方式是setter注入和構造函數注入。

setter注入:

就是在類A里面定義一個C接口的屬性D,在A的上下文通過B實例化一個對象,然后將這個對象賦值給屬性D。主要就是set 與 get

構造函數注入:

就是在創建A的對象的時候,通過參數將B的對象傳入到A中。

還有常用的注入方式就是工廠模式的應用了,這些都可以將B的實例化放到A外面,從而讓A和B沒有關系。還有一個接口注入,就是在客戶類(A)的接口中有一個服務類(B)的屬性。在實例化了這個接口的子類后,對這個屬性賦值,這和setter注入一樣。

 

 

MEF:

(一)、

下面重點介紹C#中實現依賴注入的一種組件MEF。先看一個簡單的例子

創建一個控制台項目,添加一個接口IBookService:

然后創建一個類MusicBookService來實現這個接口。下面的這個Export的作用后面再說。

創建一個客戶類,在客戶類中要調用MusicBookService中的函數來完成任務

namespace DependencyInjection.MEF
{
    class MusicBookClient
    {
        [Import]
        public IBookService Service { get; set; }
        public static void Mef()
        {
            MusicBookClient pro = new MusicBookClient();
            pro.Compose();
            if (pro.Service != null)
            {
                Console.WriteLine(pro.Service.GetBookName());
            }
            Console.Read();
        }
        
        private void Compose()
        {
            var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());//反射
            CompositionContainer container = new CompositionContainer(catalog);
            container.ComposeParts(this);
        }
    }
}

然后在main函數中調用這個MusicBookClient.Mef();運行程序就會看到音樂書籍這幾個字了。

在MusicBookClient中,按照以前的做法有3種:在Mef中實例化Service;定義一個參數為IBookService的構造函數,在創建MusicBookClient對象的時候將Service實例化;在main函數中實例化一個MusicBookService,然后賦值給MusicBookClient的service屬性。

但是看上面的這幾段代碼,沒有發現實例化MusicBookService的地方,但是確實在MusicBookClient中調用了MusicBookService的函數。這就是MEF組件來實現依賴注入的特殊之處。這個應該也是用的反射技術,但是通過MEF用起來要簡單的多。

 

現在再來看看上面的[Export(typeof(IBookService))],這句的作用是將類MusicBookService按照類型IBookService導出,如果沒有指定類型,那么將按照object導出。導出之后,看MusicBookClient類中,有個[Import],這句的作用是將剛剛導出的MusicBookService導入,下面的Compose方法,實例化CompositionContainer來實現組合。這整個過程都是MEF組件來完成,我們不用去關心它怎么做到的。但是有一點要注意,實現接口的類,必須有無參數的構造函數,否則會報錯

通過上面的代碼可以對MEF有個初步的認識。但是如果有多個類實現了IBookService,也和上面一樣用[Export(typeof(IBookService))],那么再運行代碼的時候就會報錯,因為系統不知道你要導入的是哪個具體的類。下面就來介紹一下這種情況的處理。

 

(二)、

接口還是那個接口,不變,現在重新創建接口的實現類和客戶類:

[Export("MathBookService", typeof(IBookService))]
    class MathBookService : IBookService
    {
        public string GetBookName()
        {
            return "數學書籍";
        }
    }

[Export("ChineseBookService", typeof(IBookService))]
    class ChineseBookService : IBookService
    {
        public string GetBookName()
        {
            return "語文書籍";
        }
    }

現在創建了兩個類來實現接口,但是在export屬性的構造函數就必須要指定一個名稱,這個名稱可以隨意指定,而且可以重復,但最好還是別亂起。

客戶類BookClient1:這里可以看到,import也用了上面取的名字了,在main函數中調用Mef1,輸出的是語文書籍。這里的Compose函數和上面的是一樣的。

[Import("ChineseBookService")]
        public IBookService Service { set; get; }  
        public static void Mef1()
        {
            BookClient1 pro = new BookClient1();
            pro.Compose();
            Console.WriteLine( pro.Service.GetBookName());
            Console.Read();
        }

剛才說了,export屬性的構造函數里面取的名字可以重復,那么現在我們來看看這種情況,再創建一個類,實現接口IBookService:

看到這里的export的第一個參數和MathBookService類的一樣,名字重復了。

 [Export("MathBookService", typeof(IBookService))]
    class MyMathBookService : IBookService
    {
        public string GetBookName()
        {
            return "數學書籍1";
        }
    }

在客戶類BookClient1中添加如下代碼:

[ImportMany("MathBookService")]
        public IEnumerable<IBookService> Services { get; set; }
        
        public static void Mef()
        {
            BookClient1 pro = new BookClient1();
            pro.Compose();
            if (pro.Services != null)
            {
                foreach (var s in pro.Services)
                {
                    Console.WriteLine(s.GetBookName());
                }
            }
            Console.Read();
        }

注意,這里不是用 的import,而是ImportMany,並且service也不是原來的那樣了,而是一個集合。這個機會包含了所有取名為MathBookService的類的對象。

在main函數中調用Mef函數,會輸出兩行文字。

注意:IEnumerable<T>中的類型必須和類的導出類型匹配,如類上面標注的是[Exprot(typeof(object))],那么就必須聲明為IEnumerable<object>才能匹配到導出的類。如果不指定類型,默認是object

 

(三)、

前面導出的都是類,那么方法和屬性能不能導出呢???答案是肯定的,下面就來說下MEF是如何導出方法和屬性的。

接口還是不變,重新定義接口的實現類和客戶類:

 class HistoryBookService : IBookService
    {
        //導出私有屬性
        [Export(typeof(string))]
        private string _privateBookName = "Private History BookName";

        //導出公有屬性
        [Export(typeof(string))]
        public string _publicBookName = "Public History BookName";

        //導出公有方法
        [Export(typeof(Func<string>))]
        public string GetBookName()
        {
            return "歷史書籍";
        }

        //導出私有方法
        [Export(typeof(Func<int, string>))]
        private string GetBookPrice(int price)
        {
            return "$" + price;
        }
        
    }

客戶類:

class BookClient2
    {
        //導入屬性,這里不區分public還是private
        [ImportMany]
        public List<string> InputString { get; set; }
        //導入無參數方法
        [Import]
        public Func<string> methodWithoutPara { get; set; }

        //導入有參數方法
        [Import]
        public Func<int, string> methodWithPara { get; set; }


        public static void Mef()
        {
            BookClient2 c2 = new BookClient2();
            c2.Compose();
            foreach (var str in c2.InputString)
            {
                Console.WriteLine(str);
            }
            //調用無參數方法
            if (c2.methodWithoutPara != null)
            {
                Console.WriteLine(c2.methodWithoutPara());
            }
            //調用有參數方法
            if (c2.methodWithPara != null)
            {
                Console.WriteLine(c2.methodWithPara(3000));
            }

            Console.Read();
        }

        private void Compose()
        {
            var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());//反射
            CompositionContainer container = new CompositionContainer(catalog);
            container.ComposeParts(this);
        }
    }

在main函數中調用BookClient2.Mef();,運行后:

至此,MEF組件的用法基本介紹完了,下面看看MEF在項目中如何使用。

 

 

重新建一個控制台項目,項目結構如下:

BankInterface是接口項目,BankOfChina是一個類庫項目,MEFDemo是主項目,后兩者需要引用接口項目。

接口項目中定義一個接口:

public interface ICard
    {
        //賬戶金額
        double Money { get; set; }
        //獲取賬戶信息
        string GetCountInfo();
        //存錢
        void SaveMoney(double money);
        //取錢
        void CheckOutMoney(double money);
    }

BankOfChina項目中定義一個類ZHCard,實現ICard接口:

namespace BankOfChina
{
    [Export(typeof(ICard))]
    public class ZHCard : ICard
    {
        public string GetCountInfo()
        {
            return "中國銀行";
        }

        public void SaveMoney(double money)
        {
            this.Money += money;
        }

        public void CheckOutMoney(double money)
        {
            this.Money -= money;
        }

        public double Money { get; set; }
    }
}

主項目:

class Program
    {
        [ImportMany(typeof(ICard))]
        public IEnumerable<ICard> cards { get; set; }
        static void Main(string[] args)
        {
            Program pro = new Program();
            pro.Compose();
            foreach (var c in pro.cards)
            {
                Console.WriteLine(c.GetCountInfo());
            }
            Console.Read();
        }
        private void Compose()
        {
            var catalog = new DirectoryCatalog("Cards");
            var container = new CompositionContainer(catalog);
            container.ComposeParts(this);
        }
    }

注意到Compose函數,這里的和上面的有點不一樣,在上面的代碼里面獲取的是當前項目所在的程序集,而這里呢是獲取指定目錄中的所有dll文件,其目的都是為了用反射創建對象。

然后先編譯一遍項目,在主項目的Debug文件夾下面創建一個cards文件夾,為什么是cards呢,因為代碼里面指定的是這個名字。然后將BankOfChina項目編譯的dll放到里面。然后運行才可以正確輸出信息(畢竟我們沒有引用那個項目)

運行后看到輸出的內容是中國銀行。

整個項目到此應該是完整了,現在的問題是,我們需要對項目進行擴展,需要添加一個工商銀行。怎么擴展呢,如果不用MEF組件,按照原來的方式,肯定是要重新編譯主項目的,因為要修改主項目嘛。但是現在用了MEF組件的依賴注入功能,就不用了。

新建一個項目BankOfICBC,這個項目和BankOfChina基本是一樣的。

namespace BankOfICBC
{
    [Export(typeof(ICard))]
    public class ICBCCard : ICard
    {
        public string GetCountInfo()
        {
            return "工商銀行";
        }

        public void SaveMoney(double money)
        {
            this.Money += money;
        }

        public void CheckOutMoney(double money)
        {
            this.Money -= money;
        }

        public double Money { get; set; }
    }
}

項目寫完之后,這里可以只編譯這一個項目,然后將編譯好的BankOfICBC.dll放到cards文件夾。然后運行程序,會輸出:中國銀行,工商銀行。這兩行文字。如果要擴展其他的銀行的,都可以按照這樣的方式。這就完美的實現了只擴展,不修改的原則。

 

 但是這里還有一個問題,就是在主項目MEFDemo的main函數中,我們無法知道pro.cards中的每個對象具體是哪個,也就無法分別作出處理。這就需要重新定義export特性了:

在接口項目中添加特性類ExportCardAttribute:  注意,這里的構造函數用的是無參的,然后調用了父類的構造函數,但是卻傳遞了一個參數,這里寫死了,本來打算寫一個有參的構造函數,像注釋的那樣,但是好像不行。

namespace BankInterface
{
    /// <summary>
    /// AllowMultiple = false,代表一個類不允許多次使用此屬性
    /// </summary>
    [MetadataAttribute]
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class ExportCardAttribute : ExportAttribute
    {
        public ExportCardAttribute() : base(typeof(ICard))
        {
        }
        //public ExportCardAttribute(Type t) : base(t)
        //{
        //}
        public string CardType { get; set; }
    }
}

在這個自定義的特性中,添加了一個屬性CardType,用來當作一個區分標記。

添加一個接口:

  public interface IMetaData
    {
        string CardType { get; }
    }

 

然后修改BankOfChina項目:

namespace BankOfChina
{
    //[Export(typeof(ICard))]
    [ExportCard( CardType = "BankOfChina")]
    public class ZHCard : ICard
    {
        public string GetCountInfo()
        {
            return "中國銀行";
        }

        public void SaveMoney(double money)
        {
            this.Money += money;
        }

        public void CheckOutMoney(double money)
        {
            this.Money -= money;
        }

        public double Money { get; set; }
    }
}

主項目:

 class Program
    {
        //[ImportMany(typeof(ICard))]
        //public IEnumerable<ICard> cards { get; set; }

        //其中AllowRecomposition=true參數就表示運行在有新的部件被裝配成功后進行部件集的重組.
        [ImportMany(AllowRecomposition = true)]
        public IEnumerable<Lazy<ICard, IMetaData>> cards { get; set; }
        static void Main(string[] args)
        {
            Program pro = new Program();
            pro.Compose();
            foreach (var c in pro.cards)
            {
                if (c.Metadata.CardType == "BankOfChina")
                {
                    Console.WriteLine("這是中國銀行卡");
                    Console.WriteLine(c.Value.GetCountInfo());
                }
                else if (c.Metadata.CardType == "NongHang")
                {
                    Console.WriteLine("這是農行卡"); Console.WriteLine(c.Value.GetCountInfo()); }
            }
            Console.Read();
        }
        private void Compose()
        {
            var catalog = new DirectoryCatalog("Cards");
            var container = new CompositionContainer(catalog);
            container.ComposeParts(this);
        }
    }

 

記得要重新編譯BankOfChina項目,然后將dll放到cards文件夾。

 


免責聲明!

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



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