開發(fā)人員總是喜歡就編碼規(guī)范進(jìn)行爭論,但更重要的是如何能夠在項目中自始至終地遵循編碼規(guī)范,以保證項目代碼的一致性。并且團(tuán)隊中的所有人都需要明確編碼規(guī)范所起到的作用。在這篇文章中,我會介紹一些在我多年的從業(yè)過程中所學(xué)習(xí)和總結(jié)的一些較好的實踐。
舉例為先
我們先來看一個 FizzBuzz 示例。FizzBuzz 要求編寫一個程序,遍歷從 1 到 100 的數(shù)字。其中如果某數(shù)字是 3 的倍數(shù),則程序輸出 “Fizz”。如果某數(shù)字是 5 的倍數(shù),則輸出 “Buzz”。如果某數(shù)字即是 3 的倍數(shù)也是 5 的倍數(shù),則輸出 “FizzBuzz”。如果數(shù)字既不是 3 的倍數(shù)也不是 5 的倍數(shù),則只需輸出該數(shù)字本身。
示例1:
1 public static void Test() 2 { 3 for (int i = 1; i < 101; i++) 4 { 5 if (i % 3 == 0 && i % 5 == 0) 6 { 7 Console.WriteLine("FizzBuzz"); 8 } 9 else if (i % 3 == 0) 10 { 11 Console.WriteLine("Fizz"); 12 } 13 else if (i % 5 == 0) 14 { 15 Console.WriteLine("Buzz"); 16 } 17 else 18 { 19 Console.WriteLine(i); 20 } 21 } 22 }
什么感覺?這段代碼需要改進(jìn)嗎?
示例2:
1 public static void Check() 2 { 3 for (int i = 1; i <= 100; i++) 4 { 5 string output = ""; 6 if (i % 3 == 0) { output = "Fizz"; } 7 if (i % 5 == 0) { output = output + "Buzz"; } 8 if (output == "") { output = i.ToString(); } 9 Console.WriteLine(output); 10 } 11 }
現(xiàn)在感覺如何?還能不能進(jìn)一步改進(jìn)?
好,讓我們來嘗試改進(jìn)下。代碼命名對所有軟件開發(fā)人員來說都是件非常困難的事情。我們花費(fèi)了大量的時間來做這件事,而且有太多的需要被命名的元素,例如屬性、方法、類、文件、項目等。不過我們的確需要花費(fèi)一些精力在這些命名上,以使代碼中的名稱更有意義,進(jìn)而可以提高代碼的可讀性。
1 public void DoFizzBuzz() 2 { 3 for (int number = 1; number <= 100; number++) 4 { 5 var output = GetFizzBuzzOutput(number); 6 Console.WriteLine(output); 7 } 8 } 9 10 private static string GetFizzBuzzOutput(int number) 11 { 12 string output = string.Empty; 13 if (number % 3 == 0) 14 { 15 output = "Fizz"; 16 } 17 if (number % 5 == 0) 18 { 19 output += "Buzz"; 20 } 21 if (string.IsNullOrEmpty(output)) 22 { 23 output = number.ToString(); 24 } 25 return output; 26 }
這次感覺怎樣?是不是比之前的示例要好些?是不是可讀性更好些?
什么是更好的代碼?
首先就是代碼要為人來編寫,其次是為機(jī)器。從長期來看,編寫可讀性好的代碼不會比編寫混亂的代碼要花費(fèi)更長的時間。如果你能夠非常容易地讀懂你寫的代碼,那么想確認(rèn)其可以正常工作就更容易了。這應(yīng)該已經(jīng)是編寫易讀代碼足夠充分的理由了。在很多情況下都需要閱讀代碼,例如在代碼評審中會閱讀你寫的代碼,在你或者其他人修復(fù)Bug時會閱讀你寫的代碼,在代碼需要修改時也會讀到。還有就是當(dāng)其他人準(zhǔn)備在類似的項目或有類似功能的項目中嘗試復(fù)用你的部分代碼時也會先閱讀你的代碼。
“如果你只為你自己寫代碼,為什么要使代碼更具可讀性?”
好,編寫易讀的代碼最主要的原因是,在未來的一到兩周,你將工作在另一個項目上。而此時,有其他人需要修復(fù)當(dāng)前項目的一個Bug,那么將會發(fā)生什么?我敢保證他肯定會迷失在你自己編寫的恐怖代碼中。
從我的個人觀點(diǎn)來看,好的代碼應(yīng)該擁有以下幾個特征:
代碼容易編寫,并易于修改和擴(kuò)展。
代碼干凈,并表述準(zhǔn)確。
代碼有價值,并注重質(zhì)量。
所以,要時刻考慮先為人來編寫代碼,然后再滿足機(jī)器的需要。
如何改進(jìn)可讀性?
首先,你需要閱讀學(xué)習(xí)其他人編寫的代碼,來了解什么是好的代碼,什么是不好的代碼。也就是那些你感覺非常容易理解的代碼,和感覺看起來超級復(fù)雜的代碼。然后,進(jìn)行實踐。最后花費(fèi)一些時間、經(jīng)驗和實踐來改進(jìn)你的代碼的可讀性。一般來講僅通過培訓(xùn)這種方式,在任何軟件公司中推動編碼規(guī)范都有些困難。而諸如結(jié)對代碼評審,自動化代碼評審工具等也可以幫助你。目前流行的工具有:
FxCop:對 .NET 代碼進(jìn)行靜態(tài)代碼分析,提供了多種規(guī)則來進(jìn)行不同形式的分析。
StyleCop:開源項目,其使用代碼風(fēng)格和一致性規(guī)范來對分析C#代碼�?稍� Visual Studio 中運(yùn)行,也可以集成到 MSBuild 中。StyleCop 也已經(jīng)被集成到了一些第三方開發(fā)工具中。
JetBrains ReSharper:非常著名的提升生產(chǎn)力的工具,可以使 Microsoft Visual Studio IDE 更加強(qiáng)大。全世界的 .NET 開發(fā)人員可能都無法想象,工作中怎么能沒有 ReSharper 的代碼審查、代碼自動重構(gòu)、快速導(dǎo)航和編碼助手等這些強(qiáng)大的功能呢。
規(guī)范是什么?
依據(jù)維基百科上的描述:“Coding conventions are a set of guidelines for a specific programming language that recommend programming style, practices and methods for each aspect of a piece program written in this language. These conventions usually cover file organization, indentation, comments, declarations, statements, white space, naming conventions, programming practices, programming principles, programming rules of thumb, architectural best practices, etc. These are guidelines for software structural quality. Software programmers are highly recommended to follow these guidelines to help improve the readability of their source code and make software maintenance easier. Coding conventions are only applicable to the human maintainers and peer reviewers of a software project. Conventions may be formalized in a documented set of rules that an entire team or company follows, or may be as informal as the habitual coding practices of an individual. Coding conventions are not enforced by compilers. As a result, not following some or all of the rules has no impact on the executable programs created from the source code.”。
你應(yīng)該能說出屬性、局部變量、方法名、類名等的不同,因為它們使用不同的大小寫約定,所以這些約定非常有價值。通過互聯(lián)網(wǎng),你已經(jīng)了解了很多相應(yīng)的準(zhǔn)則和規(guī)范,你所需要的僅是找到一種規(guī)范或者建立你自己的規(guī)范,然后始終遵循該規(guī)范。
下面使用到的源代碼(類庫設(shè)計準(zhǔn)則)是由微軟的 Special Interest Group 團(tuán)隊開發(fā)的,我只是做了些擴(kuò)展。
大小寫約定
下面是一些關(guān)于C#編碼標(biāo)準(zhǔn)、命名約定和最佳實踐的示例,可以根據(jù)你自己的需要來使用。
Pascal Casing
標(biāo)示符中的首字母,后續(xù)串聯(lián)的每個單詞的首字母均為大寫。如果需要,標(biāo)示符的前幾個字母均可大寫。
Camel Casing
標(biāo)示符的首字母為小寫,后續(xù)串聯(lián)的每個單詞的首字母為大寫。
參考:標(biāo)示符大小寫規(guī)則
一些命名約定示例
在互聯(lián)網(wǎng)上你可以找到足夠多的資源,我只是推薦幾個其中我最喜歡的:
C# 編碼約定
C# 編碼準(zhǔn)則
C# 編碼標(biāo)準(zhǔn)和最佳實踐
C# 編碼規(guī)范和命名約定
這里我展示了一些最基本的示例,但就像我上面已經(jīng)提到的,找到一個適合你的規(guī)范,然后堅持使用。
要使用 Pascal Casing 為類和方法命名。
public class Product { public void GetActiveProducts() { //... } public void CalculateProductAdditinalCost() { //... } }
要使用 Camel Casing 為方法的參數(shù)和局部變量命名。
public class ProductCategory { public void Save(ProductCategory productCategory) { // ... } }
不要使用縮寫語。
// Correct ProductCategory productCategory; // Avoid ProductCategory prodCat;
不要在標(biāo)示符中使用下劃線。
// Correct ProductCategory productCategory; // Avoid ProductCategory product_Category;
要在接口名稱前使用字母 I 。
public interface IAddress { }
要在類的頂端定義所有成員變量,在最頂端定義靜態(tài)變量。
public class Product { public static string BrandName; public string Name { get; set; } public DateTime DateAvailable { get; set; } public Product() { // ... } }
要使用單數(shù)的詞匯定義枚舉,除非是BitField枚舉。
public enum Direction { North, East, South, West }
不要為枚舉名稱添加Enum后綴。
//Avoid public enum DirectionEnum { North, East, South, West }
為什么我們需要編碼規(guī)范?
在大型項目中,開發(fā)人員會常依賴于編碼規(guī)范。他們建立了很多規(guī)范和準(zhǔn)則,以至于記住這些規(guī)范和準(zhǔn)則已經(jīng)變成了日常工作的一部分。計算機(jī)并不關(guān)心你寫的代碼可讀性是否好,比起讀懂那些高級的程序語言語句,計算機(jī)更容易理解二進(jìn)制的機(jī)器指令。
編碼規(guī)范提供了很多明顯的好處,當(dāng)然有可能你得到的更多。通常這些項目整體范圍的規(guī)劃,將使能夠?qū)⒕Ω嗟募性诖a中更重要的部分上。
編碼規(guī)范可以幫助跨項目的傳遞知識。
編碼規(guī)范可以幫助你在新的項目上更快速的理解代碼。
編碼規(guī)范強(qiáng)調(diào)組織中關(guān)聯(lián)項目間的關(guān)系。
你需要編寫可讀性高的代碼,以此來幫助其他人來理解你的代碼。代碼命名對我們軟件開發(fā)人員來說是件非常困難的事情,我們在這上面已經(jīng)花費(fèi)了大量的時間,并且有太多的需要命名的元素,例如屬性、方法、類、文件、項目等。所以我們確實需要花費(fèi)一些精力在命名規(guī)范上,以使名稱更有意義,進(jìn)而提高代碼的可讀性。
還有,編碼規(guī)范可以讓你晚上睡得更香。
開發(fā)人員最應(yīng)該遵循的幾個規(guī)則
始終控制類的大小
我曾經(jīng)看到過,并且也曾寫過一些超大的類。而且不幸的是,結(jié)果總是不好的。后來我找到了真正原因,就是那些超大的類在嘗試做太多的事情,這違反了單一職責(zé)原則(SRP),也就是面向?qū)ο笤O(shè)計原則SOLID 中的 S。
“The single responsibility principle states that every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.”
或者按照 Martin Fowler 的定義:"THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE."
為什么一定要將兩個職責(zé)分離到單獨(dú)的類中呢?因為每一個職責(zé)都是變化的中心。在需求變更時,這個變更將會出現(xiàn)在負(fù)責(zé)該職責(zé)的類中。如果一個類承擔(dān)了多個職責(zé),就會有一個以上的原因?qū)е缕渥兓�。如果一個類有多重職責(zé),則說明這些職責(zé)已經(jīng)耦合到了一起。并且某個職責(zé)的變化將有可能削弱或限制這個類滿足其他職責(zé)的能力。這種耦合將會導(dǎo)致非常脆弱的設(shè)計,進(jìn)而在職責(zé)發(fā)生變化時,設(shè)計可能被意想不到的破壞了。
避免過時的注釋
先說什么過時的注釋。按照 Robert C. Martin 的定義:
"A comment that has gotten old, irrelevant, and incorrect is obsolete. Comments get old quickly. It is best not to write a comment that will become obsolete. If you find an obsolete comment, it is best to update it or get rid of it as quickly as possible. Obsolete comments tend to migrate away from the code they once described. They become floating islands of irrelevance and misdirection in the code."
針對這個主題,不同水平的開發(fā)人員可能都會有自己的見解。我的建議是嘗試避免為單獨(dú)的方法或短小的類進(jìn)行注釋。因為我所見過的大部分的注釋都是在嘗試描述代碼的目的或意圖,或者某些注釋可能本身就沒什么意義。通常開發(fā)人員通過寫注釋來提高代碼的可讀性和可維護(hù)性,但要保證你所寫的注釋不會成為代碼中的噪音。比起注釋,我認(rèn)為合理的方法命名將更為有效,比如你可以為一個方法起一個更有意義的名字。大部分注釋都可能變成了無意義的代碼噪音,讓我們來看看下面這些注釋:
//ensure that we are not exporting //deleted products if (product.IsDeleted && !product.IsExported) { ExportProducts = false; } // This is a for loop that prints the 1 million times for (int i = 0; i < 1000000; i++) { Console.WriteLine(i); }
如果我們不寫注釋,而是命名一個方法,比如叫 CancelExportForDeletedProducts() ,情況會怎樣?所以,合適的方法命名比注釋更有效。然而某些情況下,代碼注釋也會非常有幫助,比如 Visual Studio 會從注釋生成 API 文檔。此處的注釋略有不同,你需要使用 “///” 標(biāo)識符來注釋,這樣其他開發(fā)人員才能看到 API 或類庫的智能提示。
我沒有說總是要避免注釋。按照 Kent Beck 說法,可以使用更多的注釋來描述程序整體是如何工作的,而不是對單獨(dú)的方法進(jìn)行注釋。如果注釋是在嘗試描述代碼的目的或意圖,那就錯了。如果你在代碼中看到了密密麻麻的的注釋,你可能就會意識到有這么多注釋說明代碼寫的很糟糕。了解更多信息可以閱讀下面這幾本書:
《Professional Refactoring in C# and ASP.NET》 by Danijel Arsenovski
《重構(gòu):改善既有代碼設(shè)計》 by Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts
避免不必要的Region
Region 是 Visual Studio 提供的一個功能,它允許你將代碼分塊。Region 的存在是因為它可以使大文件導(dǎo)航變得容易。Region 還常被用于隱藏丑陋的代碼,或者類已經(jīng)膨脹的非常大了需要分塊。而如果一個類做了太多的事情,也就說明其違反了單一職責(zé)原則。所以,下次當(dāng)你想新增一個 Region 時,先考慮下有沒有可能將這個 Region 分離到一個單獨(dú)的類中。
保持方法的短小
方法中的代碼行數(shù)越多,則方法越難理解。我們推薦每個方法中只包含 20-25 行代碼。但有些人說 1-10 行更合理,這只是些個人喜好,沒有硬性的規(guī)則。抽取方法是最常見的重構(gòu)方式之一。如果你發(fā)現(xiàn)一個方法過長,或者已經(jīng)需要一個注釋來描述它的目的了,那么你就可以應(yīng)用抽取方法了。人們總是會問一個方法到底多長合適,但其實長度并不是問題的根源。當(dāng)你在處理復(fù)雜的方法時,跟蹤所有局部變量是最復(fù)雜和消耗時間的,而通過抽取一個方法可以節(jié)省一些時間�?梢允褂� Visual Studio 來抽取方法,它會幫助你跟蹤局部變量,并將其傳遞給新的方法或者接收方法的返回值。
Using ReSharper
Using Microsoft Visual Studio
更多的信息可以參考 MSDN。
按照《重構(gòu):改善既有代碼設(shè)計》中的描述,
"Extract Method is one of the most common refactoring I do. I look at a method that is too long or look at code that needs a comment to understand its purpose. I then turn that fragment of code into its own method. I prefer short, well-named methods for several reasons. First, it increases the chances that other methods can use a method when the method is finely grained. Second, it allows the higher-level methods to read more like a series of comments. Overriding also is easier when the methods are finely grained. It does take a little getting used to if you are used to seeing larger methods. And small methods really work only when you have good names, so you need to pay attention to naming. People sometimes ask me what length I look for in a method. To me length is not the issue. The key is the semantic distance between the method name and the method body. If extracting improves clarity, do it, even if the name is longer than the code you have extracted."
避免過多的參數(shù)
通過聲明一個類來代替多個參數(shù)。創(chuàng)建一個類,用于包含所有的參數(shù)。通常來講,這是一個較好的設(shè)計,并且這個抽象非常的有價值。
//avoid public void Checkout(string shippingName, string shippingCity, string shippingSate, string shippingZip, string billingName, string billingCity, string billingSate, string billingZip) { } //DO public void Checkout(ShippingAddress shippingAddress, BillingAddress billingAddress) { }
我們需要引入類來代替所有的參數(shù)。
避免復(fù)雜的表達(dá)式
if(product.Price>500 && !product.IsDeleted && !product.IsFeatured && product.IsExported) { // do something }
復(fù)雜的表達(dá)式意味著其背后隱藏了一些涵義,我們可以通過使用屬性來封裝這些表達(dá)式,進(jìn)而使代碼更易讀些。
把警告等同于錯誤
如果你注意看代碼,你會發(fā)現(xiàn)一個變量被聲明了但從沒被使用過。正常來講,我們編譯工程后會得到一個警告,但仍可以運(yùn)行工程而不會發(fā)生任何錯誤。但是我們應(yīng)該盡可能地移除這些警告。通過如下步驟可以在工程上設(shè)置將警告等同于錯誤:
精簡多處返回
在每段程序中都減少函數(shù)返回的數(shù)量。假設(shè)從底部開始閱讀代碼,你很難意識到有可能在上面的某處已經(jīng)返回了,這樣的代碼將是非常難理解的。
僅使用一處返回可以增強(qiáng)可讀性。如果程序這么寫的話可能看起來比較干凈,但不立即返回也意味著需要編寫更多代碼。
//avoid if(product.Price>15) { return false; } else if(product.IsDeleted) { return false; } else if(!product.IsFeatured) { return false; } else if() { //..... } return true;
//DO var isValid = true; if(product.Price>15) { isValid= false; } else if(product.IsDeleted) { isValid= false; } else if(!product.IsFeatured) { isValid= false; } return isValid;
你可以想象在這 20-30 行代碼中就散落了 4 個退出點(diǎn),這會使你非常難理解到底程序內(nèi)部做了什么,到底會執(zhí)行什么,什么時候執(zhí)行。
關(guān)于這一點(diǎn)我得到了很多人的回復(fù),一些人同意這個觀點(diǎn),有些則不同意這是一個好的編碼標(biāo)準(zhǔn)。為了找出潛在的問題,我做了些單元測試,發(fā)現(xiàn)如果復(fù)雜的方法包含多個退出點(diǎn),通常情況下會需要一組測試來覆蓋所有的路徑。
if( BADFunction() == true) { // expression if( anotherFunction() == true ) { // expression return true; } else { //error } } else { //error } return false;
if( !GoodFunction()) { // error. return false } // expression if( !GoodFunction2()) { //error. return false; } // more expression return true;
進(jìn)一步理解可以參考 Steve McConnell 的《代碼大全》。
使用斷言
在軟件開發(fā)中,斷言代碼常被用于檢查程序代碼是否按照其設(shè)計在執(zhí)行。通常 True 代表所有操作按照預(yù)期的完成,F(xiàn)alse 代表已經(jīng)偵測到了一些意外的錯誤。斷言通常會接收兩個參數(shù),一個布爾型的表達(dá)式用于一個描述假設(shè)為真的假定,一個消息參數(shù)用于描述斷言失敗的原因。
尤其在開發(fā)大型的、復(fù)雜的高可靠系統(tǒng)中,斷言通常是非常有用的功能。
例如:如果系統(tǒng)假設(shè)將最多支持 100,000 用戶記錄,系統(tǒng)中可能會包含一個斷言來檢查用戶記錄數(shù)小于等于 100,000,在這種范圍下,斷言不會起作用。但如果用戶記錄數(shù)量超過了 100,000,則斷言將會拋出一個錯誤來告訴你記錄數(shù)已經(jīng)超出了范圍。
檢查循環(huán)端點(diǎn)值
一個循環(huán)通常會涉及三種條件值:第一個值、中間的某值和最后一個值。但如果你有任何其他的特定條件,也需要進(jìn)行檢測。如果循環(huán)中包含了復(fù)雜的計算,請不要使用計算器,要手工檢查計算結(jié)果。
總結(jié)
通常在任何軟件公司中推行編碼規(guī)范都需要按照組織行為、項目屬性和領(lǐng)域來進(jìn)行,在此我想再次強(qiáng)調(diào)“找到一個適合你的編碼規(guī)范,并一直遵循它”。
如果你認(rèn)為我遺漏了某個特別有用的編碼準(zhǔn)則,請在評論中描述,我會嘗試補(bǔ)充到文章中。