title: 深入理解IOC并自己实现IOC容器
categories: 后端
tags:
- .NET

背景介绍

平时开发的时候我们经常会写出这种代码:

var optionA=new A(...);
var configB=new B(...);
var configC=new C(...);
...
var targetObj=new Target(configA,configB,configC,...);

为了初始化一个需要的类,通常需要在构造的时候把它依赖的那些类都初始化一次,初始化代码只需要写一行,但是其它配置的参数还得写N行,这样的代码通常在项目中要重复N次,写起来麻烦不说,如果后续底层有一个依赖的类(如A类)变动了,那么项目中所有写过这个类的地方都得改,属实是麻烦,如果有了IOC容器,那我们可以这样写

var target=IOC.Resolve<Target>();

构造函数大概是这样的:

public Target(IA a,IB b,IC c) //都变成抽象类,不关注具体实现
{
this._a=a;
this._b=b;
this._c=c;
}

是不是看起来很简洁,所有事情都不用自己操心,都让IOC容器给干了。

实际上在ASP.Net Core中使用这种IOC非常方便,因为所有的Controller都是从IOC中拿出来的,也就是说所有的依赖关系我们都可以靠IOC去解耦,完全不用担心改一个底层,项目里N个地方要改

再举个栗子,通常我们开发的程序时,都是分层架构,如UI->BLL->DAL三层,也就是说UI层对BLL层有依赖,BLL层对DAL层有依赖,如果我们使用普通的方式去开发,如果对DAL层进行了修改,由于BLL层对DAL层有依赖,那么极有可能还需要修改BLL层,同时又由于UI层对BLL层有依赖,那么很有可能还需要修改UI层,这样的话我们如果对底层有一点点小的修改,很有可能要对整个项目进行一个很大的更新。

例如,如果我们有一个GameService类,提供了一个玩游戏的方法,参数是具体的某种设备:

class GameService
{
// 参数 联想电脑
public void PlayGame(LenovoComputer computer)
{
// 加载游戏
computer.LoadGame();
// 玩游戏
computer.PlayGame();
}
// 参数 戴尔电脑
public void PlayGame(DellComputer computer)
{
// 加载游戏
computer.LoadGame();
// 玩游戏
computer.PlayGame();
}
public void PlayGame(XiaomiComputer computer)
{
// 加载游戏
computer.LoadGame();
// 玩游戏
computer.PlayGame();
}
}

这时候我们把Service层当做高层,Computer当做低层(目前是两层),A层对B层是有很强的依赖性的,如果我们这时候又加了一种电脑,叫小米电脑,那么我们不仅需要对B层的代码进行修改,还需要修改A层的代码,如果这个代码分成了3、4、5、6层,可想而知有多少地方要修改。

如果我们改为了依赖抽象的方式去写这种代码,那就可以改成如下这样:

class GameService
{
public void PlayGame(IComputer computer)
{
// 加载游戏
computer.LoadGame();
// 玩游戏
computer.PlayGame();
}
} public interface IComputer
{
void LoadGame();
void PlayGame();
}

我们所有B层的Computer类都可以继承这个接口:

public class DellComputer : IComputer
{
public void LoadGame()
{
//...
} public void PlayGame()
{
//...
}
}

看上去是很好了,但是我们实际在使用的时候还是要像下面一样写:

 GameService gameService = new GameService();
// 实例化底层对象
var xiaomiComputer=new XiaomiComputer();
var levonoComputer=new LevenoComputer();
var dellComputer=new DellComputer(); gameService.PlayGame(xiaomiComputer);
gameService.PlayGame(levonoComputer);
gameService.PlayGame(dellComputer);

这块代码写在了A层,还是用到了具体的B层的实例,还是没有去掉A->B层的依赖性,一旦B层改了,A层的代码还是得修改。

接下来我们来彻底实现抽象的方式,首先整体项目结构如下:

项目的依赖如下:



