写点什么

动手撸一个 IOC 框架

用户头像
喵叔
关注
发布于: 2021 年 04 月 03 日
动手撸一个IOC框架

无论你是 Java 程序员还是 C# 程序员或者是其他语言的程序员,在开发中一定会直接或间接的使用到 IoC,那么你是否想过 IoC 的远离呢?你是否想过自己 DIY 一个 IoC 呢?这篇文章将带你一步一步地来实现一个 IoC 并将它发布在 NuGet 上。


Tip:因为本文主要是一步一步的讲解 IoC 原理和 DIY IoC 因此只会出现核心代码,如需完整代码请移步百度网盘,提取码 frwh 下载。

依赖倒置

在讲解开发 IoC 前,我们有必要先讲解一下 IoC 的核心依赖倒置原则。在传统项目开发中我们会将代码分为多层,曾与曾之间相互依赖(如图一),如果其中 D 层发生改变,那么依赖于它的 C 层也要跟着改变,接着依赖于 C 层的 B 层也要改变,这样就出现了一层改变层层改变的问题。


为了解决这个问题我们引入依赖倒置原则,依赖倒置原则是面向对象六大原则之一,核心思想是高层模块不应该依赖于低层模块的细节,而是应该通过抽象来依赖(如图二)。


这里所说的高层模块指的是层与层之间的依赖项为高层,被依赖项为低层,例如 A 层依赖 B 层那么 A 层是高层 B 为低层。所谓的细节就是普通类,抽象就是接口或抽象类。



图一



图二


说了这么多理论知识,下面我们来动手一步一步的实现依赖倒置。首先我们先来看一下本篇文章将用到的项目初始结构(图三)。


当前项目一共三个类库,其中 UI 类库为控制台应用程序,Service 和 Model 分别为服务层和实体层。下面我们在 UI 层先以传统的方式来实现一个显示汽车被司机开动的代码。


