嵌入式模塊化編程—— 層次框架初探
【說(shuō)在前面的話(huà)】
模塊化的目的是什么?——?復(fù)用代碼,節(jié)省開(kāi)發(fā)時(shí)間;
阻礙模塊化實(shí)現(xiàn)其最初目的的障礙是什么?——?把原本的黑盒子當(dāng)成白盒子,或者更通俗的說(shuō):閱讀模塊的源代碼;
能不能介紹一種模塊化的方法?—— Service模型;
如何在Service模型的基礎(chǔ)上真正把模塊做成黑盒子?——?掩碼結(jié)構(gòu)體;
如果你錯(cuò)過(guò)了上述內(nèi)容,可以單擊破折號(hào)后面的關(guān)鍵字跳轉(zhuǎn)到對(duì)應(yīng)的文章進(jìn)行閱讀。正如你所看到的,這一系列問(wèn)題實(shí)際上也正是沿著一個(gè)常見(jiàn)的思維過(guò)程展開(kāi)的。一般來(lái)說(shuō),當(dāng)我們學(xué)習(xí)過(guò)Service模型后,就有能力制作出一個(gè)個(gè)積木,并將這些積木在各種不同的項(xiàng)目環(huán)境中使用起來(lái)——我們可以這么理解:
模塊的內(nèi)部不僅對(duì)外是不可見(jiàn)的,本質(zhì)上也不依賴(lài)于模塊的外部環(huán)境;
模塊的內(nèi)部可以被認(rèn)為是一個(gè)獨(dú)立的小世界:
一方面根據(jù)事先約定的有限的接口通過(guò)接口頭文件與外界交互;
一方面通過(guò)配置頭文件(app_cfg.h)從外界接收“范圍可控”的配置信息;
既然是獨(dú)立的小世界,無(wú)論外部的工程環(huán)境如何,模塊本身都可以正常的編譯或者參加鏈接——換句話(huà)說(shuō),通過(guò)拷貝模塊目錄的方式應(yīng)該就算是帶上了模塊的全部家當(dāng);
【從平面到立體的轉(zhuǎn)變】
它們是如何組裝在一起的;
它們之間的領(lǐng)導(dǎo)和被領(lǐng)導(dǎo)關(guān)系是怎樣的。
對(duì)于第一個(gè)問(wèn)題,可以打個(gè)比方:
我家倉(cāng)庫(kù)里有很多書(shū),這些書(shū)最初是雜亂的堆積在地上,查找書(shū)籍非常痛苦。 我先找來(lái)幾個(gè)盒子,把同類(lèi)書(shū)籍都放在相同的盒子里。此時(shí),盒子上就可以貼上標(biāo)簽,比如,編程類(lèi)圖書(shū)、小說(shuō)、廚藝書(shū)籍、雜志等等(當(dāng)然分類(lèi)可以更細(xì)); 接下來(lái),我又找來(lái)更大的箱子,貼上標(biāo)簽:工作類(lèi)的書(shū)籍、休閑類(lèi)的書(shū)籍、要收藏的書(shū)籍、要捐獻(xiàn)的書(shū)籍等等。然后把之前的幾個(gè)紙盒子按照這些屬性放置到大箱子里。 最后,我給倉(cāng)庫(kù)貼了一個(gè)標(biāo)簽:傻孩子的私人藏書(shū)。如果家里來(lái)了同樣是書(shū)蟲(chóng)的客人,便會(huì)不無(wú)炫耀的帶著他們參觀一番。
這種按照功能或者某種功能原則對(duì)內(nèi)容進(jìn)行歸類(lèi),并套娃式的封裝的行為,跟我們進(jìn)行層次化封裝時(shí)候所做的事情是一樣的。如你所見(jiàn),如何分類(lèi)、遵循怎樣的原則是一個(gè)非常主觀的事情,可以非常確定的說(shuō),這里沒(méi)有永遠(yuǎn)正確的方法和標(biāo)準(zhǔn),只有不同人基于自己能力、閱歷、經(jīng)驗(yàn)以及項(xiàng)目的不同要求而做出的判斷——很多時(shí)候,評(píng)論他人的模塊封裝如何的不好,基本上就等于評(píng)論他人的能力或者品味——是引戰(zhàn)的代名詞。
本文也許會(huì)不小心介紹一些所謂的“劃分原則”,這里首先約定一下,無(wú)論我說(shuō)的如何“言之鑿鑿”、仿佛是“普世真理”一般,請(qǐng)相信我,這仍然只是基于我個(gè)人的主觀看法——并不具有任何客觀性(這也是為什么我一再?gòu)?qiáng)調(diào)“計(jì)算機(jī)科學(xué)是唯心”的這一說(shuō)法的原因)?;谶@樣的約定,如果你對(duì)我的所謂“劃分原則”有什么腹誹,還請(qǐng)多多見(jiàn)諒,請(qǐng)記住這句話(huà):這里不存在什么“我是對(duì)的你就是錯(cuò)的”之類(lèi)的問(wèn)題——如果你堅(jiān)持,那么結(jié)論就是“我是錯(cuò)的,你是對(duì)的”——結(jié)案了。
原本平鋪在地上的書(shū)被立體的堆積了起來(lái),這就是層次框架;而堆積過(guò)程中的指導(dǎo)原則就是整個(gè)框架的設(shè)計(jì)理念——記住,混亂的理念也是理念;前后矛盾的理念也是理念——只要你把書(shū)堆起來(lái)了,就有一個(gè)理念,哪怕你說(shuō),“我沒(méi)想那么多,也就是看到空盒子就裝起來(lái)”——不好意思,這也是你的理念。
對(duì)應(yīng)到軟件框架上,我們可以看到,實(shí)踐中有大量理念混亂復(fù)雜的系統(tǒng),仍然運(yùn)行的非常好,只不過(guò)人們親切的稱(chēng)呼他們?yōu)?strong>祖?zhèn)魇荷剑?/span>然后默默的敬而遠(yuǎn)之。