这样通过一个接口+工厂,实现了高层对底层的解耦合,工厂的实现如下:

 public class ComputerFactory
{ private static string config = ConfigurationManager.AppSettings["ComputerAssembly"];
public static IComputer CreateComputer()
{
var iu=config;
Assembly assembly = Assembly.Load(config.Split(',')[1]);
Type type = assembly.GetType(config.Split(',')[0]);
return (IComputer)Activator.CreateInstance(type);
}
}

高层的代码如下:

static void Main(string[] args)
{ GameService gameService = new GameService();
// 实例化底层对象
IComputer com= ComputerFactory.CreateComputer();
gameService.PlayGame(com); }

工厂类通过读取配置文件通过反射加载对应的实例对象然后返回,通过这样的方法,底层的改动完全不影响高层的代码,真正实现了耦合

一些概念解释

依赖倒置原则: 通过如上的例子我们也可以发现,在面向对象设计时,高层模块不应该依赖于底层模块,二者通过抽象来依赖,也就是说依赖抽象,而不是依赖于具体的细节:

IOC容器:是指的就是一个工厂,负责创建对象,IOC容器的作用就是为了少写工厂代码

IOC(控制反转):只是把上层对下层的依赖,换成了第三方的容器

这样去掉了对细节的依赖之后,就更方便去扩展了。

DI(依赖注入)

自己实现一个IOC容器

定个小目标

自己可以实现一个支持构造函数注入、生命周期的IOC容器,并且能自动选择参数最多或手动使用特性标记的的构造函数

实现过程

首先,先把IOC类的接口准备好

public interface IContainer
{
// 注册
void Register<TParent, TChild>(LifetimeType lifetimeType = LifetimeType.Transient) where TChild : TParent;
// 解析
TParent Resolve<TParent>();
}

由于容器内存的每个对象都分为三种生命周期:临时、单例、容器(Transient、Singleton、Scope),因此我们需要设计一个对应的模型类:

public class IOCRegisterModel
{
public Type TargetType { get; set; } public LifetimeType Lifetime { get; set; }
/// <summary>
/// 只有该类注册为单例的时候才需要
/// </summary>
public object SingletonInstance { get; set; }
} public enum LifetimeType
{
Transient,
Singleton,
Scope
}

然后,我们需要实现一个特性,用于需要手动标记的构造函数,能让ioc优先使用这个构造函数来构造对象:

[AttributeUsage(AttributeTargets.Constructor)]
public class ConstructorAttribute : Attribute
{
}

接着,我们来实现对应的IOC容器类,首先这个类需要两个字典,用来存储对象的类型信息、以及存储容器内的对象实例

/// <summary>
/// 模型类型字典
/// </summary>
private Dictionary<string, IOCRegisterModel> _containerDic = new Dictionary<string, IOCRegisterModel>();
/// <summary>
/// 容器内的对象
/// </summary>
private Dictionary<string, object> _containerScopeDic = new Dictionary<string, object>();

然后我们可以来实现注册对象方法的实现逻辑:

/// <summary>
/// 注册类型
/// </summary>
/// <typeparam name="TParent"></typeparam>
/// <typeparam name="TChild"></typeparam>
/// <param name="shortName"></param>
/// <param name="paraList"></param>
public void Register<TParent, TChild>( LifetimeType lifetimeType = LifetimeType.Transient) where TChild : TParent
{
this._containerDic.Add(typeof(TParent).FullName, new IOCRegisterModel()
{
Lifetime = lifetimeType,
TargetType = typeof(TChild)
});
}

这部分代码比较简单,就是在字典中存储该对象的类型,生命周期类别。在解析的部分就稍微复杂一点,我们分为两个函数实现:

public TParent Resolve<TParent>()
{
return (TParent)this.ResolveObject(typeof(TParent));
}

ResolveObject才是具体实现解析出实际对象的方法:

  /// <summary>
