標籤:
一:前言
沒有規矩,不成方圓。在代碼的世界中,尤其這樣。作為程式員,我們不想讓我們的代碼寫出去之後被人恥笑:看,連個換行都換的這麼不專業。作為開發主管,我們則不想我們的組員寫出來的代碼各類風格都有,五顏六色的,極其醜陋。寫出規範的代碼,首先需要訓練,其次,也有一定的手段或者工具來進行輔助。本小節,我們就要從這兩方面入手,講講如何規範我們的代碼。當然,由於我們現在學到的編碼知識還有限,最為規範來講,本小節也將僅僅會設計那些最基本,最常用的編碼的規範,但是即便如此,學完本小節之後,也會讓我們的代碼看上去專業多了。
注意:我不喜歡一次性將全部的知識點講完,比如,規範,我們今天可能只會涉及到80%的部分。我喜歡這個“二八原則”,即,我們只花20%的時間來完成80%的事情,但是,如果我們想完成剩下的20%的事情,反過來就要額外付出80%的時間,這就有點性價比不那麼高了。對於培訓或者說學習知識來說,這個“二八原則”很重要。我們的培訓,如果要講解100%的知識點,首先會很枯燥(因為有些知識需要我們具備鑽牛角尖的精神才能悟透),其次會很費時間(難道我們不想花最少的時間學習最多的知識嗎?),最後,成為真正的專家,從來不是被培訓出來的,所以我們的培訓,會教會你這80%的部分,剩下的,則希望這80%中已經培養給你的習慣,自己去挖掘。OK,今天廢話有點多,言歸正傳,雖然隨著我們的課程我們還沒寫了多少行的代碼,但是即便如此,我相信你也一定覺得現在到了該規範代碼的時候了。我們到目前為止,也進行了幾次的重構,重構的過程,實際就是講代碼一步步引導到更規範的過程。當然,有些規範,可能是學習完本小節課程我們就會掌握的,而更深入的規範,就需要我們在今後的學習中慢慢掌握了,而且有意思的一點是:規範本身可能還存在衝突性。。。好了,不管怎麼樣,箇中滋味,以後我們慢慢體會吧,現在,GO……
二:命名規範
1: 考慮在命名空間中使用複數
如果有一組功能相近的類型被分組到了同一個命名空間下,則可以考慮為命名空間使用複數。
最典型的例子有,在FCL中,我們需要把所有的非泛型集合類集中在一起存放,所以我們就有了System.Collections命名空間。這樣的命名規範,好處就是即便我們從來沒有使用過集合類,但是看到這樣的命名空間,我們也會知道在它之下是和集合(即Collection)相關的一些類型。不要出現System.AllCollections、System.TheCollection這樣的命名,這看上去要麼太繁瑣、要麼含義不清。
舉一個實際的例子,如果我們的項目中存在一系列Processor類型,則可以使用命名空間Processors。
2: 用名詞和名片語給類型命名
類型是什嗎?物件導向方面的先驅者會告訴我們,類型對應著現實世界中的實際對象。對象在語言學中意味它是一個名詞。所以,類型也應該以名詞或名片語去命名。
類型定義了屬性和行為。它包含行為,但不是行為本身。所以,下面的一些命名對於類型來說是好的命名:
OrderProcessor;
ScoreManager ;
CourseRepository;
UserControl;
DomainService;
相應的,如下的類型名稱則被認為是不好的典範:
OrderProcess
ScoreManage
CourseSave
ControlInit
DomainProvide
動詞類的命名更像是類型內的一個行為,而不是類型本身。
3: 用形容片語給介面命名
介面規範的是“Can do”,也就是說它規範的是類型可以具有哪些行為。所以,介面的命名應該是一個形容片語,如:
IDisposable,表示類型可以被釋放;
IEnumerable,表示類型含有Items,可以被迭代。
正是因為介面表示的是類型的行為,所以從語義上我們可以讓類型繼承多個介面,如:
class SampleClass : IDisposable, IEnumerable<SampleClass>
{
//省略
#region 實現IDisposable
public void Dispose()
{
throw new NotImplementedException();
}
#endregion
#region 實現IEnumerable
public IEnumerator<SampleClass> GetEnumerator()
{
throw new NotImplementedException();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
#endregion
}
以上的代碼我們寫起來會覺得既符合文法,又符合語義。如果,我們將介面命名為IDisposal,這給人造成的誤解是該類型是一個類,而不是介面,雖然我們在前面加了首碼I,但仍然感覺這是符合語義的。
不過話又說來,FCL中也有一些違反此規定的例外,比如IEnumerator介面。但是,這種情況相對來說還是比較少的,在大多數情況下,我們需要始終考慮用形容詞來為介面命名。
4: 以複數命名枚舉類型,以單數命名枚舉元素
枚舉類型應該具有複數形式,它表達的是將一組相關元素組合起來的語義。比如:
enum Week
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
在這裡,Week對於星期幾來說,具備複數含義。如果我們這裡將Week修改為Day,那麼在調用的代碼會變成如下:
Day.Monday
它不會比下面的代碼來的簡潔明了:
Week.Moday
5: 用PascalCasing命名公開元素
開放給調用者的屬性、欄位和方法,都應該採用PascalCasing命名方式,比如:
class Person
{
public string FirstName;
public string LastName;
public string Name
{
get
{
return string.Format("{0} {1}", FirstName, LastName);
}
}
public string GetName()
{
return Name;
}
}
這樣,調用者在調用方的代碼看起來如下:
person.Name
如果我們不注意這樣的命名規則,讓調用方的代碼看起來是這樣的:
person.name
我們首先會懷疑name是個什麼類型,其次也會懷疑其可訪問性。
6: 用camelCasing命名私人欄位和局部變數
私人欄位和局部變數只對本類型負責,它們在命名方式也採用和開放的屬性及欄位不同的方法。camelCasing很適合這類命名。
camelCasing和PascalCasing的區別是它的首字母是小寫。之所以要採用兩種不同的命名規則,是為了便於開發人員自己快速地區分它們。
在建議123中我們為公開元素給出了一個命名樣本,下面的樣本,則是私人欄位和局部變數的一個樣本:
class Person
{
private string firstName;
private string lastName;
public string Name
{
get
{
return string.Format("{0} {1}", firstName, lastName);
}
}
private int doSomething(int a, int b)
{
int iTemp = 10;
return a + b + iTemp;
}
}
在這裡例子中,我們可以看到,所有的私人欄位,包括方法的參數及局部變數全部遵循首字母小寫camelCasing規則。一旦我們脫離了這種規則,那麼在編碼過程中很容易就給自己造成混淆。firstName是什麼,難道它不是個私人欄位,而是個公開屬性嗎?這太混亂了,也太可怕了,因為作為開發人員的我們不得不回到變數的命名處才知道它的存取範圍。
7: 考慮使用肯定性的短語命名布爾屬性
布爾值無非就是True和False,所以,應該用肯定性的短語來表示它,如:以Is、Can、Has作為首碼。
布爾屬性正確命名的一個樣本如下:
class SampleClass
{
public bool IsEnabled { get; set; }
public bool IsTabStop { get; set; }
public bool AllowDrop { get; set; }
public bool IsActive { get; }
public bool? IsChecked { get; set; }
}
以上的這些命名都來自於.NET最新的WPF子集,其中AllowDrop雖然不是以肯定性短語作為首碼,但是其作為動作表達了一個是與否的含義,所以也是一個推薦的布爾型屬性的推薦命名。
布爾型屬性命名的反面教材如下:
class SampleClass
{
public bool Checked { get; set; }
public bool Loaded { get; set; }
}
肯定性形容詞或者短語雖然表達了一個肯定的含義,但是這些單詞或者短語現在都被用於命名事件或委託變數,所以不應該用於布爾屬性。
三:代碼整潔
1: 總是提供有意義的命名
除非惡意為之,否則永遠不要為自己的代碼提供無意義的命名。
害怕需要過長的命名才能提供足夠的意義?不要怕,其實我們更介意的是在讀代碼的時候出現一個iTemp。
int i這樣的命名方式只應該出現在迴圈中(如for迴圈),除此之外,我們找不到任何理由在代碼的其他地方出現這樣的無意義命名。
2: 方法抽象層級應在同一層次
方法的抽象層級應在同一個層次上,我們來看下面的代碼:
class SampleClass
{
public void Init()
{
//本地初始化代碼1
//本地初始化代碼2
RemoteInit();
}
void RemoteInit()
{
//遠程初始化代碼1
//遠程初始化代碼2
}
}
Init方法本意要完成初始化動作,而初始化包括本地初始化和遠程初始化。在這段代碼中,Init方法內部代碼的組織圖是本地初始化代碼直接運行在方法內部,而遠程初始化代碼卻被封裝為一個方法在這裡被調用。這顯然是不妥當的,因為本地初始化和遠程初始化的地方是相當的。作為方法來講,如果遠程初始化代碼作為方法存在,則本地初始化代碼也應該作為方法存在。
所以,上面的代碼應該重構為:
class SampleClass
{
public void Init()
{
LocalInit();
RemoteInit();
}
void LocalInit()
{
//本地初始化代碼1
//本地初始化代碼2
}
void RemoteInit()
{
//遠程初始化代碼1
//遠程初始化代碼2
}
}
重構後的代碼看上去清晰明了,所有的方法的抽象層級都在一個層次上,作為閱讀者的我們一眼看上去就知道Init方法完成了什麼樣的功能。
3: 一個方法只做一件事
“單一職責原則(SRP)”要求每個類型只負責一件事情。我們將此概念擴充到方法上,就變成了:一個方法只做一件事。
什麼樣的代碼才叫“做同一件事”?參照上一個建議中的代碼,其中,LocalInit方法和RemoteInit方法是兩件事情,但是在同一抽象層次上,在類型這個層次對外又可以將其歸併為“初始化”這一件事情上。所以,“同一件事”要看抽象所處的地位。
下面的方法就完成了太多事情,我們來看這段實際的代碼:
private uint status;
private uint DeveloperID;
private uint flags;
public string CheckDogAndGetKey()
{
flags = SentinelKey.SP_STANDALONE_MODE;
status = oSentinelKey.SFNTGetLicense(DeveloperID, oSentinelKeysLicense.SOFTWARE_KEY, SentinelKeysLicense.LICENSEID, flags);
if (status != SentinelKey.SP_SUCCESS)
{
throw new FileNotFoundException("未檢查到合法的加密狗,或者未正確安裝驅動");
}
status = oSentinelKey.SFNTReadString(SentinelKeysLicense.SP_1ST_STRING, readStringValue, MAX_STRING_LENGTH);
if (status != SentinelKey.SP_SUCCESS)
{
throw new FileNotFoundException("讀取加密狗資料失敗");
}
return System.Text.Encoding.ASCII.GetString(readStringValue).Substring(0, 7);
}
在方法CheckDogAndGetKey中,方法既要負責檢測加密狗是否被正確安裝,又要負責從加密狗中讀取相關的資訊。顯然,這讓CheckDogAndGetKey來說,責任太多。我們可以考慮將方法重構為如下兩個方法:
void CheckDog()
{
flags = SentinelKey.SP_STANDALONE_MODE;
status = oSentinelKey.SFNTGetLicense(DeveloperID, oSentinelKeysLicense.SOFTWARE_KEY, SentinelKeysLicense.LICENSEID, flags);
if (status != SentinelKey.SP_SUCCESS)
{
throw new FileNotFoundException("未檢查到合法的加密狗,或者未正確安裝驅動");
}
}
string GetKeyFormDog()
{
status = oSentinelKey.SFNTReadString(SentinelKeysLicense.SP_1ST_STRING, readStringValue, MAX_STRING_LENGTH);
if (status != SentinelKey.SP_SUCCESS)
{
throw new FileNotFoundException("讀取加密狗資料失敗");
}
return System.Text.Encoding.ASCII.GetString(readStringValue).Substring(0, 7);
}
經過重構,每個方法都只要負責一件事情。並且,從命名來看,CheckDog負責檢測加密狗,而GetKeyFormDog則負責擷取資訊。
4: 避免過長的方法和過長的類
若不遵循“一個方法只做一件事”及類型的“單一職責原則”,則往往會產生過長的方法和過長的類。
如果方法過長,則意味著可以站在更高的層次上重構出若干個更小的方法。那麼,有沒有具體的指標提示方法是否過長?有,是以行數做指標的,有人建議一個方法不要超過10行,有人建議不要超過30行。當然,這沒有唯一標準,在我看來,如果一個方法在Visual Studio中需要滾屏才能閱讀完,那麼就肯定有些過長了,必須想法重構它。
對於類型,除非有非常特殊的理由,類型的代碼不要超過300行。如果行數太多了,則要考慮能否重構。
5: 只對外公布必要的操作
那些不是很必要公開的方法和屬性,private之。如果需要公開的方法和屬性超過9個,在Visual Studio預設的設定下,就需要滾屏才能顯示在Intellisense中了,查看圖:
在中我們可以看到,Intellisense在可見範圍內為我們提示的方法還包括了從Object繼承過來的3個方法,實際真正在這個例子中能為我們顯示的有價值的資訊只有6條。Sample類型的全部代碼如下:
class SampleClass
{
int field1;
int field2;
int field3;
public int MyProperty1 { get; set; }
public int MyProperty2 { get; set; }
public int MyProperty3 { get; set; }
public int MyProperty4 { get; set; }
public int MyProperty5 { get; set; }
public int MyProperty6 { get; set; }
public void Mehtod1()
{
}
public void Mehtod2()
{
}
public void Mehtod3()
{
}
}
如果我們為SampleClass增加更多的公開屬性或方法,則意味著我們在使用Intellisense的時候增加了尋找成本。
若我們打算將某個方法public或internal,請仔細考慮這種必要性。記住,Visual Studio預設給我們產生的類型成員的存取修飾詞就是private的,在我看來,這是微軟在給我們心理暗示:除非必須,否則關閉訪問。
除了類成員外,類型也一樣,應將不該對其他項目公開的類型設定為internal。想想類型預設的訪問限制符是internal,這意味著類型如果我們沒有有意為之,類型就應該只對本項目開放。所以,遵守這個規則,這會使我們的API看上去清爽很多。
四:代碼規範靜態檢查工具
除了我們自己要習慣性掌握的規範外,這個世界上當然還有一些靜態檢查工具來幫我們分析我們的代碼是否符合一定的規範。目前來說,我們習慣性的做法就是使用StyleCop來幫我們完成代碼規範的靜態檢查。
StyleCop是什嗎?
StyleCop早年是微軟自己內部的靜態代碼和強制格式美化工具。雖然流出來的微軟的一些開源項目,跑跑StyleCop,我們仍舊會發現出現很多警告(當然,我們也可以理解為MS各個項目本身定義了自己都有的一些規範)。其官方地址為:
http://archive.msdn.microsoft.com/sourceanalysis
在本小節的此時此刻,目前的版本為:StyleCop-4.7.44.0,下面我們來看看如何使用StyleCop進行代碼的規範檢查。
備忘:視頻中使用到的規範設定檔案為:http://back.zuikc.com/Settings.zip
五:視頻
非公開部分,請聯絡最課程(www.zuikc.com)
C#零基礎入門08:代碼規範