代碼風(fēng)格(5)——類

一灯谣、類應(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++ 中 structclass 關(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 的確 “是一種” FooBar 才能繼承 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熙涤, finalvirtual 的其中之一進(jìn)行標(biāo)記阁苞。標(biāo)記為 overridefinal 的析構(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è)部分中雏赦,建議將類似的聲明放在一起劫笙,并且建議以如下的順序:類型 (包括 typedefusing 和嵌套的結(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. 類
    [代碼整潔之道]

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末收壕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子轨蛤,更是在濱河造成了極大的恐慌蜜宪,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祥山,死亡現(xiàn)場(chǎng)離奇詭異圃验,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)缝呕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門澳窑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人供常,你說(shuō)我怎么就攤上這事摊聋。” “怎么了栈暇?”我有些...
    開封第一講書人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵麻裁,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng)煎源,這世上最難降的妖魔是什么色迂? 我笑而不...
    開封第一講書人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮薪夕,結(jié)果婚禮上脚草,老公的妹妹穿的比我還像新娘赫悄。我一直安慰自己原献,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開白布埂淮。 她就那樣靜靜地躺著姑隅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪倔撞。 梳的紋絲不亂的頭發(fā)上讲仰,一...
    開封第一講書人閱讀 50,084評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音痪蝇,去河邊找鬼鄙陡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛躏啰,可吹牛的內(nèi)容都是我干的趁矾。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼给僵,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼毫捣!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起帝际,我...
    開封第一講書人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蔓同,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蹲诀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體斑粱,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年脯爪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了则北。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡披粟,死狀恐怖咒锻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情守屉,我是刑警寧澤惑艇,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響滨巴,放射性物質(zhì)發(fā)生泄漏思灌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一恭取、第九天 我趴在偏房一處隱蔽的房頂上張望泰偿。 院中可真熱鬧,春花似錦蜈垮、人聲如沸耗跛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)调塌。三九已至,卻和暖如春惠猿,著一層夾襖步出監(jiān)牢的瞬間羔砾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工偶妖, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留姜凄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓趾访,卻偏偏與公主長(zhǎng)得像态秧,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子腹缩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容