Loading

深入理解IOC并自己实现IOC容器

背景介绍

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

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层的代码还是得修改。

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

file

项目的依赖如下:

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

 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);
        }
    }

file

高层的代码如下:

static void Main(string[] args)
{

    GameService gameService = new GameService();
    // 实例化底层对象
    IComputer com= ComputerFactory.CreateComputer();
    gameService.PlayGame(com);

}

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

一些概念解释

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

file

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

 }

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

file

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

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

posted @ 2023-01-12 09:20  李正浩  阅读(76)  评论(0编辑  收藏  举报