/// 如果该类型的构造函数依赖了其它参数,则用递归的方式不断往下构造,直至全部构造完成
/// </summary>
/// <typeparam name="TFrom"></typeparam>
/// <returns></returns>
private object ResolveObject(Type abstractType)
{
string key = abstractType.FullName;
var model = this._containerDic[key];
#region Lifetime
switch (model.Lifetime)
{
// 用的时候就new一个新的
case LifetimeType.Transient:
Console.WriteLine("使用的时候直接new一个新的");
break;
// 单例模式
case LifetimeType.Singleton:
if (model.SingletonInstance == null)
{
break;
}
else
{
return model.SingletonInstance;
}
// 容器内有就用这个
case LifetimeType.Scope:
if (this._containerScopeDic.ContainsKey(key))
{
return this._containerScopeDic[key];
}
else
{
break;
}
default:
break;
}
#endregion Type type = model.TargetType; ConstructorInfo ctor = null;
//2 标记特性
ctor = type.GetConstructors().FirstOrDefault(c => c.IsDefined(typeof(ConstructorAttribute), true));
if (ctor == null)
{
// 没有标记就直接使用参数个数最多的
ctor = type.GetConstructors().OrderByDescending(c => c.GetParameters().Length).First();
} List<object> paraList = new List<object>(); foreach (var para in ctor.GetParameters())
{
Type paraType = para.ParameterType;//获取参数的类型 IUserDAL object paraInstance = this.ResolveObject(paraType);
paraList.Add(paraInstance);
} object oInstance = null;
oInstance = Activator.CreateInstance(type, paraList.ToArray()); #region 生命周期控制
switch (model.Lifetime)
{
case LifetimeType.Transient:
Console.WriteLine("啥事不干");
break;
case LifetimeType.Singleton:
model.SingletonInstance = oInstance;
break;
case LifetimeType.Scope:
this._containerScopeDic[key] = oInstance;
break;
default:
break;
}
#endregion return oInstance;
}

这里解释一下上面这段代码的逻辑,首先先判断需要解析的对象的生命周期,如果是单例或者是容器模式,则直接从现有的字典中取出来即可,接着找出是否有标记过的构造函数,如果有的话就用这个,没有的话就找需要参数最多的那个构造函数,使用递归的方式不断对这些参数的类型进行解析,直到素有的参数都构造完成,最后利用反射将对象实例化。

