.net framework 下可以使用 Ninject 作为 DI 容器, 适合作为依赖注入的对象应该是功能型class, 而不是数据型class, 由DI 容器自动管理不同类的依赖关系和具体类的实现子类.
关于构造函数和属性注入的对比
- 不推荐属性注入的主要原因是:
- 测试困难: 属性注入导致依赖硬编码在类中,不能通过构造函数正确初始化,允许测试。
- 顺序依赖: 当一个类依赖于多个属性时,它们的初始化顺序可能存在依赖关系。但是属性注入无法保证依赖的正确顺序。
- 难以debug: 因为属性注入是在运行时发生的,调试时很难追踪并理解。
- 难以理解: 对于一个新代码许多人来说,属性注入的依赖关系并不易于理解。
- 相比之下,构造函数注入则不具备这些缺点:
- 在构造函数中明确地声明所有的依赖
- 通过构造函数的参数顺序保证了正确的依赖注入顺序
- 在代码中明确地声明了依赖关系,易于理解和调试
- 允许通过传递 mock 依赖实现单元测试
Ninject 简单示例
- StandardKernel 为 Ninject 的 DI 容器
- 使用容器完成一个接口和实现类的注入.
- 使用容器获取了一个接口的实现类对象.
using Ninject;
public interface IHello
{
void SayHello();
}
public class HelloImpl : IHello
{
public void SayHello()
{
Console.WriteLine("Hello from Ninject!");
}
}
class Program
{
static void Main(string[] args)
{
// Create the kernel
IKernel kernel = new StandardKernel();
// Register the interface and implementation
kernel.Bind<IHello>().To<HelloImpl>();
// Resolve the dependency
IHello hello = kernel.Get<IHello>();
// Use the dependency
hello.SayHello();
}
}
Ninject 常用的几种绑定方式
- ToSelf(), 最简单, 直接绑定接口和实现类
- To(), 最常见, 显式绑定接口到实现类
- ToMethod(), 最强大, 该方式一般是使用 lambda 表达式动态创建一个被依赖对象的实例.
- ToConstructor(), 使用一个构造函数注入实现类实例
Ninject 使用一个构造函数注入实现类实例
下面的实现类构造器有一个string参数, 代码演示如何使用 Ninject 实现构造函数 .
interface IHello
{
void SayHello();
}
class HelloImpl : IHello
{
public HelloImpl(string name)
{
Name = name;
}
public string Name { get; set; }
public void SayHello()
{
Console.WriteLine($"Hello {Name}!");
}
}
class Program
{
static void Main(string[] args)
{
IKernel kernel = new StandardKernel();
kernel.Bind<IHello>().To<HelloImpl>();
// Bind constructor argument
kernel.Bind<string>().ToConstant("John");
IHello hello = kernel.Get<IHello>();
hello.SayHello(); // Prints "Hello John!"
}
}
Ninject 使用 ToConstructor() 实现多参数构造器注入实例
kernel.Bind<IGreetingService>().ToConstructor(
ctr => {
ctr.WithConstructorArgument("name", "John");
ctr.WithConstructorArgument("age", 30);
ctr.WithConstructorArgument("isActive", true);
}
);
Ninject 使用ToMethod() 注入实现类实例的代码
kernel.Bind<IMyService>().ToMethod(ctx => {
if(IsTestEnv()) {
return new TestService();
} else {
return new ProdService();
}
});
Ninject 按名称绑定
定义两个实现类:
public interface IGreetingService {...}
public class EnglishGreeting : IGreetingService {...}
public class ChineseGreeting : IGreetingService {...}
完成绑定和解析:
//绑定
kernel.Bind<IGreetingService>().To<EnglishGreeting>().Named("english");
kernel.Bind<IGreetingService>().To<ChineseGreeting>().Named("chinese");
//解析
IGreetingService english = kernel.Get<IGreetingService>("english");
IGreetingService chinese = kernel.Get<IGreetingService>("chinese");
Ninject 如何设置绑定作用域
kernel.Bind<IMyService>().To<MyService>()
.InSingletonScope();
在 Ninject 中,还有其他作用域可用:
- InTransientScope() : Transient是默认作用域,每次解析新建实例(非单例)
- InThreadScope(): 与线程绑定,每个线程解析相同实例
- InRequestScope(): 与请求绑定,每个请求解析相同实例
- InSingletonScope: 每次解析依赖时返回相同的实例。
Ninject 真实案例代码
代码讲解:
- BusinessService 有logger和name两个构造函数参数
- 我们分别为这两个参数绑定值或依赖
- 使用 ToSelf(), BusinessService 本身作为依赖
- 然后解析 BusinessService 依赖时,Ninject 将先自动创建ConsoleLogger, 然后自动创建 BusinessService
interface ILogger {
void LogInfo(string message);
}
class ConsoleLogger : ILogger {
public ConsoleLogger(string name) { ... }
public void LogInfo(string message) { ... }
}
class BusinessService {
private ILogger logger;
public BusinessService(ILogger logger, string name) {
this.logger = logger;
this.Name = name;
}
public string Name { get; }
}
class Program
{
static void Main() {
IKernel kernel = new StandardKernel();
// 绑定 logger 参数, 传入 "App"
kernel.Bind<ILogger>().To<ConsoleLogger>();
kernel.Bind<string>().ToConstant("App");
// 绑定名称 string 参数
kernel.Bind<string>().ToConstant("Business Service");
// 绑定服务类, 没有接口, 所以绑定到自身
kernel.Bind<BusinessService>().ToSelf();
//Ninject 将自动使用构造器 获取 BusinessService 对象
var service = kernel.Get<BusinessService>();
service.logger.LogInfo("Service Started!");
}
}
注册和解析不在一个文件的示例
下面代码中, 在Program.cs 中进行绑定一个singleton 实例, 在MainForm.cs 中进行解析实例. 要点:
- 将 Ninject 容器定义为静态属性, 其他类可以通过 Program.Kernel 来访问容器
// Program.cs
static class Program
{
public static IKernel Kernel { get; }
static Program()
{
Kernel = new StandardKernel();
}
}
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
// 解析依赖
var service = Program.Kernel.Get<ISomeService>();
}
}