[翻譯]清除靜態方法三板斧之一——靜態方法將使你大吃一驚

高翌翔發表於2011-12-27

原文連結:Static Methods will Shock You

宣告:本文與右側相關圖書《深入理解C#(第2版)》二者之間沒有直接關係,之所以選中此書作為相關圖書,是因為二者有個共同特點【Depth】,即對事物的深入探查(再說右邊空一塊兒也不美觀,呵呵)。


坦率地說,我不喜歡靜態方法。它們讓我坐立不安。在物件導向的宇宙中,那些靜態方法是反物質(anti-matter)。

它們並不壞,但要是使用不當,它們會變得很危險。

靜態方法何時是有益的

當使用靜態方法或變數時只有兩種情況不令人憎惡。

  1. 宣告一個真正的全域性常量,而非全域性變數。一個全域性常量。例如:Math.PI。實際說來,這是一個合理的簡寫,宇宙有一個例項,而且這個宇宙單例(singleton)包含一個數學概念單例(singleton),其中有一個不會更改的屬性PI(π,圓周率)。對我們而言這個概念似乎很奇怪,因為我們已經習慣於在物件導向職責的上下文中不去思考PI。如果我們設計過一些奇怪的遊戲,其中有若干擁有不同數學概念和常量的備選宇宙,那麼前面提到的情形會變得更明顯。
  2. 物件建立。 靜態方法是一種有價值且有效的物件建立方法。過載接受不同引數的建構函式的意圖並不非常清晰,而通常使用靜態建構函式替換它們會使意圖更加清晰。
public void BlogEntry(string title, string contents);
public void BlogEntry(string fileName);

當按以下形式編碼時,程式碼意圖會更加清晰

public static BlogEntry FromTitleAndContents(string title, string contents);
public static BlogEntry FromFile(string file);

在使用中,它會變得格外明白

BlogEntry newEntry = new BlogEntry("Hello World", "I like to write programs");
BlogEntry newEntry = new BlogEntry("helloworld.xml");
// vs.
BlogEntry newEntry = BlogEntry.FromTitleAndContents("Hello World", "I like to write programs");
BlogEntry newEntry = BlogEntry.FromFile("helloworld.xml");

靜態方法何時是有害的

除了那兩種用法以外。在我看來,任何其他用法都是令人憎惡的。(我想這裡我會略過C#擴充套件方法extension methods),因為在合適的條件下它們會非常有用,但我保留在未來作出判斷的權利。)

靜態方法通常預示著一個”無家可歸“(不知所屬)的方法。它在那裡袖手旁觀,試圖屬於它所在的那個類,但是它實際上不屬於那裡,因為它沒有使用那個類的內部狀態。當我們從單一職責原則(Single Responsibility Principle,縮寫為SRP)的角度來審視我們的類時,一個靜態方法通常是違反單一職責原則的,因為靜態方法往往會擁有一個與其附屬類不同的職責。

一種方式是將靜態方法想象為全域性處理過程(global procedures)。從本質上講,可以在任何地方的任意位置呼叫一個靜態方法。靜態方法僅僅是偽裝成某個類的一部分,那個類實際上只是被作為通過某種邏輯分組來組織該方法的”標籤(tag)“使用。我之所以從這些方面著眼於靜態方法,是因為建立全域性處理過程與物件導向設計截然相反。

靜態方法的另一主要問題是可測試性(testability)。當構建軟體時,可測試性是件大事。測試靜態方法的難度是眾所周知的,尤其是當它們建立具體類的新例項時。如果你曾在遺留程式碼上工作,並試圖為一個靜態方法編寫一個單元測試,那麼你就會懂我的痛。

靜態方法也不是多型的。如果你在某個類上建立了一個靜態方法,那麼無法重寫其行為。你被那個實現的一個硬編碼引用卡住而動彈不得。

最終,靜態方法增加了應用程式的複雜度。應用程式中的靜態方法越多,在那個應用程式上工作的程式設計師須要瞭解的東西也就越多。當在某個類中使用例項方法時,擁有該物件的例項將允許程式設計師來決定所有可以採取的行動。當使用靜態方法時,程式設計師必須知曉那些甚至可能都不在他正在工作的物件上,但用於操縱該物件的祕密方法。讓我們來看一個常見的例子:Date(日期)類和DateUtilities(日期實用工具)類。

在我曾經工作過的多個應用程式中,都有Date類和DateUtilities類。Date類的存在有助於提供一種日期和時間的實現,而DateUtilities類的存在有助於操縱日期例項,並且做些事情,例如算出每個月的第一天、或是確定假期、或是檢查兩個日期是否重疊。我不知道有多少次我想試圖編寫一個方法去做某件DateUtilities類已經做過的事情,因為我不知道去找一個被稱為DateUtilities的靜態類,其靜態方法已完成了我想做的工作。作為一名程式設計師,我必須記住,經常檢查DateUtilities類中的所有方法,以便查明其中是否有我所需的方法。當你工作在一個大型應用程式中,並與許多混在幫助類中的靜態方法一起工作時,這會導致很大的開銷。或許,我正蔓延至這裡的另一個問題,但關鍵是,與使用某個具有實際功能的類上的一個方法相比,記住那些飄忽世外的實用工具方法的存在要困難得多。(旁註:C#擴充套件方法可以稍微解決此類問題,你可以輸入”.“來檢視那些可以操縱該類的靜態方法。)

最後的話

因此,請謹記當你建立一個靜態方法時務必慎重考慮。我不主張決不使用它們。我主張要有一個充分的理由,並首先檢查該靜態方法是否實際屬於它可以使用其狀態資訊的另一個類。

應用擴充套件方法的基本準則——本人增補

通常,建議您只在不得已的情況下才謹慎地實現擴充套件方法。對於必須擴充套件現有型別的客戶端程式碼,只要有可能,都應該通過建立一個繼承自現有型別的新型別來達到此目的(即不要輕易實現擴充套件方法,它只是增強版的靜態方法而已!)。更多資訊,參閱繼承(C#程式設計指南)

當使用擴充套件方法來擴充套件你無法更改其原始碼的型別時,你需要承擔該型別實現中的更改會導致擴充套件方法失效的風險。

如果你確實為給定型別實現了擴充套件方法,請記住以下兩點:

  • 如果擴充套件方法與該型別中定義的方法具有相同的簽名,則擴充套件方法永遠不會被呼叫。
  • 擴充套件方法是在名稱空間級別被引入到(當前編碼的)作用域中的。例如,如果你在同一個名為 Extensions 的名稱空間下有多個包含擴充套件方法的靜態類,那麼通過 using Extensions; 指令將把這些擴充套件方法全部引入到作用域中。

預知後事如何,請看下回《清除靜態方法三板斧之二——我是否應該保留助手類》

相關文章