C#中依赖注入多接口类的优雅处理方案(以Renci.SshNet为例)
解决方案
方案1:自定义整合接口+适配器模式
这是最符合SOLID原则、测试友好的方案,彻底解决类型转换的问题:
- 定义整合接口:创建一个同时继承
ISftpClient和IBaseClient的接口,利用C#多接口继承特性:
public interface IEnhancedSftpClient : ISftpClient, IBaseClient { // 无需新增方法,自动继承两个接口的所有成员 }
- 实现适配器类:包装原生
SftpClient,实现上述整合接口,所有方法直接转发给内部的SftpClient实例:
public class SftpClientAdapter : IEnhancedSftpClient { private readonly SftpClient _innerClient; public SftpClientAdapter(SftpClient innerClient) { _innerClient = innerClient ?? throw new ArgumentNullException(nameof(innerClient)); } // 实现ISftpClient的方法,直接转发 public void UploadFile(Stream input, string path) => _innerClient.UploadFile(input, path); public Stream DownloadFile(string path) => _innerClient.DownloadFile(path); // ... 按需实现其他ISftpClient方法 // 实现IBaseClient的方法,直接转发 public void Connect() => _innerClient.Connect(); public void Disconnect() => _innerClient.Disconnect(); public bool IsConnected => _innerClient.IsConnected; public ConnectionInfo ConnectionInfo => _innerClient.ConnectionInfo; // ... 按需实现其他IBaseClient方法 // 实现IDisposable接口 public void Dispose() => _innerClient.Dispose(); }
- 依赖注入配置:注册适配器和原生客户端,让业务类依赖
IEnhancedSftpClient:
// 注册原生SftpClient(根据你的配置调整) services.AddScoped<SftpClient>(sp => new SftpClient("host", "username", "password")); // 注册适配器作为IEnhancedSftpClient的实现 services.AddScoped<IEnhancedSftpClient>(sp => new SftpClientAdapter(sp.GetRequiredService<SftpClient>()));
- 业务类使用:直接注入
IEnhancedSftpClient,无需任何类型转换:
public class SftpFileUploader { private readonly IEnhancedSftpClient _sftpClient; public SftpFileUploader(IEnhancedSftpClient sftpClient) { _sftpClient = sftpClient; } public void Upload(Stream fileStream, string remotePath) { if (!_sftpClient.IsConnected) { _sftpClient.Connect(); } _sftpClient.UploadFile(fileStream, remotePath); _sftpClient.Disconnect(); } }
优点:完全消除类型转换,测试时可轻松mockIEnhancedSftpClient,无需依赖Renci的具体实现;代码结构清晰,符合依赖反转原则。
缺点:需要编写适配器代码,若接口方法较多,会有一定样板代码量。
方案2:扩展方法封装类型转换逻辑
如果不想编写大量适配器代码,可以用扩展方法把类型转换逻辑封装起来,让业务代码更简洁:
public static class SftpClientExtensions { public static void Connect(this ISftpClient client) { if (client is not IBaseClient baseClient) { throw new InvalidOperationException("当前SFTP客户端实例未实现IBaseClient接口"); } baseClient.Connect(); } public static void Disconnect(this ISftpClient client) { if (client is not IBaseClient baseClient) { throw new InvalidOperationException("当前SFTP客户端实例未实现IBaseClient接口"); } baseClient.Disconnect(); } // 按需添加IBaseClient的其他方法扩展 public static bool IsConnected(this ISftpClient client) { return client is IBaseClient baseClient && baseClient.IsConnected; } }
业务类使用:直接调用扩展方法,无需显式转换:
public void Upload(Stream fileStream, string remotePath) { if (!_sftpClient.IsConnected()) { _sftpClient.Connect(); } _sftpClient.UploadFile(fileStream, remotePath); _sftpClient.Disconnect(); }
优点:代码改动极小,几乎不需要修改现有注入逻辑;业务代码保持简洁。
缺点:本质还是依赖类型转换,测试时若使用不实现IBaseClient的ISftpClient mock实例,会抛出异常(实际场景中Renci的SftpClient是实现该接口的,测试时可让mock同时实现两个接口)。
方案对比与推荐
- 若项目注重测试性和代码架构的长期可维护性,方案1是最优选择,彻底解耦了对Renci客户端具体实现的依赖。
- 若只是想快速优化现有代码,不想引入过多结构变化,方案2更适合,以最小改动解决代码优雅性问题。
内容的提问来源于stack exchange,提问作者Brian Kessler
相关产品推荐
相关产品推荐

