1、前言
很长时间没写博客了,今年的主要任务就是把原先.NET Framework
平台的项目迁移到.NET Core
平台,在研究了一阵子后,我发现.NET Core
实在是让人爱不释手,所以还没入坑的小伙伴可要抓紧了,争取早日体验一把真香定律。言归正传,由于现在的项目采用的都是前后端分离模式,因此本系列博客主要介绍ASP.NET Core
中的WebAPI
开发模式,开发环境为Visual Studio 2019
,数据库选择SQL Server 2019
,缓存数据库选择Redis
。本文主要介绍依赖注入和控制反转的相关概念,下面就让我们开始吧。
2、依赖于具体类产生的问题
首先明确一点:什么情况下会产生依赖关系?其实很简单,当一个类中包含另一个类的实例的时候就产生了依赖关系。那么这种依赖关系会给应用程序带来什么问题?先看下面的例子:
using System;
namespace App
{
class Program
{
static void Main(string[] args)
{
SqlServer db = new SqlServer();
DbManager manager = new DbManager(db);
manager.Read();
}
}
public class SqlServer
{
public void Read()
{
Console.WriteLine("SqlServer读取数据");
}
}
public class DbManager
{
private SqlServer db;
public DbManager(SqlServer db)
{
this.db = db;
}
public void Read()
{
db.Read();
}
}
}
只要对面向对象的开发方式略微熟悉一点,你就应该能看出上面代码的问题:DbManager
类中包含了一个SqlServer
类的实例,如果以后需要改成MySql
读取数据,我们将不得不修改DbManager
的代码,很显然这违反了开闭原则。因此,过多依赖于具体类将导致应用程序的可扩展性大大降低。
3、依赖于抽象接口
既然依赖于具体类会导致程序的可扩展性降低,那么我们自然而然会想到使用接口。现在对上面的例子稍作修改,增加一个IDb
接口,代码如下:
using System;
namespace App
{
class Program
{
static void Main(string[] args)
{
IDb db = new MySql();
DbManager manager = new DbManager(db);
manager.Read();
}
}
public interface IDb
{
void Read();
}
public class SqlServer : IDb
{
public void Read()
{
Console.WriteLine("SqlServer读取数据");
}
}
public class MySql : IDb
{
public void Read()
{
Console.WriteLine("MySql读取数据");
}
}
public class DbManager
{
private IDb db;
public DbManager(IDb db)
{
this.db = db;
}
public void Read()
{
db.Read();
}
}
}
对于DbManager
类,现在它不再依赖于某个具体的数据库访问类,而是依赖于一个抽象接口,这样做的好处也是显而易见的,如果以后需要改成使用Oracle
读取数据,DbManager
不需要做任何修改,只需要在Main
方法中传入一个Oracle
类实例即可。
4、依赖注入
现在我们已经解决了依赖关系,下面就得考虑第二个问题:什么是注入?其实注入就好比去医院打针,医生不可能在你身上随便找个地方就把针扎进去,肯定得选择合适的部位。依赖注入也是一样,对于一个类来说,合适的注入点一般就是构造函数和成员方法。
4.1、构造注入
构造注入 (Constructor Injection
) 是指在客户类中设置一个服务类接口类型的数据成员,并以构造函数为注入点,这个构造函数接受一个具体的服务类实例为参数,并将它赋给服务类接口类型的数据成员。由于构造函数注入只能在实例化客户类时注入一次,因此程序运行期间是没法改变一个客户类对象内的服务类实例的。其代码如下所示:
using System;
namespace App
{
class Program
{
static void Main(string[] args)
{
DbManager manager = new DbManager(new SqlServer());
manager.Read();
}
}
public interface IDb
{
void Read();
}
public class SqlServer : IDb
{
public void Read()
{
Console.WriteLine("SqlServer读取数据");
}
}
public class DbManager
{
private IDb db;
public DbManager(IDb db)
{
this.db = db;
}
public void Read()
{
db.Read();
}
}
}
4.2、Setter注入
Setter
注入 (Setter Injection
) 是指在客户类中设置一个服务类接口类型的数据成员,并设置一个Set
方法作为注入点,这个Set
方法接受一个具体的服务类实例为参数,并将它赋给服务类接口类型的数据成员。该方法的好处就是可以在程序运行期间动态修改客户端类中的服务对象,其代码如下所示:
using System;
namespace App
{
class Program
{
static void Main(string[] args)
{
DbManager manager = new DbManager();
manager.SetDb(new SqlServer());
manager.Read();
}
}
public interface IDb
{
void Read();
}
public class SqlServer : IDb
{
public void Read()
{
Console.WriteLine("SqlServer读取数据");
}
}
public class DbManager
{
private IDb db;
public void SetDb(IDb db)
{
this.db = db;
}
public void Read()
{
db.Read();
}
}
}
4.3、接口注入
接口注入是将包括抽象类型注入的入口以方法的形式定义在一个接口里,如果客户类型需要实现这个注入过程,则实现这个接口,客户类型自己考虑如何把抽象类型“引入”内部。实际上接口注入有很强的侵入性,因为它显式地定义了一个新的接口并要求客户类型实现它。与前两种注入方式相比,接口注入并不常用,其代码如下所示:
using System;
namespace App
{
class Program
{
static void Main(string[] args)
{
DbManager manager = new DbManager();
manager.Db = new SqlServer();
manager.Read();
}
}
public interface IDb
{
void Read();
}
public class SqlServer : IDb
{
public void Read()
{
Console.WriteLine("SqlServer读取数据");
}
}
public interface IDbManager
{
IDb Db { get; set; }
}
public class DbManager : IDbManager
{
public IDb Db { get; set; }
public void Read()
{
Db.Read();
}
}
}
5、控制反转
提到依赖注入,就不能不提控制反转。控制反转 (Inversion of Control
) ,简称IoC
,是面向对象编程中的一种设计原则,可以用来降低计算机代码之间的耦合度。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。
5.1、谁控制谁?什么被反转了?
在传统的程序开发中,开发人员往往会手动去new
一个对象,换言之:对象的创建过程由开发人员控制。而在控制反转的原则下,系统中会存在一个专门的IoC
容器,由IoC
容器负责对象的创建。因此,创建对象的主动权从开发人员手中被移交到了容器手中,由容器控制对象的创建过程,这就是控制反转的含义。
5.2、编写一个简单的IoC容器
下面就来编写一个简单的IoC
容器,主要分为IoC
、Controller
、Service
、Repository
、Model
五部分。依赖关系很简单,Controller
—>Service
—>Repository
,最后在IoC
中注册并调用,代码如下。
5.2.1、Model
namespace App.Model
{
public class UserInfo
{
public string UserName { get; set; }
public string Password { get; set; }
}
}
5.2.2、Repository
using System.Collections.Generic;
using App.Model;
namespace App.Repository
{
public interface IUserInfoRepository
{
IEnumerable<UserInfo> GetUserInfoList();
}
}
using System.Collections.Generic;
using App.Model;
namespace App.Repository
{
public class UserInfoRepository : IUserInfoRepository
{
public IEnumerable<UserInfo> GetUserInfoList()
{
List<UserInfo> list = new List<UserInfo>();
list.Add(new UserInfo { UserName = "super", Password = "12345" });
list.Add(new UserInfo { UserName = "admin", Password = "12345" });
return list;
}
}
}
5.2.3、Service
using System.Collections.Generic;
using App.Model;
namespace App.Service
{
public interface IUserInfoService
{
IEnumerable<UserInfo> GetUserInfoList();
}
}
using System;
using System.Collections.Generic;
using App.Model;
using App.Repository;
namespace App.Service
{
public class UserInfoService : IUserInfoService
{
private readonly IUserInfoRepository repository;
public UserInfoService(IUserInfoRepository repository)
{
this.repository = repository;
}
public IEnumerable<UserInfo> GetUserInfoList()
{
return repository.GetUserInfoList();
}
}
}
5.2.4、Controller
using System.Collections.Generic;
using App.Model;
namespace App.Controller
{
public interface IUserInfoController
{
IEnumerable<UserInfo> GetUserInfoList();
}
}
using System.Collections.Generic;
using App.Model;
using App.Service;
namespace App.Controller
{
public class UserInfoController : IUserInfoController
{
private readonly IUserInfoService service;
public UserInfoController(IUserInfoService service)
{
this.service = service;
}
public IEnumerable<UserInfo> GetUserInfoList()
{
return service.GetUserInfoList();
}
}
}
5.2.5、IoC
using System;
using System.Collections.Generic;
using System.Reflection;
namespace App.IoC
{
public class SimpleIocContainer
{
private readonly Dictionary<string, Type> container;
/// <summary>
/// 构造函数
/// </summary>
public SimpleIocContainer()
{
container = new Dictionary<string, Type>();
}
/// <summary>
/// 注册类型
/// </summary>
/// <typeparam name="TSuper">超类</typeparam>
/// <typeparam name="TSub">子类</typeparam>
public void RegisterType<TSuper, TSub>()
where TSuper : class
where TSub : class
{
string fullName = typeof(TSuper).FullName;
if (!container.ContainsKey(fullName))
{
container.Add(fullName, typeof(TSub));
}
}
/// <summary>
/// 获取需要的类型
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Resolve<T>() where T : class
{
string fullName = typeof(T).FullName;
if (!container.ContainsKey(fullName))
{
throw new Exception("Error");
}
return (T)CreateObject(container[fullName]);
}
/// <summary>
/// 递归创建对象
/// </summary>
/// <param name="type">子类类型</param>
/// <returns>object</returns>
private object CreateObject(Type type)
{
ConstructorInfo[] constructors = type.GetConstructors();
ConstructorInfo constructor = GetConstructor(constructors);
List<object> list = new List<object>();
foreach (ParameterInfo parameter in constructor.GetParameters())
{
Type paraType = parameter.ParameterType;
if (!container.ContainsKey(paraType.FullName))
{
throw new Exception("Error");
}
list.Add(CreateObject(container[paraType.FullName]));
}
return Activator.CreateInstance(type, list.ToArray());
}
/// <summary>
/// 获取构造函数
/// </summary>
/// <param name="constructors">构造函数集合</param>
/// <returns>构造函数</returns>
private ConstructorInfo GetConstructor(ConstructorInfo[] constructors)
{
// 构造函数个数为0,则抛出异常
if (constructors.Length == 0)
{
throw new Exception("Error");
}
// 构造函数个数为1,则直接返回
if (constructors.Length == 1)
{
return constructors[0];
}
// 获取形参个数最多的构造函数
ConstructorInfo constructor = constructors[0];
foreach (var item in constructors)
{
if (item.GetParameters().Length > constructor.GetParameters().Length)
{
constructor = item;
}
}
return constructor;
}
}
}
5.2.6、主程序调用
using System;
using App.Controller;
using App.IoC;
using App.Repository;
using App.Service;
namespace App
{
class Program
{
static void Main(string[] args)
{
// 创建IoC容器,注册接口和类
var container = new SimpleIocContainer();
container.RegisterType<IUserInfoRepository, UserInfoRepository>();
container.RegisterType<IUserInfoService, UserInfoService>();
container.RegisterType<IUserInfoController, UserInfoController>();
// 利用IoC容器自动创建对象
var controller = container.Resolve<IUserInfoController>();
// 输出结果
var list = controller.GetUserInfoList();
foreach (var item in list)
{
Console.WriteLine(item.UserName + "," + item.Password);
}
Console.ReadKey(true);
}
}
}
运行结果如下所示:
super,12345
admin,12345
在上面的代码中,我们并没有手动去创建Repository
、Service
、Controller
这些类的实例,而是通过在IoC
容器中注册它们的依赖关系,然后通过Resolve
方法自动创建对应的实例。这也就是上面所说的:把创建对象的主动权交给容器,让容器帮助我们自动创建对象。上面给出的只是一个最简单的IoC
容器实现方法,有兴趣的同志可以参考Autofac
、Unity
等第三方框架进行拓展。
6、结语
本文主要介绍了依赖注入和控制反转的相关概念,依赖注入的核心思想就是依赖于抽象接口而非某个具体的类,然后寻找合适的注入点注入对象,这会使系统在后期会更具灵活性和扩展性。而控制反转则是将创建对象的主动权交到IoC
容器手中,利用IoC
容器管理存在依赖关系的若干类,当我们需要使用某个对象时,直接到IoC
容器中去获取即可。