class Program{    static void Main(string[] args)    {        //实例化 BMW 对象        BMW bmw = new BMW();        //实例化服务对象        DriverService driverService = new DriverService();        driverService.Open(bmw);        Console.Read();    }}
复制代码


我们将代码运行起来控制台将打印出“我开动了 BMW 牌子的车”,虽然传统方式简单明了,但是却不利于代码的扩展,修改一层将还会出现前面所说的层层修改的问题。


Tip:后续我会在这个初始项目上一步一步的来拓展,最终称为一个 IoC。



图三


下面我们引入依赖倒置,首先我们在项目中新建一个类库 IService,并在其中新增接口 IDriverService(抽象) 并使 Service 类库中的 DriverService (实现)类继承它。UI 层的主函数代码修改如下。


class Program{    static void Main(string[] args)    {        BMW bmw = new BMW();        IDriverService driverService = new DriverService();        driverService.Open(bmw);        Console.Read();    }}
复制代码


这时我们看到 driver.open(bmw) 这段代码报错了,这是因为前面我们只定义了 IDriverService 接口并让 DriverService 继承了它,但并没有定义接口中的方法。这里就引入了一个小知识点:在依赖倒置中如果抽象中没有某一个属性或者方法,那么我们就无法调用这个属性或方法。下面是经过修正后的 IDriverService 部分代码。


public interface IDriverService{    void Open(BMW bmw);}
复制代码


我们运行程序发现程序可以正常运行了,但是在这里我们思考一个问题如果又增加一个汽车品牌 Benz,如果我们还是按照上面的代码先实例化 Benz 类,接着创建 driver 对象,然后将 Benz 对象传递给 open 方法,这样代码就会报错。


这是因为 open 方法所能接受的参数是 BMW 类型的参数,我们该如何解决这个问题呢?


这时一定会有人说向 IDriverService 接口和 DriverService 类中加入参数类型为 Benz 的 open 方法啊。那么这样真的好吗?如果我们需要增加十个汽车品牌岂不是要向接口和类中分别增加 10 个参数不同的 open 方法吗?这样我们的代码就不稳定了。


我想一定有部分读者想到了泛型,只需要创建一个抽象 AbstractCar 并让不同的汽车品牌类都继承这个抽象(这里我们使用抽象类),然后指定泛型类型必须是 AbstractCar 的继承类,这样能解决这个问题了。


下面我们首先新建一个名叫 AbstractModel 的类库,然后创建 AbstractCar 抽象类,最后让所有汽车品牌都继承这个抽象类。以下代码是根据前面所述修改后的部分代码法代码。


// 抽象类public class AbstractCar{    //more code}
// 不同汽车品牌类继承抽象类 AbstractCarpublic class BMW: AbstractCar{ public BMW() { Console.WriteLine("我是 BMW"); }}public class Benz:AbstractCar{ public Benz() { Console.WriteLine("我是 Benz"); }}
//服务抽象和服务实现都设置 Open 方法为泛型方法//并且泛型类型必须是 AbstractCar 及其子类public interface IDriverService{ void Open<T>(T car) where T: AbstractCar;}public class DriverService: IDriverService{ public void Open<T>(T car)where T : AbstractCar { string name = car.GetType().Name; Console.WriteLine($"我开动了{name.ToUpper()}牌子的车"); }}
class Program{ static void Main(string[] args) { BMW bmw = new BMW(); IDriverService driverService = new DriverService(); driverService.Open<BMW>(bmw); Benz benz = new Benz(); driverService.Open<Benz>(benz); Console.Read(); }}
复制代码


一般来说在考虑到 IoC 性能和方便性方面不推荐使用泛型来解决前面所说的问题,并且这种方式也和我们前面所说的高层不依赖于细节相悖,因此首选的应该是让 open 方法的参数类型设置为 Benz 和 BMW 共同继承的抽象类 AbstractCar。下面是根据前面所述修改后的 open 方法的代码。


public interface IDriverService{    //让 open 方法的参数类型设置为 Benz 和 BMW 共同继承的抽象类 AbstractCar    void Open(AbstractCar car);}public class DriverService: IDriverService{    public void Open(AbstractCar car)    {        string name = car.GetType().Name;        Console.WriteLine($"我开动了{name.ToUpper()}牌子的车");    }}
复制代码


到目前为止,我们已经实现了部分依赖倒置,为什么说是实现了部分呢?因为通过前面所有代码可以看出我们只是一步一步的将等号左边进行了依赖倒置处理,然而等号右边却没有进行依赖倒置处理。下面我们就对这部分进行处理。一般来说针对这部分的依赖倒置处理会使用工厂模式并利用反射来实现,即获取到 DLL 并利用反射创建出需要的类的实例对象。这里需要先创建一个 Factory 类库,该类库里主要存放创建服务和实体的工厂,然后再利用反射进行创建实例。


//服务工厂public class ServiceFactory{    public static IDriverService Create()    {        //读取服务 dll        Assembly assembly = Assembly.LoadFrom("Service.dll");        Type type = assembly.GetType("Service.DriverService");        //创建实例        object obj = Activator.CreateInstance(type);        return (IDriverService)obj;    }}
class Program{ static void Main(string[] args) { BMW bmw = new BMW(); //直接调用工厂类来创建服务 IDriverService driverService = ServiceFactory.Create(); driverService.Open(bmw); Benz benz = new Benz(); driverService.Open(benz); Console.Read(); }}
复制代码


上述代码我们把创建 DriverService 实例的方式改为了依赖倒置的方式创建,但是你是否发现这个方法依赖于 Service.dll,这就又变成了我们前面所说的高层直接依赖于底层,如果将当前的 Service.dll 换成新的 dll 那么我们就不得不改变这个方法中的代码。要解决这个问题我们可以将依赖的 dll 放入到配置文件中, CreateDriverService 方法读取配置文件来创建实例。下面是修正后的代码。


<appSettings>    <add key="DriverServiceDLL" value="Service:DriverService"/></appSettings>
复制代码


public class ServiceFactory{    private static string ServiceDic = ConfigurationManager.AppSettings["DriverServiceDLL"];    public static IDriverService Create()    {        string[] serice = ServiceDic.Split(':');        Assembly assembly = Assembly.LoadFrom($"{serice[0]}.dll");        Type type = assembly.GetType($"{serice[0]}.{serice[1]}");        object obj = Activator.CreateInstance(type);        return (IDriverService)obj;    }}
复制代码


作业:到目前我们以前完成了 Service 的依赖倒置, Model 的依赖倒置还没完成,那么我在这里给读者留一个作业:请根据前面所讲的内容自己动手完成 Model 的依赖倒置。完成后再接着看后面的内容,我想你一定会更有收获。


讲到这里你是否发现我们一直都是利用默认的无参构造函数来进行创建实例对象的,那么如果一个对象的创建需要依赖另一个对象呢(带参构造函数)?例如我们在创建 BMW 时需要依赖于 Benz,如果还是按照上面的方法创建的话在代码运行的时候就会报错提示未找到构造函数的参数。


要解决这个问题我们可以在创建对象时,先把该对象以来的对象创建出来然后再创建该对象,这种操作就叫做依赖注入(DI)。下面的代码就是根据 DI 的定义修改出来的。


