🧩 为什么依赖注入如此重要
依赖注入(Dependency Injection,简称 DI)不仅仅是个流行词——它是构建模块化、可测试、易维护软件的基础设计模式。通过反转依赖的创建与传递方式,DI 使类摆脱对具体实现的耦合。
这在真实世界的应用中有什么意义?
- 减少逻辑与实例的重复。
- 允许轻松替换实现(例如,从内存缓存切换到 Redis)。
- 测试轻松 —— 无需启动整个系统即可单元测试。
本文将带你一步步了解 C# 中的 DI:从“老旧糟糕”的方式开始,转向干净灵活的设计,并以 Redis 缓存的实战场景收尾。
🚫 天真的实现方式:没有依赖注入
假设我们要开发一个用户注册功能,并发送欢迎邮件。一种典型但紧耦合的实现可能如下:
public class EmailService
{
public void Send(string to, string message)
{
Console.WriteLine($"发送邮件到 {to}:{message}");
}
}
public class UserController
{
private EmailService _emailService = new EmailService(); // 强耦合
public void RegisterUser(string email)
{
// 注册用户逻辑...
_emailService.Send(email, "欢迎!");
}
}
🔍 这有什么问题?
- 强耦合:
UserController自己创建了EmailService的实例。 - 重复:每个需要邮件功能的类都会新建实例。
- 难以测试:想测试
UserController而不真正发送邮件?很难 stub 或 mock。 - 缺乏灵活性:想换成使用第三方邮件 API?得到处重构。
✅ 使用依赖注入的重构方案
我们通过 构造函数注入(最常见的 DI 技术)来改善设计:
public class EmailService
{
public void Send(string to, string message)
{
Console.WriteLine($"发送邮件到 {to}:{message}");
}
}
public class UserController
{
private readonly EmailService _emailService;
public UserController(EmailService emailService)
{
_emailService = emailService;
}
public void RegisterUser(string email)
{
_emailService.Send(email, "欢迎!");
}
}
我们现在让外部系统(通常是 DI 容器)提供 EmailService 实例。优势包括:
- 可复用性:共享的
EmailService实例可供多个控制器使用。 - 可测试性:可轻松注入 mock 邮件服务进行测试。
- 灵活性:无需改动控制器即可替换服务实现。
在 ASP.NET Core 中,我们可以在 Startup.cs 注册服务:
services.AddSingleton<EmailService>();
services.AddTransient<UserController>();
每个 UserController 都会获取一个新的实例,但它们共用同一个 EmailService 实例(因为它是 singleton)。
接下来我们会进一步探讨,看看如何使用 DI 来轻松切换本地缓存与 Redis 缓存,而无需修改核心业务逻辑。
⚙️ 实战案例:在 Redis 与本地缓存之间切换
在真实应用中,缓存对性能和扩展性至关重要。无论是用户会话、产品数据还是 API 结果,缓存都能极大地减少加载时间和系统负载。
问题是: 如何设计系统,才能轻松在本地缓存(适用于开发或简单场景)与 Redis(适用于生产环境)之间切换?
答案就是:依赖注入 + 接口编程。
🧱 第一步:定义缓存接口
我们先定义一个抽象接口,封装缓存逻辑:
public interface ICacheService
{
Task<string> GetAsync(string key);
Task SetAsync(string key, string value);
}
🚀 第二步:实现本地内存缓存
public class InMemoryCacheService : ICacheService
{
private readonly Dictionary<string, string> _cache = new();
public Task<string> GetAsync(string key)
{
_cache.TryGetValue(key, out var value);
return Task.FromResult(value);
}
public Task SetAsync(string key, string value)
{
_cache[key] = value;
return Task.CompletedTask;
}
}
🌐 第三步:实现 Redis 缓存
public class RedisCacheService : ICacheService
{
private readonly IDatabase _redis;
public RedisCacheService(IConnectionMultiplexer redis)
{
_redis = redis.GetDatabase();
}
public async Task<string> GetAsync(string key)
{
return await _redis.StringGetAsync(key);
}
public async Task SetAsync(string key, string value)
{
await _redis.StringSetAsync(key, value, TimeSpan.FromMinutes(30));
}
}
⚙️ 第四步:根据环境注册服务
使用 DI,我们可以根据环境动态选择缓存实现:
if (env.IsDevelopment())
{
services.AddSingleton<ICacheService, InMemoryCacheService>();
}
else
{
services.AddSingleton<IConnectionMultiplexer>(
ConnectionMultiplexer.Connect("localhost:6379"));
services.AddSingleton<ICacheService, RedisCacheService>();
}
现在,任何控制器或服务只需请求 ICacheService,它就会自动获取正确的实现。
🧪 加分项:在控制器中使用
public class ProductController
{
private readonly ICacheService _cache;
public ProductController(ICacheService cache)
{
_cache = cache;
}
public async Task<string> GetProduct(string productId)
{
var cached = await _cache.GetAsync(productId);
if (cached != null)
return $"来自缓存:{cached}";
// 模拟数据库查询
string productData = $"Product_{productId}_Details";
await _cache.SetAsync(productId, productData);
return $"来自数据库:{productData}";
}
}
无需改动
ProductController的任何代码,只需调整 DI 注册逻辑,就能在 Redis 与本地缓存之间自由切换。
🧠 为什么 Redis + DI 如此强大?
- Redis 是分布式的内存型键值数据库,非常适合需要跨服务或容器共享数据的场景。
- DI 让应用逻辑与缓存实现解耦。
- 你可以在运行时切换缓存实现,而不需要改动业务代码。
- 使用 DI 管理
IConnectionMultiplexer连接,可以确保资源利用最优化(使用单例模式复用连接)。
✅ 总结
依赖注入不仅仅是组织代码的工具 —— 它是构建可扩展、灵活、可测试系统架构的基石。
通过使用 DI:
- 避免了不必要的实例创建。
- 能轻松进行单元测试和模拟依赖。
- 可以灵活切换实现(如 Redis 和本地缓存),无需改动业务逻辑。
当 DI 与 Redis 结合使用时,你的系统就能实现水平扩展、智能缓存、干净的模块边界 —— 面对高并发也能从容应对。