下面还是以我们的Computer类举例子:

 static void Main(string[] args)
{ #region 使用IOC的方式 IContainer container= new Container();
container.Register<IComputer, DellComputer>();
IComputer computer= container.Resolve<IComputer>();
computer.PlayGame();
#endregion }

像这样先注册,然后在需要的时候注入即可,执行的结果如下:

如果要实现属性注入和方法注入的话也非常简单,和构造函数类似,自己新加两种特性,如IOCFunctionAttributeIOCPropertyAttribute,通过反射的方法找到类型上面带有这两种标记的方法和属性,通过同样的方式去解析出来即可,如果是方法注入的话,在解析完需要的参数之后就直接调用

平时我们使用IOC的时候90%都是构造函数注入,基本上用这一种就可以解决绝大部分业务场景的需要

深入理解IOC并自己实现IOC容器的更多相关文章

  1. 深入理解DIP、IoC、DI以及IoC容器

    摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.DI以及Ioc容器等概念.通过本文我们将一起学 ...

  2. 深入理解DIP、IoC、DI以及IoC容器(转)

    深入理解DIP.IoC.DI以及IoC容器 摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.D ...

  3. 【转】深入理解DIP、IoC、DI以及IoC容器

    原文链接:http://www.cnblogs.com/liuhaorain/p/3747470.html 前言 对于大部分小菜来说,当听到大牛们高谈DIP.IoC.DI以及IoC容器等名词时,有没有 ...

  4. 再看IOC, 读深入理解DIP、IoC、DI以及IoC容器

    IoC则是一种 软件设计模式,它告诉你应该如何做,来解除相互依赖模块的耦合.控制反转(IoC),它为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制,即依赖对象不在被依赖模 ...

  5. 【来龙去脉系列】深入理解DIP、IoC、DI以及IoC容器

    摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.DI以及Ioc容器等概念.通过本文我们将一起学 ...

  6. 深入理解DIP、IoC、DI以及IoC容器(转载)

    <转载的这个up的其他的文章也很nice> 这几个词第一眼看,懵逼,第二眼看,更特么懵逼..... 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序. 其中,OOD有一 ...

  7. 对依赖倒置原则(DIP)及Ioc、DI、Ioc容器的一些理解

    1.概述 所谓依赖倒置原则(Dependence Inversion Principle)就是要依赖于抽象,不要依赖于具体.简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模 ...

  8. 对依赖倒置原则(DIP)及Ioc、DI、Ioc容器的一些理解(转)

    所谓依赖倒置原则(Dependence Inversion Principle)就是要依赖于抽象,不要依赖于具体.简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合 ...

  9. 不可不知的DIP、IoC、DI以及IoC容器

    面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.当中.OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.DI以及Ioc容器等概念. 本文首先用实例阐述四个概 ...

  10. DIP、IoC、DI以及IoC容器

    深入理解DIP.IoC.DI以及IoC容器 摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.D ...

随机推荐

  1. __g is not defined

    新手小白学习小程序开发遇到的问题以及解决方法 文章目录 1.出现的问题 2.解决的方法 1.出现的问题 2.解决的方法 删除app.json中的 "lazyCodeLoading" ...

  2. Linux系统安装宝塔面板教程

    # Linux系统宝塔安装教程 注意:安装宝塔面板的前提条件 首先要有一台服务器或者使用linux系统的虚拟机. 安装前请确保是[全新的机器].必须是没装过其它环境的新系统,如Apache/Nginx ...

  3. eDP接口简介

    1. eDP背景介绍   随着显示分辨率的越来越高,传统的VGA.DVI等接口逐渐不能满足人们的视觉需求. 随后就产生了以HDMI.DisplayPort为代表的新型数字接口,外部接口方面HDMI占据 ...

  4. Codeforces Round #828 (Div. 3) A-F

    比赛链接 A 题解 知识点:贪心,模拟. 遇到没用过的数字就给个字母,遇到用过的数字就对照字母是否一致. 时间复杂度 \(O(n)\) 空间复杂度 \(O(n)\) 代码 #include <b ...

  5. FastAPI + tortoise-orm基础使用

    更改sqlite为mysql from tortoise import Tortoise import asyncio async def init(): user = 'root' password ...

  6. ubuntu 安装anaconda3

    ubuntu 安装anaconda3 官网:https://www.anaconda.com/ 下载:https://www.anaconda.com/products/individual#Down ...

  7. Oracle性能优化之运行参数设置

    Oracle参数调整建议值 sessions=2150 processes=2000 open_cursors=5120 db_file_multiblock_read_count=64 log_bu ...

  8. phpmyadmin 数据库导出数据到excel(图文版)

    查询到想要的数据后,点击上方或下方的"导出"按钮 格式选择"CSV for MS Excel" 如果快速导出的数据乱码,可以选择"导出方式" ...

  9. Kubernetes基础_Service暴露的两种方式

    一.前言 kubernetes集群中,pod是多变的,可以被新建或删除,而且ip不稳定,不方便集群外部访问,所以提供了一种新的资源 Service ,就是就是 a set of Pod ,作用是提供一 ...

  10. linux sublime-text ctrl+shift+b 快捷键失效问题解决

    解决办法 由于fcitx拦截了这个ctrl+shift+b 这个快捷键,所以取消即可 点击全局配置里面高级选项,然后找到ctrl+shift+b这个快捷键,点击后,按esc就可以将快捷键设置为空,不过 ...