實(shí)際上,1)設(shè)計(jì)理念統(tǒng)一、2)簡(jiǎn)潔、3)可執(zhí)行且4)執(zhí)行力充分是每一個(gè)好框架在設(shè)計(jì)之初都曾設(shè)立過(guò)的美好夢(mèng)想——可惜最終基本上沒(méi)有誰(shuí)完全實(shí)現(xiàn)這一愿景——如果每一項(xiàng)都用5星來(lái)衡量的話(huà),對(duì)一個(gè)項(xiàng)目來(lái)說(shuō),平均下來(lái)這里列舉的每一個(gè)目標(biāo)都達(dá)到3星就是一個(gè)很好很好的軟件框架了。

#include 本質(zhì)上就是將指定的文件直接“包含”到當(dāng)前的文件中,這一工作在預(yù)編譯階段完成——當(dāng)進(jìn)入編譯階段時(shí),已經(jīng)看不到任何#include了;
C編譯器支持多個(gè)錨點(diǎn),用戶(hù)可以通過(guò)命令行 "-I<路徑>"的方式添加一個(gè)新的錨點(diǎn)(或者通過(guò)IDE用戶(hù)界面的方式來(lái)輔助添加);
每一個(gè)#include?都有一個(gè)默認(rèn)錨點(diǎn),也就是#include所在文件的當(dāng)前目錄;
對(duì)于每一個(gè) "#include",編譯器會(huì)按照一定的順序嘗試多個(gè)錨點(diǎn)——去看看,按照以某個(gè)錨點(diǎn)為基準(zhǔn)的相對(duì)路徑能不能找到對(duì)應(yīng)的文件:如果找得到,則大功告成,找不到則再試試別的錨點(diǎn)。我們常說(shuō)的?#include <> 和 include "" 的區(qū)別就是編譯器嘗試錨點(diǎn)的順序區(qū)別,其中:
使用<>?表示編譯器會(huì)首先從用戶(hù)指定的錨點(diǎn)去查找路徑,找不到了再去找默認(rèn)錨點(diǎn)(也就是當(dāng)前目錄);
使用"" 表示編譯器會(huì)首先嘗試默認(rèn)錨點(diǎn),找不到了再去嘗試用戶(hù)指定的錨點(diǎn);
基于上述事實(shí),我們可以規(guī)定:
在一個(gè)層次框架模型中,只允許使用相對(duì)路徑來(lái)進(jìn)行模塊間的引用;
除了默認(rèn)錨點(diǎn)外,一個(gè)層次框架中,應(yīng)該包含一個(gè)指向模塊頂層模塊目錄的錨點(diǎn)——我們稱(chēng)之為根錨點(diǎn)。
當(dāng)一個(gè)模塊明確知道自己所引用的目標(biāo)模塊(或者頭文件)相對(duì)自己的位置在未來(lái)是不太可能會(huì)變化時(shí),推薦使用默認(rèn)錨點(diǎn)——也就是以#include所在文件自己為錨點(diǎn)來(lái)描述相對(duì)路徑;
當(dāng)一個(gè)模塊明確知道自己與目標(biāo)模塊的相對(duì)位置在未來(lái)是很可能會(huì)變化的;但目標(biāo)模塊相對(duì)根錨點(diǎn)的位置卻不太可能會(huì)變化時(shí),推薦使用根錨點(diǎn)來(lái)描述相對(duì)路徑;
在某些極端(且應(yīng)該極力避免)的情況下,一個(gè)模塊完全不能確定目標(biāo)模塊的位置會(huì)如何變化——也就是即不知道相對(duì)自己會(huì)怎么變化,也不知道目標(biāo)模塊相對(duì)根錨點(diǎn)會(huì)如何變化——此時(shí),應(yīng)該直接#include 目標(biāo)文件名,而不包含任何路徑信息。這樣做的目的本質(zhì)上就是甩鍋給用戶(hù)——請(qǐng)“您”在工程配置里為這個(gè)“只有您會(huì)知道會(huì)放在那里”的目標(biāo)模塊配置一個(gè)錨點(diǎn)。我們把這種錨點(diǎn)叫做用戶(hù)錨點(diǎn)。這里,我們人為規(guī)定:應(yīng)該避免用戶(hù)錨點(diǎn)的使用——需要注意的是,這里沒(méi)有任何客觀的關(guān)于對(duì)錯(cuò)的判斷,請(qǐng)避免不必要的對(duì)錯(cuò)性爭(zhēng)論。