 //让 BMW 继承于 IBMW,且 BMW 依赖于 Benz 的抽象public class BMW: AbstractCar,IBMW{    public BMW(IBenz benz)    {        Console.WriteLine("我是 BMW,我依赖于 BENZ");    }}//修改 Open 方法public interface IDriverService{    void Open(IBenz benz);    void Open(IBMW bmw);}public class DriverService: IDriverService{    public void Open(IBenz benz)    {        Console.WriteLine($"我开动了 Benz 牌子的车");    }
public void Open(IBMW bmw) { Console.WriteLine($"我开动了 BMW 牌子的车"); }}//通过反射实例化 BMW 和 Benz 对象public static IBMW CreateBMW(){ Assembly assembly = Assembly.LoadFrom($"Model.dll"); Type type = assembly.GetType($"Model.BMW"); //创建 BMW 所以来的 Benz 对象,并作为参数在创建 BMW 实例的时候传递个 BMW 的构造函数 object objBenz = CreateBenz(); object obj = Activator.CreateInstance(type,new object[] { objBenz }); return (IBMW)obj;}
public static IBenz CreateBenz(){ Assembly assembly = Assembly.LoadFrom($"Model.dll"); Type type = assembly.GetType($"Model.Benz"); object obj = Activator.CreateInstance(type); return (IBenz)obj;}class Program{ static void Main(string[] args) { IBMW bmw = ModelFactory.CreateBMW(); IDriverService driverService = ServiceFactory.Create(); driverService.Open(bmw); Console.Read(); }}
复制代码


Tip:ICO 是目标,DI 是实现 IoC 的一种方式。


下面我们来思考一个问题,如果实例的创建是层层依赖的(如图四),如果按照上面的代码来处理的话岂不是要不断地创建依赖的对象吗,况且我们不知道到底有多少层依赖。那么有没有一次创建完成所有依赖的方法呢?答案是有的,下一小节我们将在本小节的基础上创建一个可以处理多层依赖的 IoC。



图四

DIY 自己的 IoC

依赖注入容器是 IoC 的灵魂没有依赖注入容器就无所谓 IoC,因此本小节的核心就是依赖注入容器的开发,开发完依赖注入容器我们的 IoC 就可以说是完成了。


一般来说依赖注入容器包含如下两个方法 Add 和 GetService 方法,Add 方法主要用于注册抽象和具体的映射关系,依赖关系的存储使用 KV 的形式(本小节使用字典来存储),其中 K 为抽象的全名称,V 为实现抽象的类。首先我们需要创建一个名字叫 IoC 的类库,并在其下面创建一个依赖容器类 Container,下面的代码就是 Add 方法的具体实现代码,其中 serviceType.FullName 取的是抽象的全名称,这样就能保证写入到字典中的抽象类不会因为不同类库下存在相同名称的抽象而导致注册失败。


public interface IContainer{    void Add(Type serviceType, Type implementationType);    T GetService<T>();}public class Container{    private static Dictionary<string, Type> mapping = new Dictionary<string, Type>();    public static void Add(Type serviceType,Type implementationType)    {        mapping.Add(serviceType.FullName,implementationType);    }}
复制代码


GetService 方法主要用于从字典中把映射关系取出来并创建相应的实例,下面是 GetService 方法的实现。


public static T GetService<T>(){    Type type= mapping[typeof(T).FullName];    object obj = Activator.CreateInstance(type);    return (T)obj;}
复制代码


到这里,我们创建了一个简单的依赖注入容器,它的 GetService 方法调用无参构造函数创建实例对象,但是如果需要带参构造函数来创建实例对象的话我们这个方法就不适合了。因此我们就需要先构造当前类构造函数需要的参数,然后再去创建当前类的实例对象。这时一定有同学心中有一个疑问,如果一个类有多个构造函数我们该用哪个构造函数呢?其实这里有一个所有 IoC 所遵循的默认规则:如果存在多个构造函数时,就是用参数最多的那个构造函数来创建参数和实例对象。下面是根据前面所述修改后的代码。


public static T GetService<T>(){    Type type = mapping[typeof(T).FullName];    //获取 T 所有的构造函数    var ctorArray = type.GetConstructors();    //获取参数最多的构造函数    var ctor = ctorArray.OrderByDescending(p => p.GetParameters().Length).FirstOrDefault();    //获取构造函数所有参数    var paras = ctor.GetParameters();    List<object> ctorParas = new List<object>();    foreach (var para in paras)    {        Type paraType = para.ParameterType;        Type paraTargetType = mapping[paraType.FullName];        var target = Activator.CreateInstance(paraTargetType);        ctorParas.Add(target);    }    object obj = Activator.CreateInstance(type, ctorParas.ToArray());    return (T)obj;}
复制代码


我们解决了多构造函数的问题,下面再来解决一个问题:层层依赖问题。如果依赖是多层的(等于两层)就必须把每层的依赖都创建出来才,因此可以使用递归的方式来解决这个问题。


public static T GetService<T>(){    Type type = mapping[typeof(T).FullName];    return (T)GetService(type);}//递归实例化创建对象所需的参数private static object GetService(Type type){    var ctorArray = type.GetConstructors();    var ctor = ctorArray.OrderByDescending(p => p.GetParameters().Length).FirstOrDefault();    var paras = ctor.GetParameters();    List<object> ctorParas = new List<object>();    foreach (var para in paras)    {        Type paraType = para.ParameterType;        Type paraTargetType = mapping[paraType.FullName];        object target = GetService(paraTargetType);        ctorParas.Add(target);    }    return Activator.CreateInstance(type, ctorParas.ToArray());}
复制代码


上面的代码已经可以支持无限层级的依赖,那么如果使用 IoC 的程序员要自己指定实例化对象所使用的构造函数该怎么办呢?这个时候我们可以使用特性来解决,首先我们定义一个特性,并指定这个特性只能在构造函数上使用,然后使用这就可以把这个特性加在它们想要让 IoC 使用的构造函数上,IoC 的 GetService 方法会检查是否有构造函数带有这个特性,如果有就是指定的构造函数创建实例对象,反之就是用参数最多的那个构造函数来创建实例对象。


 //创建一个特性并设置该特性只能用在构造函数上[AttributeUsage(AttributeTargets.Constructor)]public class ThisConstructorAttribute:Attribute{}//修改 Container 类中的 GetServce 方法private static object GetService(Type type){    var ctorArray = type.GetConstructors();    ConstructorInfo ctor = null;    //找出所有构造函数中具有 ThisConstructorAttribute 特性的构造函数    ctor = ctorArray.Where(p => p.IsDefined(typeof(ThisConstructorAttribute), true)).FirstOrDefault();    //如果为 null 则使用参数最多的构造函数    if (ctor == null)    {        ctor = ctorArray.OrderByDescending(p => p.GetParameters().Length).FirstOrDefault();    }    var paras = ctor.GetParameters();    List<object> ctorParas = new List<object>();    foreach (var para in paras)    {        Type paraType = para.ParameterType;        Type paraTargetType = mapping[paraType.FullName];        object target = GetService(paraTargetType);        ctorParas.Add(target);    }    return Activator.CreateInstance(type, ctorParas.ToArray());}
复制代码

扩展一下

前面所讲的都是使用构造函数注入,本小节我们扩展一下 IoC 让它可以支持属性注入和方法注入。


1. 属性注入


所谓的属性注入就是找出对象中需要注入的属性并实例化属性。这里需要注意的是实例对象中的属性并非都需要注入,因此我们必须在属性上加上特性来告诉 IoC 哪些属性必须注入。


[AttributeUsage(AttributeTargets.Property)]public class ThisPropertyAttribute:Attribute{}//修改 Container 类中的 GetServce 方法private static object GetService(Type type){    var ctorArray = type.GetConstructors();    ConstructorInfo ctor = null;    //找出所有构造函数中具有 ThisConstructorAttribute 特性的构造函数    ctor = ctorArray.Where(p => p.IsDefined(typeof(ThisConstructorAttribute), true)).FirstOrDefault();    //如果为 null 则使用参数最多的构造函数    if (ctor == null)    {        ctor = ctorArray.OrderByDescending(p => p.GetParameters().Length).FirstOrDefault();    }    var paras = ctor.GetParameters();    List<object> ctorParas = new List<object>();    foreach (var para in paras)    {        Type paraType = para.ParameterType;        Type paraTargetType = mapping[paraType.FullName];        object target = GetService(paraTargetType);        ctorParas.Add(target);    }    object obj = Activator.CreateInstance(type, ctorParas.ToArray());    var attrArry = type.GetProperties().Where(p => p.IsDefined(typeof(ThisPropertyAttribute), true));    foreach (var attr in attrArry)    {        Type attrType = attr.PropertyType;        Type attrImpType = mapping[attrType.FullName];        object attrInstance = GetService(attrImpType);        attr.SetValue(obj, attrInstance);    }    return obj;}
复制代码


2. 方法注入


方法注入在 IoC 中使用较少,所谓方法注入就是当我们既没有通过构造函数去实例化一个属性也没有通过属性注入去实例化一个属性,这时就可以使用方法去实例化一个属性。方法注入和属性注入一样也需要通过特性来告知 IoC 对哪个方法进行注入。


[AttributeUsage(AttributeTargets.Method)]public class ThisMethodAttribute:Attribute{}//修改 Container 类中的 GetServce 方法private static object GetService(Type type){    var ctorArray = type.GetConstructors();    ConstructorInfo ctor = null;    //找出所有构造函数中具有 ThisConstructorAttribute 特性的构造函数    ctor = ctorArray.Where(p => p.IsDefined(typeof(ThisConstructorAttribute), true)).FirstOrDefault();    //如果为 null 则使用参数最多的构造函数    if (ctor == null)    {        ctor = ctorArray.OrderByDescending(p => p.GetParameters().Length).FirstOrDefault();    }    var paras = ctor.GetParameters();    List<object> ctorParas = new List<object>();    foreach (var para in paras)    {        Type paraType = para.ParameterType;        Type paraTargetType = mapping[paraType.FullName];        object target = GetService(paraTargetType);        ctorParas.Add(target);    }    object obj = Activator.CreateInstance(type, ctorParas.ToArray());    //属性注入    var attrArry = type.GetProperties().Where(p => p.IsDefined(typeof(ThisPropertyAttribute), true));    foreach (var attr in attrArry)    {        Type attrType = attr.PropertyType;        Type attrImpType = mapping[attrType.FullName];        object attrInstance = GetService(attrImpType);        attr.SetValue(obj, attrInstance);    }    //方法注入    var methodArry = type.GetMethods().Where(p => p.IsDefined(typeof(ThisMethodAttribute), true));    foreach (var method in methodArry)    {        List<object> methodParas = new List<object>();        foreach (var para in method.GetParameters())        {            Type paraType = para.ParameterType;            object methodInstance = GetService(mapping[paraType.FullName]);            methodParas.Add(methodInstance);        }        method.Invoke(obj, methodParas.ToArray());    }    return obj;}
复制代码


3. 多实现注入


到目前为止我们的 IoC 基本完成,为什么说是基本完成呢?因为在实际项目开发中大部分情况是一个抽象对应多个实现,但是 IoC 是使用字典来存储映射关系的,字典的特点我们斗志到 K 是不能重复的,因此当向 IoC 中注册一个抽象的多个实现时就会报错。解决这个问题也很简单,方法有两种:一种是使用后来居上原则,也就是说当再次注册抽象的另一个实现时将前一个实现替换成新的实现(这种方式不推荐)。另一种方法是使用别名,即在注册的时候给关系映射指定一个唯一的名称。下面是经过修正过的代码。


public class Container: IContainer    {        private Dictionary<string, Type> mapping = new Dictionary<string, Type>();        private string GetK(Type serviceType,string shortName)        {            return $"{serviceType.FullName}_{shortName}";        }        public void Add(Type serviceType, Type implementationType, string shortName = null)        {            string k = GetK(serviceType,shortName);            mapping.Add(k , implementationType);        }
public T GetService<T>(string shortName=null) { Type type = mapping[GetK(typeof(T), shortName)]; return (T)GetService(type); }
private object GetService(Type type) { var ctorArray = type.GetConstructors(); ConstructorInfo ctor = null; //找出所有构造函数中具有 ThisConstructorAttribute 特性的构造函数 ctor = ctorArray.Where(p => p.IsDefined(typeof(ThisConstructorAttribute), true)).FirstOrDefault(); //如果为 null 则使用参数最多的构造函数 if (ctor == null) { ctor = ctorArray.OrderByDescending(p => p.GetParameters().Length).FirstOrDefault(); } var paras = ctor.GetParameters(); List<object> ctorParas = new List<object>(); foreach (var para in paras) { Type paraType = para.ParameterType; string shortName = GetK(paraType, null); Type paraTargetType = mapping[GetK(paraType, shortName)]; object target = GetService(paraTargetType); ctorParas.Add(target); } object obj = Activator.CreateInstance(type, ctorParas.ToArray()); //属性注入 var attrArry = type.GetProperties().Where(p => p.IsDefined(typeof(ThisPropertyAttribute), true)); foreach (var attr in attrArry) { Type attrType = attr.PropertyType; string shortName = GetK(attrType, null); Type attrImpType = mapping[GetK(attrType, shortName)]; object attrInstance = GetService(attrImpType); attr.SetValue(obj, attrInstance); } //方法注入 var methodArry = type.GetMethods().Where(p => p.IsDefined(typeof(ThisMethodAttribute), true)); foreach (var method in methodArry) { List<object> methodParas = new List<object>(); foreach (var para in method.GetParameters()) { Type paraType = para.ParameterType; string shortName = GetK(paraType, null); object methodInstance = GetService(mapping[GetK(paraType, shortName)]); methodParas.Add(methodInstance); } method.Invoke(obj, methodParas.ToArray()); } return obj; } }
复制代码

发布到 NuGet

将自己写的 IoC 发布到 NuGet 上一共分 4 步。


1. 配置包的项目属性


选择 项目>属性菜单命令,然后选择应用程序选项卡,在程序集名称字段中为包提供唯一标识符,接着选择**程序集信息...**按钮,此时会出现一个对话框输入清单中的要发布的包的其他属性,最后生成项目。


2. 生成初始清单


打开命令提示符并导航到包含 IOC.csproj 文件的项目文件夹下,命令行运行:nuget spec IOC.csproj,NuGet 会创建匹配项目名称的清单。


3. 运行 pack 命令


从包含 .nuspec 文件的文件夹中运行命令 nuget pack,NuGet 以 identifier-version.nupkg 形式在当前文件夹中生成 .nupkg 文件。


4. 发布


首先我们需要从 NuGet 网站获取到所需的发布密钥,最后使用 nuget push 命令将包发布到 NuGet 上。


nuget push MYIOC.1.0.0.nupkg 你的发布密钥
复制代码

小结

本篇文章主要讲解了 IoC 核心原理依赖倒置,并带领大家一步一步的实现了一个简单的 IoC,还实现了属性注入和方法注入,同时也将我们的 IoC 发布到了 NuGet 上。文章如有不足请批评指正。




本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。

发布于: 2021 年 04 月 03 日阅读数: 29
用户头像

喵叔

关注

还未添加个人签名 2020.01.14 加入

还未添加个人简介

评论

发布
暂无评论
动手撸一个IOC框架