一灯谣、類應(yīng)該短小
類和函數(shù)一樣應(yīng)該短小。對(duì)于函數(shù)蛔琅,我們通過(guò)計(jì)算代碼行數(shù)衡量大小胎许。對(duì)于類,我們采用不同的衡量方法,計(jì)算 權(quán)責(zé)
辜窑。
類的名稱應(yīng)當(dāng)描述其權(quán)責(zé)钩述。實(shí)際上,命名正是幫助判斷類的長(zhǎng)度的第一個(gè)手段穆碎。如果無(wú)法為某個(gè)類命以精確的名稱牙勘,這個(gè)類大概就太長(zhǎng)了。類名越含混所禀,該類越有可能擁有過(guò)多權(quán)責(zé)方面。例如,如果類名中包括含義模糊的詞色徘,如 Processor 或 Manager 或 Super葡幸,這種現(xiàn)象往往說(shuō)明有不恰當(dāng)?shù)臋?quán)責(zé)聚集情況存在。
1.1 單一權(quán)責(zé)原則
單一權(quán)責(zé)原則(SRP)認(rèn)為贺氓,類或模塊應(yīng)有且只有 一條加以修改的理由
蔚叨。該原則既給出了權(quán)責(zé)的定義,又是關(guān)于類的長(zhǎng)度的指導(dǎo)方針辙培。類只應(yīng)有一個(gè)權(quán)責(zé)——只有一條修改的理由蔑水。
public class SuperDashboard extends JFrame implements MetaDataUser
{
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}
上述 SuperDashboard 類有兩條加以修改的理由。首先扬蕊,它跟蹤大概會(huì)隨軟件每次發(fā)布而更新的版本信息搀别。第二,它管理 Java Swing 組件(派生自 JFrame尾抑,頂層 GUI 窗口的 Swing 表現(xiàn)形態(tài))歇父。每次修改 Swing 代碼時(shí),無(wú)疑都要更新版本號(hào)再愈,但反之未必可行:也可能依據(jù)系統(tǒng)中其他代碼的修改而更新版本信息榜苫。
鑒別權(quán)責(zé)(修改的理由)常常幫助我們?cè)诖a中認(rèn)識(shí)到并創(chuàng)建出更好的抽象◆岢澹可以輕易地將全部三個(gè)處理版本信息的 SuperDashboard 方法拆解到名為 Version 的類中垂睬。Version 類是個(gè)極有可能在其他應(yīng)用程序中得到復(fù)用的構(gòu)造!
public class Version
{
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber()
}
再?gòu)?qiáng)調(diào)一下:系統(tǒng)應(yīng)該由許多短小的類而不是少量巨大的類組成抗悍。每個(gè)小類封裝一個(gè)權(quán)責(zé)驹饺,只有一個(gè)修改的原因,并與少數(shù)其他類一起協(xié)同達(dá)成期望的系統(tǒng)行為缴渊。
1.2 內(nèi)聚
類應(yīng)該只有少量實(shí)體變量赏壹。類中的每個(gè)方法都應(yīng)該操作一個(gè)或多個(gè)這種變量。通常而言衔沼,方法操作的變量越多蝌借,就越黏聚到類上田柔。如果一個(gè)類中的每個(gè)變量都被每個(gè)方法所使用,則該類具有最大的內(nèi)聚性骨望。
一般來(lái)說(shuō)硬爆,創(chuàng)建這種極大化內(nèi)聚類是既不可取也不可能的;另一方面擎鸠,我們希望內(nèi)聚性保持在較高位置缀磕。內(nèi)聚性高,意味著類中的方法和變量相互依賴劣光、互相結(jié)合成一個(gè)邏輯整體袜蚕。
如下 Stack 類的實(shí)現(xiàn)方法。這個(gè)類非常內(nèi)聚绢涡。在三個(gè)方法中牲剃,只有 size() 方法沒(méi)有使用所有兩個(gè)變量。
public class Stack
{
private int topOfStack = 0;
List<Integer> elements = nes LinkedList<Integer>();
public int size()
{
return topOfStack;
}
public void push(int element)
{
topOfStack++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty
{
if(topOfStack == 0)
{
throw new PoppedWhenEmpty();
}
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}
保持函數(shù)和參數(shù)列表短小的策略雄可,有時(shí)會(huì)導(dǎo)致為一組子集方法所用的實(shí)體變量數(shù)量增加凿傅。出現(xiàn)這種情況時(shí),往往意味著至少有一個(gè)類要從大類中掙扎出來(lái)数苫。你應(yīng)當(dāng)嘗試將這些變量和方法分拆到兩個(gè)或多個(gè)類中聪舒,讓新的類更為內(nèi)聚。
1.3 保持內(nèi)聚性就會(huì)得到許多短小的類
僅僅是將較大的函數(shù)切割為小函數(shù)虐急,就將導(dǎo)致更多的類出現(xiàn)箱残。想想看一個(gè)有許多變量的大函數(shù)。你想把該函數(shù)中某一小部分拆解成單獨(dú)的函數(shù)止吁。不過(guò)被辑,你想要拆出來(lái)的代碼使用了該函數(shù)中聲明的4個(gè)變量。是否必須將這4個(gè)變量都作為參數(shù)傳遞到新函數(shù)中去呢敬惦?
完全沒(méi)必要盼理!只要將4個(gè)變量提升為類的實(shí)體變量,完全無(wú)需傳遞任何變量就能拆解代碼了仁热。應(yīng)該很容易將函數(shù)拆分為小塊榜揖。
可惜這也意味著類喪失了內(nèi)聚性勾哩,因?yàn)槎逊e了越來(lái)越多只為允許少量函數(shù)共享而存在的實(shí)體變量抗蠢。如果有些函數(shù)想要共享某些變量,為什么不讓它們擁有自己的類呢思劳?當(dāng)類喪失了內(nèi)聚性迅矛,就拆分它!
所以潜叛,將大函數(shù)拆為許多小函數(shù)秽褒,往往也是將類拆分為多個(gè)小類的時(shí)機(jī)壶硅。程序會(huì)更加有組織,也會(huì)擁有更為透明的結(jié)構(gòu)销斟。
二庐椒、構(gòu)造函數(shù)
2.1 總述
不要在構(gòu)造函數(shù)中調(diào)用虛函數(shù),也不要在無(wú)法報(bào)出錯(cuò)誤時(shí)進(jìn)行可能失敗的初始化蚂踊。
2.2 定義
在構(gòu)造函數(shù)中可以進(jìn)行各種初始化操作约谈。
2.3 優(yōu)點(diǎn)
- 無(wú)需考慮類是否被初始化。
- 經(jīng)過(guò)構(gòu)造函數(shù)完全初始化后的對(duì)象可以為
const
類型犁钟,也能更方便地被標(biāo)準(zhǔn)容器或算法使用棱诱。
2.4 缺點(diǎn)
- 如果在構(gòu)造函數(shù)內(nèi)調(diào)用了自身的虛函數(shù),這類調(diào)用是不會(huì)重定向到子類的虛函數(shù)實(shí)現(xiàn)涝动。即使當(dāng)前沒(méi)有子類化實(shí)現(xiàn)迈勋,將來(lái)仍是隱患。
- 在沒(méi)有使程序崩潰 (因?yàn)椴⒉皇且粋€(gè)始終合適的方法) 或者使用異常 (因?yàn)橐呀?jīng)被 禁用 了) 等方法的條件下醋粟,構(gòu)造函數(shù)很難上報(bào)錯(cuò)誤靡菇。
- 如果執(zhí)行失敗,會(huì)得到一個(gè)初始化失敗的對(duì)象米愿,這個(gè)對(duì)象有可能進(jìn)入不正常的狀態(tài)镰官,必須使用
bool IsValid()
或類似這樣的機(jī)制才能檢查出來(lái),然而這是一個(gè)十分容易被疏忽的方法吗货。 - 構(gòu)造函數(shù)的地址是無(wú)法被取得的泳唠,因此,舉例來(lái)說(shuō)宙搬,由構(gòu)造函數(shù)完成的工作是無(wú)法以簡(jiǎn)單的方式交給其他線程的笨腥。
2.5 結(jié)論
- 構(gòu)造函數(shù)不允許調(diào)用虛函數(shù)。如果代碼允許勇垛,直接終止程序是一個(gè)合適的處理錯(cuò)誤的方式脖母。否則,考慮用
Init()
方法或工廠函數(shù)闲孤。 - 構(gòu)造函數(shù)不得調(diào)用虛函數(shù)谆级,或嘗試報(bào)告一個(gè)非致命錯(cuò)誤。如果對(duì)象需要進(jìn)行有意義的 (non-trivial) 初始化讼积,考慮使用明確的 Init() 方法或使用工廠模式肥照。Avoid
Init()
methods on objects with no other states that affect which public methods may be called (此類形式的半構(gòu)造對(duì)象有時(shí)無(wú)法正確工作)。 - 不在構(gòu)造函數(shù)中做太多邏輯相關(guān)的初始化勤众。
三舆绎、結(jié)構(gòu)體和類
僅當(dāng)只有數(shù)據(jù)成員時(shí)使用 struct
,其它一概使用 class
们颜。
在 C++ 中 struct
和 class
關(guān)鍵字幾乎含義一樣吕朵。我們?yōu)檫@兩個(gè)關(guān)鍵字添加我們自己的語(yǔ)義理解猎醇,以便為定義的數(shù)據(jù)類型選擇合適的關(guān)鍵字。
struct
用來(lái)定義包含數(shù)據(jù)的被動(dòng)式對(duì)象努溃,也可以包含相關(guān)的常量硫嘶,但除了存取數(shù)據(jù)成員之外,沒(méi)有別的函數(shù)功能梧税。并且存取功能是通過(guò)直接訪問(wèn)位域音半,而非函數(shù)調(diào)用。除了構(gòu)造函數(shù)贡蓖,析構(gòu)函數(shù)曹鸠,Initialize()
,Reset()
斥铺,Validate()
等類似的用于設(shè)定數(shù)據(jù)成員的函數(shù)外彻桃,不能提供其它功能的函數(shù)。
如果需要更多的函數(shù)功能晾蜘,class
更適合邻眷。如果拿不準(zhǔn),就用 class
剔交。
為了和 STL 保持一致肆饶,對(duì)于仿函數(shù)等特性可以不用 class
而是使用 struct
。
注意:類和結(jié)構(gòu)體的成員變量使用不同的命名規(guī)則岖常。
四驯镊、繼承
4.1 總述
使用組合常常比使用繼承更合理。如果使用繼承的話竭鞍,定義為 public
繼承板惑。
4.2 定義
當(dāng)子類繼承基類時(shí),子類包含了父基類所有數(shù)據(jù)及操作的定義偎快。C++ 實(shí)踐中冯乘,繼承主要用于兩種場(chǎng)合:實(shí)現(xiàn)繼承,子類繼承父類的實(shí)現(xiàn)代碼晒夹;接口繼承裆馒,子類僅繼承父類的方法名稱。
4.3 優(yōu)點(diǎn)
實(shí)現(xiàn)繼承通過(guò)原封不動(dòng)的復(fù)用基類代碼減少了代碼量丐怯。由于繼承是在編譯時(shí)聲明喷好,程序員和編譯器都可以理解相應(yīng)操作并發(fā)現(xiàn)錯(cuò)誤。從編程角度而言响逢,接口繼承是用來(lái)強(qiáng)制類輸出特定的 API绒窑。在類沒(méi)有實(shí)現(xiàn) API 中某個(gè)必須的方法時(shí),編譯器同樣會(huì)發(fā)現(xiàn)并報(bào)告錯(cuò)誤舔亭。
4.4 缺點(diǎn)
對(duì)于實(shí)現(xiàn)繼承些膨,由于子類的實(shí)現(xiàn)代碼散布在父類和子類間之間,要理解其實(shí)現(xiàn)變得更加困難钦铺。子類不能重寫父類的非虛函數(shù)订雾,當(dāng)然也就不能修改其實(shí)現(xiàn)∶矗基類也可能定義了一些數(shù)據(jù)成員洼哎,因此還必須區(qū)分基類的實(shí)際布局。
4.5 結(jié)論
所有繼承必須是 public
的沼本。如果你想使用私有繼承噩峦,你應(yīng)該替換成把基類的實(shí)例作為成員對(duì)象的方式。
不要過(guò)度使用實(shí)現(xiàn)繼承抽兆。組合常常更合適一些识补。盡量做到只在 “是一個(gè)” (“is-a”, YuleFox 注: 其他 “has-a” 情況下請(qǐng)使用組合) 的情況下使用繼承:如果 Bar
的確 “是一種” Foo
,Bar
才能繼承 Foo
辫红。
必要的話凭涂,析構(gòu)函數(shù)聲明為 virtual
。如果你的類有虛函數(shù)贴妻,則析構(gòu)函數(shù)也應(yīng)該為虛函數(shù)切油。
對(duì)于可能被子類訪問(wèn)的成員函數(shù),不要過(guò)度使用 protected
關(guān)鍵字名惩。 注意澎胡,數(shù)據(jù)成員都必須是 私有的。
對(duì)于重載的虛函數(shù)或虛析構(gòu)函數(shù)娩鹉,使用 override
滤馍, 或 (較不常用的) final
關(guān)鍵字顯式地進(jìn)行標(biāo)記。較早 (早于 C++11) 的代碼可能會(huì)使用 virtual
關(guān)鍵字作為不得已的選項(xiàng)底循。因此巢株,在聲明重載時(shí),請(qǐng)使用 override
熙涤, final
或 virtual
的其中之一進(jìn)行標(biāo)記阁苞。標(biāo)記為 override
或 final
的析構(gòu)函數(shù)如果不是對(duì)基類虛函數(shù)的重載的話,編譯會(huì)報(bào)錯(cuò)祠挫,這有助于捕獲常見(jiàn)的錯(cuò)誤那槽。這些標(biāo)記起到了文檔的作用,因?yàn)槿绻÷赃@些關(guān)鍵字等舔,代碼閱讀者不得不檢查所有父類骚灸, 以判斷該函數(shù)是否是虛函數(shù)。
四慌植、聲明順序
將相似的聲明放在一起甚牲,將 public
部分放在最前义郑。
類定義一般應(yīng)以 public:
開始,后跟 protected:
丈钙,最后是 private:
非驮。省略空部分。
在各個(gè)部分中雏赦,建議將類似的聲明放在一起劫笙,并且建議以如下的順序:類型 (包括 typedef
, using
和嵌套的結(jié)構(gòu)體與類)星岗,常量填大,工廠函數(shù),構(gòu)造函數(shù)俏橘,賦值運(yùn)算符允华,析構(gòu)函數(shù),其它函數(shù)敷矫,數(shù)據(jù)成員例获。
不要將大段的函數(shù)定義內(nèi)聯(lián)在類定義中。通常曹仗,只有那些普通的榨汤,或性能關(guān)鍵且短小的函數(shù)可以內(nèi)聯(lián)在類定義中。參見(jiàn) 內(nèi)聯(lián)函數(shù) 一節(jié)怎茫。
? 由 Leung 寫于 2019 年 11 月 3 日
? 參考:Google 開源項(xiàng)目風(fēng)格指南——3. 類
[代碼整潔之道]