重構筆記 - .NET 壞味道補充包
4 |
整理完「重構」一書列舉的壞味道,再補上另一份參考資料。
NDepend 是個評估程式碼好壞,估算技術債的 Visual Studio 擴充套件。它會用事先定義的規則分析專案,清查程式碼的壞味道,給出一組品質指標及技術債(Technical-Debt)估價,技術債的計量單位是預估償債(調整程式)所需的工時,還包含利息(如果放著不修,每年增加的工時),在專案開發過程可以持續觀察債務變多還是減少,是蠻具體有趣的程式碼品質指標。
關於 NDepend 的應用說明可參考保哥跟亂馬客的文章:
我特別感興趣的部分是 NDepend 盤點列舉的程式碼問題點,也可視為是 NDepend 判定的壞味道。NDepend 在網站提供一份規則清單,其中包含不少與壞味道有關的規則說明(注意:以 .NET 為主),頗具參考價值,趁著最近在研究壞味道,我就一併整理當成補充包:(註:部分項目不到「壞味道」的程度,較偏向「好習慣」,參考時請自行評估拿捏)
Avoid types too big
當型別程式碼超過 200 行(俗稱 God Class,包辦大小事的萬用類別),因複雜度上升,維護難度也會提高。解決方式是將 God Class 依職責拆成多個較小的巢狀子類別,
Avoid types with too many methods
型別擁有超過 20 個方法(不含建構式、屬性及事件),導致不易理解及維護,通常這意味型別被賦與過多職責,解法同上。
Avoid types with too many fields
型別擁有超過 15 個欄位(不含常數、靜態唯讀欄位、列舉),導致不易理解及維護,通常這意味型別被賦與過多職責,解法同上。
Avoid methods too big, too complex
方法內的程式碼過多或過於複雜(俗稱 God Method),解法是拆解成多個較小方法。
Avoid methods with too many parameters
方法擁有超過七個參數。(可參考前文 Long Parameter List)
Avoid methods with too many overloads
方法擁有超過六個多載。建議用 .NET 選擇性及具名參數特性取代。
Avoid methods potentially poorly commented
擁有 20 行以上程式碼的方法註解不到 10%。
Avoid types with poor cohesion
型別大部分的方法有大量使用自家欄位,可視為型別職責是否單一的參考指標。
Avoid methods with too many local variables
方法使用的暫存變數應少於 15 個。(理由可參考前文 Long Function)
Avoid interfaces too big
介面定義的方法應少於 10 個,以避免職責過多。
Base class should not use derivatives
基底類別使用(依賴)其衍生類別,易違反開放封閉原則(Open Closed Principle,對擴展是開放的-加程式即可,對修改是封閉的(不需修改既有程式))。
Class shouldn't be too deep in inheritance tree
繼承關係不應超過 3 層,否則多是設計不良難以維護的徵兆。
Class with no descendant should be sealed if possible
當類別並不打算當成基底類別供人繼承改寫,加上 sealed 防止繼承可讓程式意圖更明確。
Overrides of Method() should call base.Method()
覆寫基底類別方法時,應呼叫父類別的同名方法,以避免遺漏必要處理。
Do not hide base class methods
當基底類別未以 virtual 開放覆寫,衍生類別不宜同名方法置換基底類別,以免造成混淆。
A stateless class or structure might be turned into a static type
只指定靜態屬性、方法的型別,應該也要宣告成靜態。(建議)
Non-static classes should be instantiated or turned to static
若類別的建構式從未被使用過,也沒建立過 Instance,應該要改為靜態類別。
Methods should be declared static if possible
方法的存取範圍(Scope)愈小愈不容易出錯,宣告成靜態後就不能存取非靜態成員,有助於縮小範圍。(對於效能也有微小幫助)
Constructor should not call a virtual method
在建構式中呼叫虛擬方法,可能出現處擬方法需等待建構式執行完畢狀態資訊才會完整的僵局,應避免之。
Avoid the Singleton pattern
Singleton 常造成程式不易測試與維護,尤其是用 GetInstance() 靜態方法。替代做法是在程式啟動時建立單一 Instance,並將其傳給所有需要的類別。(註:用 IoC 或依賴注入機制也很 OK)
Don't assign static fields from instance methods
在 Instance 方法中修改靜態欄位造成程式不易維護且易有 Thread-Safe 議題。
Avoid empty interfaces
用不定義方法的空白介面當成分類依據易造成混淆,不是好主意(介面不是給你拿來這樣用滴),建議改用 Attribute。
Avoid types initialization cycles
靜態建構式會在第一次使用該型別時呼叫,若你在 T1 靜態建遘式中用到 T2, 在 T2 靜態建遘式中用到 T1,就會產生難以預測的結果。
Avoid custom delegates
用 Action<...>、Func<...>、Lambda 取代自訂委派(Delegate)(除非是 DllImport 呼叫外部函式需要)。
Avoid namespaces mutually dependent、Avoid namespaces dependency cycles、Avoid mutually-dependent types
避免命名空間、型別出現交叉依賴或循環依賴。
Avoid partitioning the code base through many small library Assemblies
避免產生大量細碎的 dll。(不利於編譯、啟動、部署效率)
UI layer shouldn't use directly DB types、UI layer shouldn't use directly DAL layer
UI 層(WPF/WinForm/ASP.NET Web)不該直接存取 ADO.NET、EF、NHibernate...
Assemblies with poor cohesion、Namespaces with poor cohesion
同一組件、命名空間內的型別應互流密切、緊密相關。
Methods/Types/Fields could have a lower visibility
若無需要,儘量以 private/internal 取代 public。
Immutability
儘可能將(靜態/Instance)欄位設為 readonly,使用不可修改的型別,避免被外界更改內容
Instance fields naming convensions
小寫字母起首 / "_" 加小寫字母起首 / "m_" 加大寫字母起首
Static fields naming convention
大寫字母起首 / "_" 加大寫字母起首 / "s_" 加大寫字母起首
Interface name should begin with a 'I'
介面名稱永遠以大寫 I 起首。
Abstract base class should be suffixed with 'Base'
抽象類別名稱以 Base 結尾。
Exception class name should be suffixed with 'Exception'
例外類別名稱以 Exception 結尾。
Attribute class name should be suffixed with 'Attribute'
Attribute 類別名稱以 Attribute 結尾。
Types/Methods name should begin with an Upper character
型別/方法名稱永遠以大寫字母起首。
Avoid XXXX with name too long
避免型別、方法、欄位名稱過長(建議不超過 40 個字元)。
Avoid having different types with same name
避免型別與系統或第三方組件內的型別名稱相同。
Avoid prefixing type name with parent namespace name
型別名稱前方不要再冠上命名空間的名字。
Avoid naming types and namespaces with the same identifier
型別名稱不要跟命名空間相同。
Methods prefixed with 'Try' should return a boolean
命名為 TryXXXX 的方法應該要傳回 bool。
Properties and fields that represent a collection of items should be named Items
傳回集合的屬性或欄位命名請用複數名詞,如 NewDirectories, Words, Values, UpdatedDates, TasksToRun, KeysDisabled, VersionsSupported, ChildrenFilesPath, DatedValuesDescending, ApplicationNodesChanged 或 ListOfElementsInResult.
Avoid fields with same name in class hierarchy
避免使用父類別已存在的欄位名稱。
Avoid various capitalizations for method name
避免使用英文相同只有大小寫不同的坑人命名。
Avoid defining multiple types in a source file
避免在一個程式檔定義多個型別。
Namespaces and type source files structure
命名空間、型別應儘量對映成實體資料夾與檔案。
Remove calls to GC.Collect()
避免呼叫 GC.Collect() 強制記憶體回收,通常這樣會影響效能。如果你有充分理由非做不行,請依下列方法:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Do not raise too general exception types
如要回拋錯誤,請使用明確一點的例外型別方便呼叫端識別處理。避免使用 Exception、ApplicationException、SystemException。
Do not raise reserved exception types
回拋錯誤時避免借用系統保留的特殊例外型別,例如:ExecutionEngineException、IndexOutOfRangeException、NullReferenceException、OutOfMemoryException、StackOverflowException、InvalidProgramException、AccessViolationException、CannotUnloadAppDomainException、BadImageFormatException、DataMisalignedException,以免造成呼叫端混淆。
Types should not extend System.ApplicationException
自訂例外型別時,請直接繼承 System.Exception,不要繼承 System.ApplicationException。
Don't Implement ICloneable
ICloneable 是 .NET 1.x 時代的遺跡,如要實現複製功能,不必實作任何介面,提供 DeepClone() 或 ShallowClone()。
Collection properties should be read only
集合屬性(System.Collections)應保持唯讀避免被外界整個置換,如要開放重設,可考慮提供 Clear()、AddRange() 方法。
Don't use .NET 1.x HashTable and ArrayList
請改用 List<T>、Dictionary<K,V>、Queue<T>、Stack<T>...
Caution with List.Contains()
List.Contains() 採逐一比對,筆數一多時效率很差,請改用 HashSet<T>。延伸閱讀:選擇合適的集合類別
Prefer return collection abstraction instead of implementation
當方法傳回結果 List<T>、Dictionary<K,V>、HashSet<T>,呼叫端通常不需要知道實際的集合型別,此時可改用 IList<T>、<T>、IDictionary<K,V>,未來更換底層實作時呼叫端不會受影響。
P/Invokes should be static and not be publicly visible
使用 DllImport 連的 Unmanaged API 方法請宣告為 private/internal static,避免被外界存取 Windows API 形成安全漏洞。 並建議集中放到三個 private static 類別 NativeMethods、SafeNativeMethods、UnsafeNativeMethods,依其誤用風險分類管理。
Don't create threads explicitly
儘量使用 TPL/ThreadPool,避免自己建立 Thread。建立、消滅、切換 Thread 很消資源,若 Thread 數過多、頻率過高都會傷害效能。
Don't use dangerous threading methods
請謹慎使用 Thread.Abort()、Sleep()、Suspend()、Resume()。參考
Digest of NDepend's reference of code smells related rules.
Comments
# by Paul Ma
1 個Developer License 399 美金
# by ovo
感謝分享!
# by OUO
請問不繼承ICloneable的複製,要如何實做呢 目前都是使用ICloneable來做Clone使用MemberwiseClone 可是有時候會發現輸出跟預期的有點落差
# by Jeffrey
to OUO, 我對 ICloneable 沒啥研究,依參考資料的說法,它屬於.NET 1.x 時的過時標準,沒區分淺層複製或深層複製,可能造成結果與預期不同。建議做法可參考這篇:http://codinghelmet.com/articles/how-to-avoid-the-need-to-implement-icloneable-interface-and-what-to-do-instead