一、IOC 控制反转
面向对象编程的一种设计原则,可以用来降低代码之间的耦合度。
IOC容器:就是专门做实例化的工厂。
二、依赖倒置
一种设计模式,以前是高层依赖低层,现在不这么干了,所有层级都依赖于抽象,并且负责实现自己继承的抽象。
三、实现
IOC实现的三步:1、注册Register,先告诉IOC抽象与实现的关系。2、有了关系才有了Resolve生成实例。3、有了实例就能依赖IOC容器注入实例。
1、实现register和Resolve
在上一节的common中添加两个类,叫TestContainer和接口ITestContainer。
public interface ITestContainer { /// <summary> /// 注册抽象与实例的映射关系 /// </summary> /// <typeparam name="IT"></typeparam> /// <typeparam name="T"></typeparam> void Register<IT, T>() where T : IT; /// <summary> /// 通过映射返回实例 /// </summary> /// <typeparam name="IT"></typeparam> /// <returns></returns> IT Resolve<IT>(); }
public class TestContainer:ITestContainer { /// <summary> /// 映射字典 /// </summary> private Dictionary<string, Type> RegisterTypes = new Dictionary<string, Type>(); /// <summary> /// 注册 /// </summary> /// <typeparam name="IT">抽象</typeparam> /// <typeparam name="T">实现抽象细节</typeparam> public void Register<IT, T>() where T : IT { string key = typeof(IT).FullName; if (!RegisterTypes.ContainsKey(key)) { RegisterTypes.Add(key, typeof(T)); } } /// <summary> /// 返回实例 /// </summary> /// <typeparam name="IT"></typeparam> /// <returns></returns> /// <exception cref="Exception"></exception> public IT Resolve<IT>() { string key = typeof(IT).FullName; if (!RegisterTypes.ContainsKey(key)) { throw new Exception($"未注册{key},不能被实例化!"); } Type instanceType = RegisterTypes[key]; Object instance = Activator.CreateInstance(instanceType); return (IT)instance; } }
如上代码,Register方法将注册进来的抽象名和实例的映射保存到一个字典里,Resolve方法,通过传进来的抽象去注册映射字典里找到对应的实现细节类,并返回该实现类的实例。
ITestContainer container=new TestContainer(); container.Register<IUserDal,UserMySqlDal>(); IUserDal userDal = container.Resolve<IUserDal>(); IUserBll userBll = Factory.CreateBll(userDal); bool result= userBll.Login(1); Console.WriteLine(result);
如上,修改一下UI层的代码,先调用Register注册userDal的关联,然后再用Resolve实例化方法返回实例。可以实现。
2、实现构造函数注入实例(依赖注入)
上面我们只是实现了简单的一个注册和实例化方法。只能对UserDal这种只有无参构造方法的类进行实例化。遇到像UserBll这个需要传一个IUserDal参数的构造方法,那就会报错了。
那么如果要实现这种构造函数带一个参数的类呢?只有先实例了UserDal再去实例UserBll。修改一下revolse的方法。
public IT Resolve<IT>() { string key = typeof(IT).FullName; if (!RegisterTypes.ContainsKey(key)) { throw new Exception($"未注册{key},不能被实例化!"); } Type instanceType = RegisterTypes[key]; ConstructorInfo userConstructor = null; //使用的构造方法 ConstructorInfo[] constructors=instanceType.GetConstructors();//获取类所有的构造方法 //获取构造方法 if (constructors != null && constructors.Length > 0) { foreach (var constructor in constructors) { userConstructor = constructor; //暂时取第一个 break; } } //保存参数的实例 List<object> constructorParamInfos = new List<object>(); //获取构造方法的参数 if (userConstructor != null) { ParameterInfo[] paramInfos= userConstructor.GetParameters(); if (paramInfos != null && paramInfos.Length > 0) { foreach (ParameterInfo pInfo in paramInfos) { //对参数进行实例化 Type pType= pInfo.ParameterType; string pTypeKey = pType.FullName; if (RegisterTypes.ContainsKey(pTypeKey)) { Type pInstanceType = RegisterTypes[pTypeKey]; Object pInstance = Activator.CreateInstance(pInstanceType); constructorParamInfos.Add(pInstance); } else { throw new Exception($"未注册{pTypeKey},不能被实例化!"); } } } } Object instance = null; if(constructorParamInfos.Count()>0) { instance = Activator.CreateInstance(instanceType,constructorParamInfos.ToArray()); } else { instance = Activator.CreateInstance(instanceType); } return (IT)instance; }
如上述代码,先通过反射获取实例类型的所有构造方法,暂时先取第一个构造方法,获取构造方法中的所有参数,遍历参数并实现参数的实例化加到参数实例集合里。这叫什么?这个实现就叫做依赖注入。即在构造B对象的过程中发现B依赖于A对象,那么在构造B对象的时候能够动态的构造A对象并传递给B,让B能够正常的通过构造方法进行实例化。
3、依赖注入的好处
下面对比一下用factory实例UserBll和使用依赖注入实例UserBll。依赖注入屏蔽掉了实例UserDAL的细节。而一般工厂必须要接收UserDal的实例才能去实例UserBLL。那如果UserBll的构造方法依赖100个其他实例,那么还需要先实例化100个实例再传给UserBll构造方法。这代码写下来,手酸不说,这样的代码好吗?所以依赖注入的好处便是上端不再需要去实现实例UserBll的所有细节,就是不管你UserBll的实例化需要依赖多少其他实例,我只需要写一条语句就行。
ITestContainer container = new TestContainer(); container.Register<IUserDal, UserMySqlDal>(); container.Register<IUserBll, UserBll>(); IUserBll userBll = container.Resolve<IUserBll>(); //依赖注入实现实例 //IUserDal userDal = container.Resolve<IUserDal>(); // //IUserBll userBll = Factory.CreateBll(userDal); //工厂生成实例 bool result = userBll.Login(1); Console.WriteLine(result);
4、递归实现多层依赖注入
上面我们实现了一层的构造方法调用,那如果UserDal的构造方法需要传入一个另一个对象呢?即UserBll需要先构造UserDal,要构造UserDal需要先构造UserContext。这个时候怎么实现呢?这种结构是不是树结构?那么遍历树的算法有很多种(有兴趣可以去研究下数据结构与算法)。递归应该是最简单的了,那么就用递归来实现吧。递归存在内存泄露的风险,实际项目中最好避免使用。修改代码
加一个IUserContext,和UserContext,再给UserMysqlDal加上构造函数。
public class UserMySqlDal:IUserDal { private IUserContext UserContext { get; set; } public UserMySqlDal(IUserContext userContext) { UserContext = userContext; } public object Find(int id) { return null; } }
public IT Resolve<IT>() { Type type = typeof(IT); object instance = ResolveObject(type); return (IT)instance; } /// <summary> /// 递归实例化对象 /// </summary> /// <param name="objectType"></param> /// <returns></returns> /// <exception cref="Exception"></exception> private object ResolveObject(Type objectType) { string key = objectType.FullName; if (!RegisterTypes.ContainsKey(key)) { throw new Exception($"未注册{key},不能被实例化!"); } Type instanceType = RegisterTypes[key]; ConstructorInfo userConstructor = null; //使用的构造方法 ConstructorInfo[] constructors=instanceType.GetConstructors();//获取类所有的构造方法 //获取构造方法 if (constructors != null && constructors.Length > 0) { foreach (var constructor in constructors) { userConstructor = constructor; //暂时取第一个 break; } } //保存参数的实例 List<object> constructorParamInfos = new List<object>(); //获取构造方法的参数 if (userConstructor != null) { ParameterInfo[] paramInfos= userConstructor.GetParameters(); if (paramInfos != null && paramInfos.Length > 0) { foreach (ParameterInfo pInfo in paramInfos) { //对参数进行实例化 Type pType= pInfo.ParameterType; Object pInstance = ResolveObject(pType); constructorParamInfos.Add(pInstance); //string pTypeKey = pType.FullName; //if (RegisterTypes.ContainsKey(pTypeKey)) //{ // Type pInstanceType = RegisterTypes[pTypeKey]; // Object pInstance = Activator.CreateInstance(pInstanceType); // constructorParamInfos.Add(pInstance); //} //else //{ // throw new Exception($"未注册{pTypeKey},不能被实例化!"); //} } } } Object instance = null; if(constructorParamInfos.Count()>0) { instance = Activator.CreateInstance(instanceType,constructorParamInfos.ToArray()); } else { instance = Activator.CreateInstance(instanceType); } return instance; }
5、处理构造方法优先选择
上面的代码选择构造方法构造的时候暂时选取的是第一个。这个显然是不合理的。Unity和autofac采用的是参数最多的构造方法,而.netcore内置的IOC采用的时候参数的类型最多的构造方法。这两种默认算法这里就不写了,还有另一种叫指定方式。指定方式怎么指定呢?可以利用特性来指定。
public class DefaultInstantiateAttribute: Attribute { }
//获取构造方法 if (constructors != null && constructors.Length > 0) { //获取第一个特性指定的构造方法 var defaultConstructor = constructors.Where(c => c.IsDefined(typeof(DefaultMemberAttribute), true)).ToList(); ; if (defaultConstructor != null&& defaultConstructor.Count()>0) { userConstructor = defaultConstructor[0]; } else { foreach (var constructor in constructors) { ///实现构造方法的优先选择 userConstructor = constructor; //暂时取第一个 break; } } }
四、总结
IOC(控制反转):是一种设计模式。
DI(依赖注入):是一种实现方式。