一、IOC

1.什么是IOC?

控制反转(英语:Inversion of Control,缩写为IoC),是[面向对象编程]中的一种设计原则,可以用来减低计算机代码之间的[耦合度]其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup).

IoC:是一种设计模式

DI:是践行控制反转思想的一种方式

2.为什么要用IOC

因为IoC 控制反转是依赖抽象,而抽象是稳定的,不依赖细节,因为细节还可能会依赖其他细节,为了屏蔽细节,需要使用依赖注入去解决无限层级的对象依赖。

3.Net中常用的IoC容器

目前用的最多的是AutoFac和Castle,在.Net Core中框架内置了IOC容器,Unity和ObjectBuilder是相对比较久远的框架,用的比较少。

  1. AutoFac

  2. Castle

  3. Unity

  4. ObjectBuilder

二、如何手写实现?

1.基本设计

核心思想: 工厂 + 反射

首先我们想自己实现一个IoC容器其实并不难,我们在使用现有的IoC容器都知道,在使用前,我们需要先注册,然后才能使用

所以我们将工厂换成手动注册的方式,因为写一大堆if else 或者switch也不太美观,根据主流IoC的使用方式来以葫芦画瓢,如果后期继续完善功能加入程序集注入的话,还是得实现一个工厂,来省略手动注册

但是这次目标是实现一个简易版的IoC容器,我们先实现基础功能,待后面一步一步去完善,再加入一些新的功能,即我们不考虑性能或者扩展度,目的是循序渐进,在写之前我们先整理出 实现步骤和实现方式

  1. 方便接入和扩展,我们在这先定义一个容器接口 IManualContainer

  2. 定义ManualContainer继承实现IManualContainer

  3. 声明一个静态字典对象存储注册的对象

  4. 利用反射构建对象,考虑到性能可以加入Expression或者Emit的方式来做一些优化

classDiagram
IManualContainer <|-- ManualContainer
IManualContainer: +Register<TFrom, To>()
IManualContainer: +Resolve<Tinterface>()
class ManualContainer{
-Dictionary<string, Type> container
+Register()
+Resolve()
-CreateInstance()
}
public interface IManualContainer
{
void Register<TFrom, To>(string servicesName = null) where To : TFrom; Tinterface Resolve<Tinterface>(string servicesName = null);
}
2.要实现的功能

1.基本对象构造

2.构造函数注入

3.多级依赖和多构造函数及自定义注入

4.属性注入&方法注入

5.单接口多实现

三、编码实现及思路剖析

1.实现构造对象(单接口注入)

1.首先实现接口来进行编码私有字段 container用来存储注册的类型,key是对应接口的完整名称,Value是需要Resolve的类型。

2.泛型约束保证需要被Resolve类型 (To) 实现或者继承自注册类型 (TFrom)

public class ManualContainer : IManualContainer
{
//存储注册类型
private static Dictionary<string, Type> container =
new Dictionary<string, Type>(); //注册
public void Register<TFrom, To>(string servicesName = null) where To : TFrom
{
string Key = $"{typeof(TFrom).FullName}{servicesName}";
if (!container.ContainsKey(Key))
{
container.Add(Key, typeof(To));
}
}

1.实现构造对象,首先需要传入被构造的类型的抽象接口T

2.在Resolve中根据T作为Key,在存储容器中找到注册时映射的类型,并通过反射构造对象

   //构建对象
public TFrom Resolve<TFrom>(string servicesName = null)
{
string Key = $"{typeof(TFrom).FullName}{servicesName}";
container.TryGetValue(key, out Type target);
if(target is null)
{
return default(TFrom);
}
object t = Activator.CreateInstance(target);
}
}

1.首先我们准备需要的接口(ITestA)和实例(TestA)来利用容器来构造对象

public interface ITestA
{
void Run();
}
public class TestA : ITestA
{
public void Run()=> Console.WriteLine("这是接口ITestA的实现");
}

2.调用IoC容器来创建对象

IManualContainer container = new ManualContainer();
//注册到容器中
container.Register<ITestA, TestA>();
ITestA instance = container.Resolve<ITestA>();
instance.Run();
//out put "这是接口ITestA的实现"
2.构造函数注入

1.假设我们的TestA类中需要ITestB接口的实例或者其他更多类型的实例,并且需要通过构造函数注入,我们应该如何去完善我们的IoC容器呢?

