SOLID 审查
作者:Guerth Castro (github.com/GuerthCastro)。MIT 许可。
审查流程
当提供代码进行审查时:
- 识别语言和层(C# 领域实体、应用程序处理器、Angular 服务、 React 组件等)。
- 独立评估五个原则中的每一个。
- 使用以下格式生成结构化报告。
- 按严重性对违规进行排序:严重性(破坏架构)> 主要(降低可维护性)> 次要(风格偏好)。
- 为每个严重性或主要违规提供具体的重构版本。
不要填充报告。如果一个原则确实得到满足,则标记为“通过”并继续。
五个原则
S:单一职责原则
一个类,一个变更理由。
需查找的违规情况:
- 一个类同时进行数据访问和业务逻辑。
- 一个服务发送电子邮件、验证数据并写入数据库。
- 一个组件获取数据、格式化数据并管理 UI 状态。
- 一个处理器协调多个不相关的操作。
- 包含十个或更多方法、跨越多个领域的上帝类。
违规。 OrderService 进行验证、持久化和通知。通知调用是不属于的部分。
public class OrderService(IOrderRepository repository, IEmailService email)
{
public async Task<bool> AddItem(long orderId, long productId)
{
Order order = await repository.GetById(orderId);
if (order.ItemCount >= order.MaxItems)
{
return false;
}
await repository.AddItem(orderId, productId);
await email.SendOrderUpdated(orderId);
return true;
}
}
修复:提取一个通知服务,并让处理器协调两个调用。
O:开闭原则
对扩展开放,对修改关闭。
需查找的违规情况:
- 基于类型字符串或枚举的
if/else if链或switch块,随每个功能而增长。 - 添加新报告类型需要编辑现有的报告服务。
- 添加新支付方式需要编辑结账处理器。
- 本应使用抽象的地方却使用了具体依赖。
违规。 新导出格式意味着需要编辑此方法。
public byte[] ExportInvoice(string format, IEnumerable<InvoiceLine> lines)
{
if (format == "pdf")
{
return GeneratePdf(lines);
}
if (format == "excel")
{
return GenerateExcel(lines);
}
throw new NotSupportedException(format);
}
修复:创建 IInvoiceExporter 抽象,包含 PdfInvoiceExporter 和 ExcelInvoiceExporter
实现,并通过容器按键解析。
L:里氏替换原则
子类型必须可替换为其基类型。
需查找的违规情况:
- 派生类对继承的成员抛出
NotImplementedException。 - 重写改变了预期行为:静默无操作、不同的返回形状、新的异常类型。
- 接口声明了某些实现无法有意义地实现的成员。
- 通过强制转换为具体类型来访问行为:
if (repository is SqlOrderRepository sql)。
I:接口隔离原则
不应强迫任何类实现它不使用的成员。
需查找的违规情况:
- 包含十五个或更多成员的胖接口,而实现仅使用其中三个。
- 通用的
IRepository<T>强制每个实体使用GetAll、GetById、Create、Update、Delete、Paginate、Search和BulkInsert。 - 服务接口混合了读写操作,而消费者只需要一侧。
违规。 只读报告消费者被迫看到 Create、Update 和 Delete。
public interface IOrderRepository
{
Task<Order> GetById(long id);
Task<IEnumerable<Order>> GetAll();
Task Create(Order order);
Task Update(Order order);
Task Delete(long id);
}
修复:拆分为 IOrderReader 和 IOrderWriter。在需要两者时组合它们。
D:依赖反转原则
依赖于抽象,而不是具体实现。
需查找的违规情况:
- 在业务逻辑中使用
new实例化服务或仓库。 - 在服务内部直接依赖
SqlConnection或NpgsqlConnection。 - 应用层导入基础设施类型。
- 持有可变状态的静态服务类。
违规。 处理器构建自己的具体仓库和连接。
public class GetOrderHandler
{
public async Task<OrderDto> Handle(long id)
{
OrderRepository repository = new(new SqlConnection("..."));
Order order = await repository.GetById(id);
return order.MapToDto();
}
}
修复:通过主构造函数注入 IOrderRepository,并在组合根容器中注册它。
报告格式
每次审查生成以下结构:
## SOLID 审查:[ClassName 或 文件名]
**语言/层:** C# 应用处理器、Angular 服务等
### S:单一职责
状态:通过 / 违规(严重、主要、次要)
发现: [该类超出其单一职责的行为]
修复: [具体操作]
### O:开闭
状态:通过 / 违规
发现:...
修复:...
### L:里氏替换
状态:通过 / 违规 / 不适用(无继承)
发现:...
### I:接口隔离
状态:通过 / 违规 / 不适用(无接口)
发现:...
### D:依赖反转
状态:通过 / 违规
发现:...
修复:...
### 优先级违规
1. [严重] DIP:处理器实例化了具体仓库
2. [主要] SRP:服务还发送了通知电子邮件
### 重构版本
[完整修正代码块]
整洁架构分层违规
除了 SOLID 之外,还要标记这些横切问题:
- 领域实体导入应用或基础设施类型(依赖方向违规)。
- 控制器包含应属于处理器的业务逻辑。
- 仓库返回 DTO 而不是领域实体。映射应属于应用层。
- 基础设施直接调用外部 API,而不是实现应用层或领域层声明的接口。
- 处理器触及
HttpContext,将基础设施关注点泄露到应用层。 - DI 注册位于错误的程序集中,导致层为了编译而下向引用。