首页 > 技术文章 > C# 依赖注入

jin- 2018-09-20 22:17 原文

依赖注入是一个过程,就是当一个类需要调用另一个类来完成某项任务的时候,在调用类里面不要去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文件夹。

 

推荐阅读