public class TestA : ITestA
{
private ITestB testB = null;
//构造函数
public TestA(ITestB testB)=> this.testB = testB; public void Run()
{
this.testB.Run();
Console.WriteLine("这是接口ITestA的实现");
}
}

2.我们按照上面的步骤照常注册和构造对象,发现报错了,在Resolve()的时候,经过调试知道是使用反射构造的时候报错了,因为在构造TestA缺少构造参数,那么我们就需要在反射构造时加入参数。

  1. 先定义List<object>集合存储对象构造时需要的参数列表

  2. 通过需要被实例的目标类型找到类中的构造函数,暂不考虑多构造函数case

  3. 找到构造函数参数及类型,然后创建参数的实例加入List中,在反射构造时传入参数就解决了

   //完善Resolve构建对象函数
public TFrom Resolve<TFrom>(string servicesName = null)
{
string Key = $"{typeof(TFrom).FullName}{servicesName}";
container.TryGetValue(key, out Type target);
if(target is null)
{
return default(TFrom);
} //存储参数列表
List<object> paramList = new List<object>();
//找到目标类型的构造函数,暂不考虑多构造函数case
var ctor = target.GetConstructors().FirstOrDefault();
//找到参数列表
var ctorParams = ctor.GetParameters();
foreach (var item in ctorParams)
{
//参数类型
Type paramType = item.ParameterType;
string paramKey = paramType.FullName;
//找到参数注册时映射的实例
container.TryGetValue(paramKey, out Type ParamType);
//构造出实例然后加入参数列表
paramList.Add(Activator.CreateInstance(ParamType));
}
object t = Activator.CreateInstance(target,paramList);
}
}
3.多级依赖(递归)

根据上面我们目前实现的结果来看,这是解决了构造函数和多参数注入以及基本的构造对象问题,那现在问题又来了

  1. 如果是很多层的依赖该怎么办?

  2. 例如多个构造函数怎么办呢?

  3. 在多个构造函数中用户想自定义需要被注入的构造函数怎么办?

总结3点问题

  • 1.多级依赖问题

例如ITestB 的实例中依赖ITestC,一直无限依赖我们怎么解决呢?毫无疑问,做同样的事情,但是要无限做下去,就使用 递归,下面我们来改造我们的方法

  • 2.多个构造函数

    1. 取参数最多的方式注入 (AutoFac)

    2. 取并集方式注入 (ASP .NET Core)

  • 3.自定义注入

    我们可以使用特性标记的方式来实现,用户在需要被选择注入的构造函数上加入特性标签来完成


1.创建私有递归方法,这个方法的作用就是创建对象用

private object CreateInstance(Type type,string serviceName = null)
{
}

2.我们选择第一种方式实现,修改之前获取第一个构造函数的代码,选择最多参数注入

 //找到目标类型的构造函数,找参数最多的构造函数
ConstructorInfo ctor = null;
var ctors = target.GetConstructors();
ctor = ctors.OrderByDescending(x => x.GetParameters().Length).First();

3.自定义特性CtorInjection,可以使用户自定义选择

 //自定义构造函数注入标记
[AttributeUsage(AttributeTargets.Constructor)]
public class CtorInjectionAttribute : Attribute
{
}

4.最终代码

private object CreateInstance(Type type,string serviceName = null)
{
string key = $"{ type.FullName }{serviceName}";
container.TryGetValue(key, out Type target);
//存储参数列表
List<object> paramList = new List<object>(); ConstructorInfo ctor = null;
//找到被特性标记的构造函数作为注入目标
ctor = target.GetConstructors().FirstOrDefault(x => x.IsDefined(typeof(CtorInjectionAttribute), true)); //如果没有被特性标记,那就取构造函数参数最多的作为注入目标
if (ctor is null)
{
ctor = target.GetConstructors().OrderByDescending(x => x.GetParameters().Length).First();
}
//找到参数列表
var ctorParams = ctor.GetParameters();
foreach (var item in ctorParams)
{
//参数类型
Type paramType = item.ParameterType;
//递归调用构建对象
object paramInstance = CreateInstance(paramType);
//构造出实例然后加入参数列表
paramList.Add(paramInstance);
}
object t = Activator.CreateInstance(target);
return t;
} public TFrom Resolve<TFrom>()
{
return (TFrom)this.CreateInstance(typeof(TFrom));
}
4.属性注入&方法注入

