zoukankan      html  css  js  c++  java
  • 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文件夹。

  • 相关阅读:
    luogu P2661 信息传递 强连通分量求最小环
    luogu P1346 电车 最短路
    luogu P1113 杂务
    luogu P1111 修复公路 最小生成树prim
    python提交要注意的几个地方
    【图论】拓扑排序
    算法竞赛入门经典 第六章
    实用函数
    Markdown数学公式语法
    Codeforces Round #627 (Div. 3) 补题
  • 原文地址:https://www.cnblogs.com/jin-/p/9683694.html
Copyright © 2011-2022 走看看