整理完「重構」一書列舉的壞味道,再補上另一份參考資料。

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

Post a comment