SOLID 審查
作者:Guerth Castro (github.com/GuerthCastro)。採用 MIT 授權。
審查流程
當提供程式碼進行審查時:
- 識別語言與層級(C# 領域實體、應用程式處理程式、Angular 服務、React 組件等)。
- 獨立評估五項原則。
- 使用以下格式產出結構化報告。
- 按嚴重程度排序違規:重大(破壞架構)> 主要(降低可維護性)> 次要(風格偏好)。
- 為每一項重大或主要違規提供具體的重構版本。
請勿在報告中填充無關內容。如果某項原則確實滿足,則標記為 PASS 並繼續。
五項原則
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:單一職責
狀態:PASS / 違規(重大、主要、次要)
發現: [該類別超出其單一職責的行為]
修正: [具體行動]
### O:開放/封閉
狀態:PASS / 違規
發現:...
修正:...
### L:里氏替換
狀態:PASS / 違規 / N/A(無繼承)
發現:...
### I:介面隔離
狀態:PASS / 違規 / N/A(無介面)
發現:...
### D:依賴反轉
狀態:PASS / 違規
發現:...
修正:...
### 優先違規
1. [重大] DIP:處理程式實例化具體儲存庫
2. [主要] SRP:服務同時發送通知電子郵件
### 重構版本
[完整的修正程式碼區塊]
整潔架構層級違規
除了 SOLID 之外,同時標記這些跨領域問題:
- 領域實體匯入應用程式或基礎設施型別(依賴方向違規)。
- 控制器包含本應屬於處理程式的商業邏輯。
- 儲存庫回傳 DTO 而非領域實體。對映屬於應用程式層。
- 基礎設施直接呼叫外部 API,而不是實作在應用程式或領域中宣告的介面。
- 處理程式接觸
HttpContext,將基礎設施關注點洩漏到應用程式層。 - DI 註冊在錯誤的組件中,導致某個層級為了編譯而必須向下參考。