1.自定义特性PropInjection,可以使用户自定义选择

 //自定义构造属性注入标记
[AttributeUsage(AttributeTargets.Property)]
public class PropInjectionAttribute : Attribute
{
}
//自定义构造方法注入标记
[AttributeUsage(AttributeTargets.Method)]
public class MethodInjectionAttribute : Attribute
{
}

2.遍历实例中被标记特性的属性。

3.获取属性的类型,调用递归构造对象函数。

4.设置目标对象的属性值。

5.方法注入也是同理,换汤不换药而已

private object CreateInstance(Type type,string serviceName = null)
{
string key = $"{ type.FullName }{serviceName}";
container.TryGetValue(key, out Type target);
//存储参数列表
List<object> paramList = new List<object>(); ConstructorInfo ctor = null;
//找到被特性标记的构造函数作为注入目标
ctor = target.GetConstructors().FirstOrDefault(x => x.IsDefined(typeof(CtorInjectionAttribute), true)); //如果没有被特性标记,那就取构造函数参数最多的作为注入目标
if (ctor is null)
{
ctor = target.GetConstructors().OrderByDescending(x => x.GetParameters().Length).First();
}
//找到参数列表
var ctorParams = ctor.GetParameters();
foreach (var item in ctorParams)
{
//参数类型
Type paramType = item.ParameterType;
//递归调用构建对象
object paramInstance = CreateInstance(paramType);
//构造出实例然后加入参数列表
paramList.Add(paramInstance);
}
object t = Activator.CreateInstance(target); //获取目标类型的被特性标记的属性<属性注入>
var propetys = target.GetProperties().Where(x => x.IsDefined(typeof(PropInjectionAttribute), true));
foreach (var item in propetys)
{
//获取属性类型
Type propType = item.PropertyType;
object obj = this.CreateInstance(propType);
//设置值
item.SetValue(t, obj);
} //获取目标类型的被特性标记的方法<方法注入>
var methods = target.GetMethods().Where(x => x.IsDefined(typeof(MethodInjectionAttribute), true)).ToList();
foreach (var item in methods)
{
List<object> methodParams = new List<object>();
foreach (var p in item.GetParameters())
{
//获取方法参数类型
Type propType = p.ParameterType;
object obj = this.CreateInstance(propType);
methodParams.Add(obj);
}
item.Invoke(t, methodParams);
}
return t;
} public TFrom Resolve<TFrom>()
{
return (TFrom)this.CreateInstance(typeof(TFrom));
}
5.单接口多实现

假设我们一个接口被多个实例实现,我们需要注入怎么操作呢?

毫无疑问我们应该想到在注入时取一个别名,没错正是这种方法,但是也会存在一个问题,我们取了别名之后只能解决注入的场景?那依赖接口的地方如何知道注入哪一个实例呢?

1.注册单接口多实例

container.Register<ITest, test1>("test1");
container.Register<ITest, test2>("test2");
ITest instance = container.Resolve<ITest>("test1");
ITest instance1 = container.Resolve<ITest>("test2");

2.创建标记特性,用来标记目标对象

[AttributeUsage(AttributeTargets.Parameter| AttributeTargets.Property)]
public class ParamterInjectAttribute : Attribute
{
public ParamterInjectAttribute(string nickName) => NickName = nickName;
public string NickName { get; private set; }
}

3.我们需要在依赖“单接口多实例的类中”使用时告诉参数,我们需要的实例,依然使用参数特性标记

public class use : IUse
{
private ITest _Test = null;
//告诉构造函数依赖ITest接口时使用别名为test1的实例
public BLL1([ParamterInjectAttribute("test1")] ITest接口时使用别名为test1的实例 Test)
{
this._Test = Test;
}
}

4.在构造对象时查找是否被特性标记,然后构造对象

foreach (var item in ctor.GetParameters())
{
Type paramType = item.ParameterType;
string nickName = string.Empty;
//查找构造函数的参数是否被特性标记
if (item.IsDefined(typeof(ParamterInjectAttribute), true))
{
//找到被标记需要构造的实例别名
nickName = item.GetCustomAttribute<ParamterInjectAttribute>().NickName;
}
//根据别名创建对象
object instance = CreateInstance(paramType,nickName);
if (instance != null)
{
paramList.Add(instance);
}
}

四、总结

目前还不是很完善,只是实现了属性,方法,以及构造函数注入,很多必要功能还没有,下一步将在现有代码基础上利用Emit的方式来创建对象,加入基本的验证环节以提高健壮性,加入生命周期管理,和AOP扩展。

