什么是依赖注入
依赖注入(Dependency Injection,简称 DI)是一种软件设计模式,用于解耦软件组件之间的依赖关系。在 C# 开发中,它主要解决的是类与类之间的强耦合问题。例如,一个类 A 依赖于另一个类 B,如果不使用依赖注入,那么在类 A 内部可能会直接实例化类 B,这就使得类 A 和类 B 紧密地耦合在一起。而依赖注入的方式是将类 B 的实例通过外部(通常是在类 A 的构造函数、属性或者方法参数中)传递给类 A,从而降低它们之间的耦合程度。
- 作用
提高可维护性:当系统规模变大时,如果各个组件之间耦合紧密,修改其中一个组件可能会牵一发而动全身。通过依赖注入,组件之间的依赖关系更加清晰,维护起来更加容易。
方便单元测试:在进行单元测试时,可以方便地模拟依赖对象,而不是依赖于真实的复杂对象,从而使测试更加简单和准确。
常用的依赖注入实现
Microsoft.Extensions.DependencyInjection
using Microsoft.Extensions.DependencyInjection;
class MyClassA
{
private MyClassB _dependency;
public MyClassA(MyClassB dependency)
{
_dependency = dependency;
}
public void DoSomething()=>_dependency.SomeMethod();
}
class MyClassB
{
public void SomeMethod()=>Console.WriteLine("MyClassB's method is called.");
}
class Program
{
static void Main()
{
// 创建服务容器
var serviceCollection = new ServiceCollection();
// 注册MyClassB为服务,每次请求MyClassB时会创建一个新的实例
serviceCollection.AddTransient<MyClassB>();
// 注册MyClassA为服务,并且注入MyClassB
serviceCollection.AddTransient<MyClassA>();
// 构建服务提供器
var serviceProvider = serviceCollection.BuildServiceProvider();
// 获取MyClassA的实例,此时会自动注入MyClassB的实例
var a = serviceProvider.GetService<MyClassA>();
a.DoSomething();
}
}
Autofac
using Autofac;
class MyClassA
{
private MyClassB _dependency;
public MyClassA(MyClassB dependency)
{
_dependency = dependency;
}
public void DoSomething()=>_dependency.SomeMethod();
}
class MyClassB
{
public void SomeMethod()=>Console.WriteLine("MyClassB's method is called.");
}
class Program
{
static void Main()
{
var builder = new ContainerBuilder();
builder.RegisterType<MyClassB>().AsSelf();
builder.RegisterType<MyClassA>().AsSelf();
var container = builder.Build();
var a = container.Resolve<MyClassA>();
a.DoSomething();
}
}
什么是动态加载
在 C# 中,动态加载程序集允许程序在运行时加载和使用程序集,而不是在编译时静态引用它们。这为程序提供了更高的灵活性,例如可以根据不同的条件或用户需求加载不同的功能模块,或者在程序运行时更新某些功能而无需重新编译整个程序。
using System.Reflection;
class Program
{
static void Main()
{
// 加载程序集,参数为程序集的完整名称
Assembly assembly = Assembly.Load("xxx.dll");
// 从程序集中获取类型
Type type = assembly.GetType("MyNamespace.MyClass");
// 创建类型的实例
object instance = Activator.CreateInstance(type);
// 调用类型的方法(假设该类型有一个无参数的方法叫做 DoSomething)
MethodInfo method = type.GetMethod("DoSomething");
method.Invoke(instance, null);
}
}
注意Assembly.Load
加载的程序集在进行卸载前,被加载的dll将会一直处于被占用状态,若要在非占用状态下进行加载程序集,比如进行动态更新之类的,可以采用以下方式
byte[] rawAssembly = File.ReadAllBytes(dllPath);
Assembly assembly = Assembly.Load(rawAssembly);
对动态加载的DLL进行依赖注入
这里采用Autofac
进行服务的注入和管理
- 加载程序集
byte[] rawAssembly = File.ReadAllBytes(dllPath);
Assembly assembly = Assembly.Load(rawAssembly);
- 获取程序集中定义的类型,并过滤出要进行依赖注入的类型,假设要为打了
Inject
标签同时继承ITool
接口的类型进行服务提供
var types = assembly.DefinedTypes.ToList();
[Inject("工具2", "类别1")]
public class MainFrame : ITool
[AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
public class InjectAttribute(string name, string category) : Attribute
{
public string Name { get; } = name;
public string Category { get; } = category;
}
public interface ITool
{
void DoWork();
void ShowDialog();
}
- 创建
ContainerBuilder
- 用
ContainerBuilder
进行依赖注入,若存在同类型多个不同实例的注入,可以采用Key或Token的方式来注入和解析服务
var builder = new ContainerBuilder();
types = types.Where(IsToolType).ToList();
foreach (var type in types)
{
IList<CustomAttributeTypedArgument> constructorArguments = type
.CustomAttributes.First(a => a.AttributeType == typeof(InjectAttribute))
.ConstructorArguments;
var (name, categoryName) = (
constructorArguments[0].Value.ToString(),
constructorArguments[1].Value.ToString()
);
string key = $"{categoryName}_{name}";
builder.RegisterType(type).Named<ITool>(key);
}
- 创建Container
- 解析服务
var container = builder.Build();
var tool1 = container.ResolveNamed<ITool>("类别1_工具1");
tool1.DoWork();
var tool2 = container.ResolveNamed<ITool>("类别1_工具2");
tool2.ShowDialog();
var tool3 = container.ResolveNamed<ITool>("类别2_工具1");
tool3.ShowDialog();
Console.ReadLine();
至此,多个ITool实例但都对应不同实现的类型都被注入了,要使用时直接Resolve即可
若被注入的类型存在对其他类型的引用,则依赖注入容器会帮我们自动解析。例如对日志和数据库功能的引用,需要该服务的类型只需要引用定义,而不需要关心实现,如下
ILogger
可能有多种实现,比如FileLogger
,ConsoleLogger
,DBLogger
等
IDatabase
可能有多种实现,比如Mysql
,Sqlite
,Postgresql
实现的内容由主框架确定,其他工具dll只需要引用ILogger
和IDatabase
即可使用,示例代码如下
public interface IDatabase
{
List<string> Select();
void Insert<T>(T entity);
}
public interface ILogger
{
void Log(string message);
}
单例方式注入
var builder = new ContainerBuilder();
builder.RegisterType<ConsoleLogger>().SingleInstance().As<ILogger>();
builder.RegisterType<MysqlMocker>().SingleInstance().As<IDatabase>();
使用
[Inject("工具1", "类别1")]
public class Tool1: ITool
{
internal readonly ILogger logger;
internal readonly IDatabase database;
public MainFrame(ILogger logger, IDatabase database)
{
this.logger = logger;
this.database = database;
}
public void DoWork()
{
logger?.Log("This is a log message from Tool1");
logger?.Log("Doing work in MainFrame");
var data = database?.Select();
var str = JsonConvert.SerializeObject(data);
logger?.Log($"Data from database: {str}");
}
public void ShowDialog() { }
}