深入理解IOC并自己实现IOC容器
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
}
像这样先注册,然后在需要的时候注入即可,执行的结果如下:
如果要实现属性注入和方法注入的话也非常简单,和构造函数类似,自己新加两种特性,如IOCFunctionAttribute
、IOCPropertyAttribute
,通过反射的方法找到类型上面带有这两种标记的方法和属性,通过同样的方式去解析出来即可,如果是方法注入的话,在解析完需要的参数之后就直接调用
平时我们使用IOC的时候90%都是构造函数注入,基本上用这一种就可以解决绝大部分业务场景的需要
深入理解IOC并自己实现IOC容器的更多相关文章
- 深入理解DIP、IoC、DI以及IoC容器
摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.DI以及Ioc容器等概念.通过本文我们将一起学 ...
- 深入理解DIP、IoC、DI以及IoC容器(转)
深入理解DIP.IoC.DI以及IoC容器 摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.D ...
- 【转】深入理解DIP、IoC、DI以及IoC容器
原文链接:http://www.cnblogs.com/liuhaorain/p/3747470.html 前言 对于大部分小菜来说,当听到大牛们高谈DIP.IoC.DI以及IoC容器等名词时,有没有 ...
- 再看IOC, 读深入理解DIP、IoC、DI以及IoC容器
IoC则是一种 软件设计模式,它告诉你应该如何做,来解除相互依赖模块的耦合.控制反转(IoC),它为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制,即依赖对象不在被依赖模 ...
- 【来龙去脉系列】深入理解DIP、IoC、DI以及IoC容器
摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.DI以及Ioc容器等概念.通过本文我们将一起学 ...
- 深入理解DIP、IoC、DI以及IoC容器(转载)
<转载的这个up的其他的文章也很nice> 这几个词第一眼看,懵逼,第二眼看,更特么懵逼..... 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序. 其中,OOD有一 ...
- 对依赖倒置原则(DIP)及Ioc、DI、Ioc容器的一些理解
1.概述 所谓依赖倒置原则(Dependence Inversion Principle)就是要依赖于抽象,不要依赖于具体.简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模 ...
- 对依赖倒置原则(DIP)及Ioc、DI、Ioc容器的一些理解(转)
所谓依赖倒置原则(Dependence Inversion Principle)就是要依赖于抽象,不要依赖于具体.简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合 ...
- 不可不知的DIP、IoC、DI以及IoC容器
面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.当中.OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.DI以及Ioc容器等概念. 本文首先用实例阐述四个概 ...
- DIP、IoC、DI以及IoC容器
深入理解DIP.IoC.DI以及IoC容器 摘要 面向对象设计(OOD)有助于我们开发出高性能.易扩展以及易复用的程序.其中,OOD有一个重要的思想那就是依赖倒置原则(DIP),并由此引申出IoC.D ...
随机推荐
- cmd中pip加速的方法
临时加速: pip install dlib -i https://pypi.tuna.tsinghua.edu.cn/simple/ 永久加速: 在user文件夹里新建pip文件夹,再建pip.in ...
- python和C语言从路径中获取文件名
1.Python import os file_name = os.path.basename(filepath)#带后缀的文件名(不含路径) file_name_NoExtension = os.p ...
- PyCharm配置远程Docker环境
1. docker 配置 使用-p参数暴露一个端口用于ssh连接. docker run -itd --name wangchao_paddle --gpus all -p 8899:8888 -p ...
- scrapy传递 item时的 数据不匹配 和一些注意事项
item 在传递数据时需要拷贝内存地址 yield scrapy.Request( url=title_url, callback=self.parse_detail, # 用深拷贝的方式 复制子对象 ...
- 在 .NET 7上使用 WASM 和 WASI
WebAssembly(WASM)和WebAssembly System Interface(WASI)为开发人员开辟了新的世界..NET 开发人员在 Blazor WebAssembly 发布时熟悉 ...
- Jenkinsfile 同时检出多个 Git 仓库
前置 通常,在 Jenkinsfile 中使用 Git 仓库是这样的: stage('Checkout git repo') { steps { checkout([ $class: 'GitSCM' ...
- 详解Native Memory Tracking之追踪区域分析
摘要:本篇图文将介绍追踪区域的内存类型以及 NMT 无法追踪的内存. 本文分享自华为云社区<[技术剖析]17. Native Memory Tracking 详解(3)追踪区域分析(二)> ...
- Seata 1.5.2 源码学习(Client端)
在上一篇中通过阅读Seata服务端的代码,我们了解到TC是如何处理来自客户端的请求的,今天这一篇一起来了解一下客户端是如何处理TC发过来的请求的.要想搞清楚这一点,还得从GlobalTransacti ...
- 模块/collections/random/time/datetime
内容概要 模块--包的具体使用 编程思想介绍 软件开发--目录规范 常用模块介绍--collections模块 常用模块介绍--time.datetime 常用模块介绍--random 1.包的具体使 ...
- (Java)设计模式:创建型
前言 这篇内容是从另一篇:UML建模.设计原则 中分离出来的,原本这个创建型设计模式是和其放在一起的 但是:把这篇创建型设计模式放在一起让我贼别扭,看起来贼不舒服,越看念头越不通达,导致老衲躺在床上脑 ...