第一版最终代码

 public class ManualContainer : IManualContainer
{
private static Dictionary<string, Type> container = new Dictionary<string, Type>();
public void Register<TFrom, To>(string servicesName = null) where To : TFrom
{
string Key = $"{typeof(TFrom).FullName}{servicesName}";
if (!container.ContainsKey(Key))
{
container.Add(Key, typeof(To));
}
} public Tinterface Resolve<Tinterface>(string serviceName = null)
{
return (Tinterface)this.CreateInstance(typeof(Tinterface), serviceName);
} private object CreateInstance(Type type, string serviceName = null)
{
string key = $"{ type.FullName }{serviceName}";
container.TryGetValue(key, out Type target);
object instance = ctorInjection(target);
propInjection(target, instance);
methodInjection(target, instance);
return instance;
} /// <summary>
/// 构造函数注入
/// </summary>
/// <param name="target">需要被创建的类型</param>
/// <returns>被创建的实例</returns>
private object ctorInjection(Type targetType)
{
List<object> paramList = new List<object>();
ConstructorInfo ctor = null;
ctor = targetType.GetConstructors().FirstOrDefault(x => x.IsDefined(typeof(CtorInjectionAttribute), true));
if (ctor is null)
{
ctor = targetType.GetConstructors().OrderByDescending(x => x.GetParameters().Length).First();
}
foreach (var item in ctor.GetParameters())
{
Type paramType = item.ParameterType;
string nickName = GetNickNameByAttribute(item);
object instance = CreateInstance(paramType, nickName);
if (instance != null)
{
paramList.Add(instance);
}
}
object t = Activator.CreateInstance(targetType, paramList.ToArray());
return t;
} /// <summary>
/// 属性注入
/// </summary>
/// <param name="targetType">需要被创建的类型</param>
/// <param name="sourceType">根据需要被创建的类型构造出的实例</param>
private void propInjection(Type targetType, object inststance)
{
var propetys = targetType.GetProperties().Where(x => x.IsDefined(typeof(PropInjectionAttribute), true));
foreach (var item in propetys)
{
Type propType = item.PropertyType;
string nickName = GetNickNameByAttribute(item);
object obj = this.CreateInstance(propType, nickName);
item.SetValue(inststance, obj);
}
} /// <summary>
/// 方法注入
/// </summary>
/// <param name="targetType">需要被创建的类型</param>
/// <param name="sourceType">根据需要被创建的类型构造出的实例</param>
private void methodInjection(Type targetType, object inststance)
{
List<object> methodParams = new List<object>();
var methods = targetType.GetMethods().Where(x => x.IsDefined(typeof(MethodInjectionAttribute), true)).ToList();
foreach (var item in methods)
{
foreach (var p in item.GetParameters())
{
Type propType = p.ParameterType;
string nickName = GetNickNameByAttribute(item);
object obj = this.CreateInstance(propType, nickName);
methodParams.Add(obj);
}
item.Invoke(inststance, methodParams.ToArray());
}
} private string GetNickNameByAttribute(ICustomAttributeProvider provider)
{
if (provider.IsDefined(typeof(ParamterInjectAttribute), true))
{
ParamterInjectAttribute attribute = provider.GetCustomAttributes(typeof(ParamterInjectAttribute), true)[0] as ParamterInjectAttribute;
return attribute.NickName;
}
return string.Empty;
}
}

