使用通用接口解耦时调用组件独有特性的最佳实践探讨
最佳实践解析:ILogger解耦与Serilog特性的平衡
针对你提到的用ILogger
1. 按需权衡:区分通用与特有语法的使用场景
- 通用语法优先:Microsoft.Extensions.Logging(MEL)定义的基础模板语法(比如
{Elapsed})是所有兼容MEL的日志库都支持的,这类语法放心用,完全不影响解耦。 - 特有特性视场景取舍:像Serilog的
{@Position}解构语法,能把对象完整序列化为结构化日志,大幅提升日志的可查询性和可读性。如果项目已经确定Serilog为长期日志方案,且这个特性的价值远超过“可能更换日志库”的极小概率成本,直接使用就好——毕竟Serilog的生态成熟度和稳定性,决定了大部分项目不会轻易更换。 - 严格解耦的替代方案:如果必须保留绝对解耦,就放弃Serilog特有语法,改用MEL通用方式拆分对象参数,比如:
缺点是丢失了结构化日志的优势,适合对日志结构化要求不高的场景。_logger.LogInformation("Processed Latitude:{Latitude}, Longitude:{Longitude} in {Elapsed} ms", position.Latitude, position.Longitude, elapsedMs);
2. 封装隔离:用自定义接口屏蔽特有逻辑
如果既要用Serilog的特有特性,又想保留业务代码与Serilog的解耦,可以把特有日志逻辑封装到自定义接口中:
- 定义一个抽象接口,比如
IStructuredLogger<T>,暴露需要的结构化日志能力 - 用Serilog实现这个接口,内部处理特有语法
- 业务代码依赖自定义接口,而非直接使用ILogger
写特有语法
示例代码:
// 自定义抽象接口 public interface IStructuredLogger<T> { void LogStructuredInformation(string messageTemplate, object structuredProperty, params object[] args); } // Serilog实现类 public class SerilogStructuredLogger<T> : IStructuredLogger<T> { private readonly ILogger<T> _logger; public SerilogStructuredLogger(ILogger<T> logger) { _logger = logger; } public void LogStructuredInformation(string messageTemplate, object structuredProperty, params object[] args) { // 内部使用Serilog特有语法 _logger.LogInformation(messageTemplate, structuredProperty, args); } }
后续如果更换日志库,只需要重新实现IStructuredLogger<T>,业务代码无需修改,完美兼顾特性与解耦。
3. 接受“有限解耦”:放弃理想化的完全解耦
实际项目中,完全解耦是理想化目标,很多时候深度使用组件的特有特性,带来的效率提升和维护便利,远大于未来更换组件的成本。
- 解耦的核心目的是降低维护成本,而非为了解耦而解耦。如果Serilog的结构化日志能让排查问题的效率提升数倍,那依赖它的特有特性就是合理的选择。
- 团队内部达成技术栈共识,文档化所有使用的组件特有特性,就算未来真的需要更换,也能快速定位需要修改的代码块。
通用组件使用原则(扩展到其他场景)
- 优先使用抽象层通用能力:先看组件对应的抽象标准(比如MEL对于日志、EF Core对于ORM)提供的通用功能,能满足需求就用通用能力保证解耦。
- 特有特性封装隔离:如果组件特有特性是业务必需的,通过自定义接口或服务封装,把特有逻辑限制在实现层,不侵入业务代码。
- 评估成本后决策:不要为了虚无的“未来可能更换”放弃核心功能,先评估更换组件的实际概率和成本,再决定是否使用特有特性。
内容的提问来源于stack exchange,提问作者Cheng Chen
相关产品推荐
相关产品推荐

