一:前言
沒有規矩,不成方圓。在代碼的世界中,尤其這樣。作為程序員,我們不想讓我們的代碼寫出去之后被人恥笑:看,連個換行都換的這么不專業。作為開發主管,我們則不想我們的組員寫出來的代碼各類風格都有,五顏六色的,極其丑陋。寫出規范的代碼,首先需要訓練,其次,也有一定的手段或者工具來進行輔助。本小節,我們就要從這兩方面入手,講講如何規范我們的代碼。當然,由于我們現在學到的編碼知識還有限,最為規范來講,本小節也將僅僅會設計那些最基本,最常用的編碼的規范,但是即便如此,學完本小節之后,也會讓我們的代碼看上去專業多了。
注意:我不喜歡一次性將全部的知識點講完,比如,規范,我們今天可能只會涉及到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 實現IDisposablepublic 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)
新聞熱點
疑難解答