C#中的條件重構:避免Derek Comartin的條件混亂
在 C# 中,像 if 陳述式、if else 陳述式和 switch 陳述式這樣的條件陳述式是基本工具。 但是當這些結構被過度使用時會發生什麼事——特別是與枚舉綁定時? Derek Comartin 在他的影片中"Enums Aren't Evil. Conditionals Everywhere Are",帶領我們詳盡地重構,將廣泛的條件邏輯替換為更乾淨、更易維護的模式。
在這篇文章中,我們將逐步走過 Derek 的推理,使用他的時間標記作為錨點。 我們還將探討他的想法如何應用於 C# 中的常見條件模式,如三元運算符、else 陳述式和 switch-case 結構——突出這些可能在大型代碼庫中造成問題的地方,以及如何重構以達到更好的設計。
條件 If 爆炸:真正的問題
Derek 首先展示了一個 if 陳述式,用來檢查產品類型:
if (productType == ProductType.Template || productType == ProductType.Ebook)
if (productType == ProductType.Template || productType == ProductType.Ebook)
乍看之下,上面的條件 if 看似簡單直觀。 但 Derek 警告說,這種陳述式會評估給定的條件,然後只有在條件為真時執行代碼塊——這種邏輯在各處重複使用時會變得有問題。
您可能會在另一個方法或類中再次看到這個塊:
if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)
if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)
Derek 解釋說,這種模式會迅速在大型系統中擴散。 同樣的 if else 邏輯出現在多個服務中,當添加一個新的枚舉值時,會導致不一致和錯誤。 例如,當您引入新的產品類型,如影片時,會發生什麼? 您需要記住更新每一個存在該條件表達式的代碼塊。
重複放大複雜性
在下面的例子中,Derek 深入研究了巢狀條件。 在一個方法中,if else 陳述式檢查同一個枚舉,然後將結果傳遞給另一個方法,而該方法也包含類似的檢查。
該陳述式檢查 Template 或 Ebook,並返回某個值——否則返回 null。 Derek 指出,這種冗餘不僅使代碼變長,還帶來了維護上的危險。 同樣的邏輯被拷貝到多個文件中,導致控制流混亂。
如果您的系統要求您在每次修改枚舉時都添加一個預設情況,則說明出了問題。
改變我們對條件的思考方式
Derek 不斷檢查類型的 if else,而是建議提出一個更好的問題:
該產品是否具有可下載的特性?
這是一種更好的表達意圖。 它使您的代碼更具可讀性,並減少對枚舉的依賴。 與其寫一個具有兩個條件的 if 陳述式:
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
您可以簡單地將邏輯封裝在模型中並編寫:
if (product.HasDownloadableResource())
if (product.HasDownloadableResource())
這只在存在可下載資源時返回 true——減少複雜的條件表達式的需求。
從 If 陳述式到封裝行為
為了解決核心問題,Derek 引入了一種類型 DownloadableResource。 這種類型包括一個下載 URL 和預設文件名。 它成為您域中的一等部分,而不是依賴 if 陳述式推導其存在。
現在,您不再重複這個:
if (product.Type == ProductType.Template)
{
// Generate file name
}
else if (product.Type == ProductType.Ebook)
{
// Generate file name
}
if (product.Type == ProductType.Template)
{
// Generate file name
}
else if (product.Type == ProductType.Ebook)
{
// Generate file name
}
您編寫這樣的程式碼:
var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
Console.WriteLine(downloadable.FileName);
}
var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
Console.WriteLine(downloadable.FileName);
}
這大幅簡化邏輯,消除了 else 陳述式分支甚至 switch 陳述式的需求。
運行時超越編譯時:一個戰略轉移
Derek 進一步解釋了一個重要的設計選擇:將邏輯從編譯時轉移至運行時。這意味著在運行時查詢系統以查看產品是否存在 DownloadableResource。 如果存在,則執行操作。 如果不存在,則跳過。
他指出,這種移動將靜態的 if else 邏輯轉換為運行時查詢。 這可能增加一個數據庫調用,但它減少了巢狀的 if else 邏輯並集中化行為。 這在大規模上改善了可維護性。
使用繼承來處理可下載產品
Derek 探討的另一個路徑是繼承。 您可以創建一個抽象基類 Product,然後定義衍生類型,如 Ebook、Template 或 OfflineCourse。
每個都覆蓋像這樣的方法:
public virtual string GetDownloadUrl() { ... }
public virtual string GetDownloadUrl() { ... }
這種方法允許每個產品處理自己的邏輯。 雖然這避免了 switch 陳述式或多個條件陳述式,但 Derek 指出,如果不小心,您仍有可能在內部編寫條件表達式。
更好的封裝而無需繼承
如果繼承太過繁重,Derek 建議使用像 DownloadableProduct 這樣的明確類型,包含其自身的屬性和方法——而不需要被綁定到一個層級結構。
在您的程式中,這可能看起來是這樣的:
var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());
var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());
不需要 if else 或 switch 陳述式來決定行為——每個對象知道要做什麼。
輕量修正:擴展方法上的枚舉
如果您尚未準備好放棄枚舉,Derek 提供了一個輕量解決方案——創建擴展方法:
public static bool IsDownloadable(this ProductType type)
{
return type == ProductType.Template || type == ProductType.Ebook;
}
public static bool IsDownloadable(this ProductType type)
{
return type == ProductType.Template || type == ProductType.Ebook;
}
現在,與其編寫:
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
您可以簡化為:
if (product.Type.IsDownloadable())
if (product.Type.IsDownloadable())
這使您的邏輯集中化,避免一遍又一遍地重複大括號和代碼塊。
避免過度使用三元運算符和 switch
Derek 也警告不要過度使用像三元運算符這樣的簡短代碼:
string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";
string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";
雖然它是有效語法,但當邏輯變得複雜時可能容易出錯且難以閱讀。 尤其是當您的條件評估為 false 時,錯誤的值可能會在不明顯的方式中被賦予。
同樣,帶有 break 陳述式和預設情況的 switch 也會陷入這個陷阱。最好問對象該如何行為,而不是使用 switch-case 邏輯。
結論:以更少的條件混亂實現更智能的控制
總之,Derek 的 影片 並不是對枚舉的攻擊——而是對我們在枚舉周圍使用條件 if 結構的批判。 通過在您的代碼庫中分散 if else 和 switch 陳述式,您使系統更難以測試、維護和發展。
無論您選擇封裝、運行時查詢、繼承還是簡單的擴展方法,目標都保持不變:減少條件,並將邏輯移動到應在的位置。
請記住:
-
條件並不是壞的。
-
條件混亂才是。
-
乾淨的代碼不依賴散佈在類之間的多個 if else 陳述式。
- 評估您的情境並相應地重構。
正如 Derek 所說,"這取決於您的上下文。"但有一點是肯定的:產品並非總是僅僅是產品——有時它是一個重新思考您設計的信號。