手写IOC实践的更多相关文章

  1. 手写IOC实现过程

    一.手写ioc前基础知识 1.什么是IOC(Inversion of Control 控制反转)? IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合.更优良 ...

  2. 闭关修炼180天--手写IOC和AOP(xml篇)

    闭关修炼180天--手写IOC和AOP(xml篇) 帝莘 首先先分享一波思维导图,涵盖了一些Spring的知识点,当然这里并不全面,后期我会持续更新知识点. 在手写实现IOC和AOP之前(也就是打造一 ...

  3. 手写HashMap实践

    1.什么是HashMap 2.源码分析 3.手写实现 4.不足 一.什么是HashMap hash散列 将一个任意长度通过某种算法(hash函数算法)换成一个固定值 map: 地图x,y 存储 总结: ...

  4. 初学源码之——银行案例手写IOC和AOP

    手写实现lOC和AOP 上一部分我们理解了loC和AOP思想,我们先不考虑Spring是如何实现这两个思想的,此处准备了一个『银行转账」的案例,请分析该案例在代码层次有什么问题?分析之后使用我们已有知 ...

  5. 第三节:工厂+反射+配置文件(手写IOC)对缓存进行管理。

    一. 章前小节 在前面的两个章节,我们运用依赖倒置原则,分别对 System.Web.Caching.Cache和 System.Runtime.Cacheing两类缓存进行了封装,并形成了ICach ...

  6. 手写IOC容器

    IOC(控制翻转)是程序设计的一种思想,其本质就是上端对象不能直接依赖于下端对象,要是依赖的话就要通过抽象来依赖.这是什么意思呢?意思就是上端对象如BLL层中,需要调用下端对象的DAL层时不能直接调用 ...

  7. 手写IOC框架

    1.IOC框架的设计思路 ① 哪些类需要我们的容器进行管理 ②完成对象的别名和对应实例的映射装配 ③完成运行期对象所需要的依赖对象的依赖

  8. 我自横刀向天笑,手写Spring IOC容器,快来Look Look!

    目录 IOC分析 IOC是什么 IOC能够带来什么好处 IOC容器是做什么工作的 IOC容器是否是工厂模式的实例 IOC设计实现 设计IOC需要什么 定义接口 一:Bean工厂接口 二:Bean定义的 ...

  9. 手写IOC-SPRINGPMVC-CONNPOOL

    (一)  手写IOC思路 1.扫包,将所有class文件加载到内存,判断类上是否加了ExtService注解,有就添加入map中 ,  map<String ,Object>:  key是 ...

随机推荐

  1. Beyond compare 4.2.3 激活和秘钥

    安装完 Beyond Compare 4.2.3 以后.打开输入密匙是不是会跳到官网去?不用慌,我们只需要删除你安装Beyond Compare 4目录下的 BCUnrar.dll 文件,然后再打开就 ...

  2. Python基础(序列化)

    #pickling import pickle,json # d = dict(name='傻狗1',age=300,score=100) # d1 = pickle.dumps(d)#pickle. ...

  3. ajax的post请求获取kfc官网数据

    # _*_ coding : utf-8 _*_# @Time : 2021/11/2 13:45# @Author : 秋泊酱 # 1页 # http://www.kfc.com.cn/kfccda ...

  4. Redis | 第一部分:数据结构与对象 上篇《Redis设计与实现》

    目录 前言 1. 简单动态字符串 1.1 SDS的定义 1.2 空间预分配与惰性空间释放 1.3 SDS的API 2. 链表 2.1 链表与节点的定义 2.2 链表的API 3. 字典 3.1 哈希表 ...

  5. 【从头到脚品读 Linux 0.11 源码】第一回 最开始的两行代码

    从这一篇开始,您就将跟着我一起进入这操作系统的梦幻之旅! 别担心,每一章的内容会非常的少,而且你也不要抱着很大的负担去学习,只需要像读小说一样,跟着我一章一章读下去就好. 话不多说,直奔主题.当你按下 ...

  6. [noi38]游戏

    用线段数维护一段区间内的两个信息:1.需要多少经验就可以让有一个人升级,2.等级和.单点修改直接暴力做就可以,区间修改考虑如果这个区间不会产生升级就不递归下去而是打上懒标记. 考虑这个算法的时间复杂度 ...

  7. Scrum精髓读书笔记

    Scrum精髓 四 . Sprint Sprint的定义 Scrum在最长一个月的迭代或周期中安排工作,一般为2个星期,这些迭代或周期称为Sprint Sprint提供基本的Scrum骨架,大多数其他 ...

  8. char数据可以放入int[]中会自动转换

    int[] ary ={'b','c','a','d','e','f'};System.out.println(ary[0]);//98String str = new String(ary, 2, ...

  9. 基于Docker搭建Maven私服Nexus,Nexus详解

    备注:首先在linux环境安装Java环境和Docker,私服需要的服务器性能和硬盘存储要高一点,内存不足可能到时启动失败,这里以4核8GLinux服务器做演示 一:基于Docker安装nexus3 ...

  10. vue项目中使用 SheetJS / js-xlsx 导入文件

    原表格样式; 导入效果: 1.  安装 npm install xlsx 2. 在App.vue 中引入xlsx import * as XLSX from 'xlsx'; // 数据导出导入所需要的 ...