在介紹service模型的文章末尾,我們指出了一個(gè)令人頭疼的問(wèn)題:即,如果每個(gè)模塊都有一個(gè)app_cfg.h,那么層次結(jié)構(gòu)下往往會(huì)有一串的“app_cfg.h”。這一問(wèn)題在IDE環(huán)境下進(jìn)行頭文件包含路徑展開(kāi)時(shí)尤為突出——簡(jiǎn)直到了不能容忍的地步——廣大service模型愛(ài)好者親切的稱(chēng)之為“app_cfg.h的鬼畜”。借助錨點(diǎn),我們就能輕松的解決這一問(wèn)題,思路如下:
每一個(gè)擁有復(fù)雜縱深的大模塊在最頂層,根據(jù)模塊的名稱(chēng)建立一個(gè)唯一的配置頭文件 "<模塊名稱(chēng)>_cfg.h";
刪除所有子模塊自己的 app_cfg.h;
由于該配置頭文件相對(duì)模塊頂層目錄的位置是固定的,因此大模塊內(nèi)所有的文件都以相對(duì)根錨點(diǎn)描述的相對(duì)路徑來(lái)包含"<模塊名稱(chēng)>_cfg.h";
具體情況如下圖所示(注意,為了美觀,這里把每個(gè)子模塊對(duì)"<模塊名稱(chēng)>_cfg.h"的#include箭頭都省略了):

【模塊間調(diào)用/引用規(guī)約】
它們是如何組裝在一起的;
它們之間的領(lǐng)導(dǎo)和被領(lǐng)導(dǎo)關(guān)系是怎樣的。
其中,模塊間的領(lǐng)導(dǎo)與被領(lǐng)導(dǎo)關(guān)系與現(xiàn)實(shí)中的公司內(nèi)部結(jié)構(gòu)非常類(lèi)似,表現(xiàn)為部門(mén)間的協(xié)作原則也可以被“直接”拿過(guò)來(lái)用于指導(dǎo)模塊間的協(xié)作關(guān)系。下面,我就為大家介紹三條關(guān)鍵的基本原則:
跨部門(mén)(必須)調(diào)老大原則;
同部門(mén)(至少)平級(jí)調(diào)用原則;
以及
領(lǐng)導(dǎo)避諱原則。
跨部門(mén)(必須)調(diào)用老大原則

在這一圖中,左邊方框所表示的模塊嘗試去包含右邊大部門(mén)內(nèi)的一個(gè)子模塊(橙色虛線(xiàn)所示),根據(jù)“調(diào)老大”原則,我們不能繞開(kāi)“隔壁部門(mén)”的老大而去直接給它的小弟布置任務(wù)。
同部門(mén)(至少)平級(jí)調(diào)用原則

在圖中,左邊方框中的模塊與右邊它想包含的模塊同屬于一個(gè)大部門(mén)。由于“平級(jí)調(diào)用”原則的限制,我們同樣不能繞開(kāi)隔壁團(tuán)隊(duì)的領(lǐng)導(dǎo)而去直接招呼人家的小弟。
領(lǐng)導(dǎo)避諱原則
對(duì)一個(gè)接口頭文件來(lái)說(shuō),無(wú)論如何都不能直接或者間接的包含自己的“領(lǐng)導(dǎo)”;
在這一前提基礎(chǔ)上,我們應(yīng)該盡可能也去“調(diào)老大”——哪怕隸屬于同一大模塊。
一個(gè)具體的例子如下圖所示:

在途中,一個(gè)模塊的接口頭文件試圖去包含隔壁部門(mén)的小弟,考慮到“盡快可能掉老大”的要求,以及“避諱領(lǐng)導(dǎo)”的限制,圖中就有了兩條可行的包含方案(以藍(lán)色箭頭表示)——實(shí)際上,選擇最大的有效領(lǐng)導(dǎo)(也就是斜向上的藍(lán)色箭頭所示的頭文件)才是我們所推薦的。
其實(shí),如果你理解了上述三條原則,你很快就會(huì)發(fā)現(xiàn)一句更為凝練的口訣,即:避開(kāi)自己的領(lǐng)導(dǎo),盡可能調(diào)老大,完畢。
【不是結(jié)束的后記】
計(jì)算機(jī)技術(shù)是“唯心的”,只不過(guò),誰(shuí)的話(huà)語(yǔ)權(quán)強(qiáng)大,誰(shuí)說(shuō)了算;
除了少數(shù)與數(shù)學(xué)、物理、經(jīng)濟(jì)學(xué)(分配資源相關(guān)的知識(shí))有關(guān)的硬核理論外,更多的計(jì)算機(jī)技術(shù)和術(shù)語(yǔ)都是像你我這樣普普通的一線(xiàn)程序員從生活中借鑒而來(lái)的——所以說(shuō),加油吧,打工人。
如果你喜歡我的思維、覺(jué)得我的文章對(duì)你有所啟發(fā),
請(qǐng)務(wù)必 “點(diǎn)贊、收藏、轉(zhuǎn)發(fā)” 三連,這對(duì)我很重要!謝謝!
歡迎訂閱 裸機(jī)思維
