下面通过一个 WinForm 示例来演示接口抽象的实际应用。假设我们需要在界面中展示用户数据,数据可以来自数据库或本地文件,使用接口可以灵活地切换数据源。
首先定义一个用户数据的接口和实体类:
// 用户实体类
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
// 数据访问接口
public interface IUserRepository
{
List<User> GetAllUsers();
User GetUserById(int id);
bool SaveUser(User user);
}
然后实现两个不同的数据源:
// 数据库实现
public class SqlUserRepository : IUserRepository
{
public List<User> GetAllUsers()
{
// 实际项目中这里会连接数据库查询
return new List<User>
{
new User { Id = 1, Name = "张三", Email = "zhangsan@example.com" },
new User { Id = 2, Name = "李四", Email = "lisi@example.com" }
};
}
public User GetUserById(int id)
{
// 数据库查询逻辑
return new User { Id = id, Name = "数据库用户", Email = "db@example.com" };
}
public bool SaveUser(User user)
{
// 保存到数据库
return true;
}
}
// 文件实现
public class FileUserRepository : IUserRepository
{
public List<User> GetAllUsers()
{
// 实际项目中这里会读取文件
return new List<User>
{
new User { Id = 3, Name = "王五", Email = "wangwu@example.com" },
new User { Id = 4, Name = "赵六", Email = "zhaoliu@example.com" }
};
}
public User GetUserById(int id)
{
// 从文件读取
return new User { Id = id, Name = "文件用户", Email = "file@example.com" };
}
public bool SaveUser(User user)
{
// 保存到文件
return true;
}
}
在 WinForm 窗体中使用接口(而非具体实现):
public partial class UserForm : Form
{
// 依赖于接口而非具体实现
private IUserRepository _userRepository;
// 通过构造函数注入,灵活切换实现
public UserForm(IUserRepository userRepository)
{
InitializeComponent();
_userRepository = userRepository;
}
private void UserForm_Load(object sender, EventArgs e)
{
// 调用接口方法,无需关心具体是数据库还是文件
var users = _userRepository.GetAllUsers();
dataGridView1.DataSource = users;
}
private void btnGetUser_Click(object sender, EventArgs e)
{
int id = int.Parse(txtUserId.Text);
var user = _userRepository.GetUserById(id);
MessageBox.Show($"找到用户:{user.Name},邮箱:{user.Email}");
}
}
最后在 Program.cs 中选择具体实现:
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// 这里可以根据配置灵活切换数据源
// Application.Run(new UserForm(new SqlUserRepository()));
Application.Run(new UserForm(new FileUserRepository()));
}
}
这个示例的好处:
- 窗体代码完全不依赖具体的数据来源,只和接口交互
- 切换数据源时只需修改一处(Program.cs),无需改动窗体代码
- 便于单元测试,可以用模拟实现替代真实数据源
- 符合依赖倒置原则,高层模块(窗体)依赖抽象(接口)
当需要添加新的数据源(如 Web 服务)时,只需新增一个实现 IUserRepository 的类即可,现有代码无需修改。
在上面的示例中,依赖注入(Dependency Injection,简称 DI)是一种设计模式,它的核心思想是 **"让外部为类提供它所需要的依赖,而不是由类自己创建依赖"**。
结合前面的代码,我们来具体解释:
1. 什么是依赖注入?
在示例的UserForm中,我们需要一个IUserRepository的实例来获取用户数据。按照依赖注入的思想:
UserForm不自己创建IUserRepository的实例(比如不直接写_userRepository = new SqlUserRepository())- 而是通过构造函数参数的方式,由外部(比如
Program.cs)将具体的实现(SqlUserRepository或FileUserRepository)传递给它 -
// 构造函数注入:依赖由外部传入 public UserForm(IUserRepository userRepository) { InitializeComponent(); _userRepository = userRepository; // 直接使用外部传入的实例 }这里的
IUserRepository就是UserForm的 "依赖",而通过构造函数传递这个依赖的过程,就叫做 "依赖注入"。
2. 和直接new的区别
假设不使用依赖注入,而是在UserForm内部直接new一个具体实现:
// 直接new的方式(不推荐)
public partial class UserForm : Form
{
private IUserRepository _userRepository;
public UserForm()
{
InitializeComponent();
// 自己创建依赖,直接绑定到具体实现
_userRepository = new SqlUserRepository();
}
}
这种写法和依赖注入有本质区别:
| 对比维度 | 直接new的方式 | 依赖注入方式 |
|---|---|---|
| 耦合度 | 高:UserForm直接依赖SqlUserRepository的具体实现,改数据源必须修改UserForm代码 | 低:UserForm只依赖接口IUserRepository,与具体实现无关 |
| 灵活性 | 差:如果要切换到FileUserRepository,需要修改UserForm中的new语句 | 好:只需在外部(如Program.cs)修改传入的实现,UserForm无需任何改动 |
| 可测试性 | 差:无法替换为测试用的模拟实现(比如MockUserRepository) | 好:可以轻松传入模拟实现,单独测试UserForm的逻辑 |
| 代码职责 | 混乱:UserForm既要处理 UI 逻辑,又要负责创建数据源实例 | 清晰:UserForm只关注 UI 逻辑,数据源的创建由外部负责 |
3. 依赖注入的核心价值
还是用前面的例子:当需求变化(比如从 "数据库存储" 改为 "文件存储")时:
- 直接
new的方式:需要打开UserForm的代码,找到new SqlUserRepository()这一行,改成new FileUserRepository() - 依赖注入方式:只需在
Program.cs中改一行代码(从new SqlUserRepository()换成new FileUserRepository()),UserForm的代码完全不用动
这就是依赖注入的核心优势:将 "使用依赖" 和 "创建依赖" 的职责分离,让代码更灵活、更易维护,尤其在中大型项目中效果显著。
1520

被折叠的 条评论
为什么被折叠?



