官术网_书友最值得收藏!

2.7.2 模塊的設(shè)計(jì)方法

1. 程序的構(gòu)成

一段程序的構(gòu)成如圖2.8所示。

(1)程序由多個(gè)模塊構(gòu)成。

(2)每個(gè)模塊內(nèi)包含數(shù)據(jù)的定義、函數(shù)的實(shí)現(xiàn)和類的實(shí)現(xiàn)。

圖2.8 程序的構(gòu)成

2. 模塊的形態(tài)

在實(shí)際代碼中,模塊長什么樣子?

對于這個(gè)問題,我同樣在多次現(xiàn)場交流中提出過。有些意外的是,確實(shí)有不少軟件工程師答不上來。

下面我們看看在C語言、Python語言和Go語言中模塊的表現(xiàn)形態(tài)。

(1)在C語言中:一個(gè).c文件加上一個(gè).h文件就構(gòu)成了一個(gè)模塊。.h文件用于聲明模塊對外的接口(包括需要外部看到的結(jié)構(gòu)體定義),.c文件中包含的是模塊的實(shí)現(xiàn)。

(2)在Python語言中:一個(gè).py文件就構(gòu)成了一個(gè)模塊。一個(gè)文件要使用另外一個(gè)文件中的變量、函數(shù)和類,并需要用import來顯式地聲明。

(3)在Go語言中:一個(gè)Package是一個(gè)模塊。一個(gè)Package內(nèi)可能包含多個(gè).go文件,但是這些文件之間是沒有“隔離”的(不用import就可以直接訪問另外一個(gè)文件中的變量或函數(shù)),所以不能將一個(gè).go文件視為一個(gè)模塊。

其他語言的模塊形態(tài)這里不再一一贅述,大家可以參考以上幾個(gè)語言的例子來判斷。

3. 模塊劃分的重要性

在程序設(shè)計(jì)中,模塊的切分非常重要,但是我發(fā)現(xiàn)在大部分的編程書中并沒有對這點(diǎn)給予足夠多的重視。

好的模塊劃分是讓軟件架構(gòu)穩(wěn)定的基礎(chǔ)。如果模塊劃分得好,未來僅需要對模塊內(nèi)的實(shí)現(xiàn)做一些修改即可,對程序的改動量并不大;如果模塊劃分得不好,整個(gè)程序很可能要完全推翻重寫。這就好比一座大樓,如果基礎(chǔ)框架沒有問題,若干年后只需要重新對內(nèi)部做一次裝修即可;但是如果大樓的基礎(chǔ)框架存在問題,就需要將大樓完全推倒重建。兩種結(jié)果的成本差異巨大!

模塊劃分的好壞極大地影響了軟件的復(fù)雜度,而軟件的復(fù)雜度決定了軟件的可維護(hù)性。如果模塊劃分得不好,一段程序內(nèi)的多個(gè)模塊間會存在嚴(yán)重的耦合,這樣的軟件難以理解,也難以修改,往往牽一發(fā)而動全身。程序中的“耦合”并不是外部強(qiáng)加給軟件工程師的,而是軟件工程師自己造成的。模塊間復(fù)雜的耦合關(guān)系就像蠶吐出的絲,最后把軟件工程師自己給“綁”住了。我們在程序設(shè)計(jì)中要盡量避免“耦合”。

模塊劃分得好壞也決定了代碼的可復(fù)用性。前面介紹“好代碼的特性”時(shí),有一條標(biāo)準(zhǔn)是關(guān)于代碼的“共享”。如果一段程序內(nèi)的模塊劃分得不夠清楚,這段程序的模塊是不可能被抽出來供其他程序使用的。

4. 模塊設(shè)計(jì)的方法

說到模塊的設(shè)計(jì),“緊內(nèi)聚,松耦合”是大家經(jīng)常聽到的一句話。這句話到底是什么意思呢?我曾多次在線下交流會上問過現(xiàn)場觀眾,發(fā)現(xiàn)沒有幾個(gè)人能解釋清楚,他們更說不清楚在實(shí)踐中如何落實(shí)這個(gè)原則。

關(guān)于模塊設(shè)計(jì),我這里給出三點(diǎn)說明。

(1)單一目的Single Purpose一個(gè)模塊所提供的功能一定要聚焦和單一。不要把很多無關(guān)的功能都放在一個(gè)模塊中。“單一目的”是模塊設(shè)計(jì)中最重要的原則。只有做到了“單一目的”,才能實(shí)現(xiàn)“緊內(nèi)聚”。

(2)明確對外接口。一個(gè)模塊的對外接口是清晰和明確的。在2.6.5節(jié)中介紹了對外接口的重要性,對于一個(gè)模塊來說,也要仔細(xì)設(shè)計(jì)它的對外接口。如前文所述,“全局變量”就是一種非常不好的接口方式。如果實(shí)現(xiàn)了“明確對外接口”,就可以做到“松耦合”。

(3)以數(shù)據(jù)為中心。在做程序的模塊劃分時(shí),首先考慮有哪些數(shù)據(jù)類的模塊,然后再考慮其他模塊(如過程類的模塊)。具體方法見本章2.7.3節(jié)“劃分模塊的方法”。在20多年前,GNU創(chuàng)始人Richard Stallman曾經(jīng)到清華大學(xué)訪問并發(fā)表演講,現(xiàn)場有一位同學(xué)問他應(yīng)該如何寫程序。Richard說,應(yīng)該從數(shù)據(jù)出發(fā)來思考。

5. 模塊劃分的誤區(qū)

在模塊劃分方面,經(jīng)常出現(xiàn)以下幾種誤區(qū)。

誤區(qū)1:所有代碼放在一個(gè)模塊中,因?yàn)橐?guī)模太小。

(1)對錯(cuò)誤行為的描述:很多軟件工程師把“代碼量”作為劃分模塊的重要標(biāo)準(zhǔn)。例如,用Python語言編寫某段程序,因?yàn)橹挥?00行,于是都將其放在一個(gè).py文件中。類似這樣的情況我見過不少。

(2)對錯(cuò)誤行為的反駁:首先,模塊劃分的原則和代碼量沒有任何關(guān)系。依據(jù)代碼量來劃分模塊,違反了上面提到的“單一目的”原則。其次,程序的規(guī)模在早期是無法預(yù)估的。初始只有幾百行的程序,經(jīng)過一段時(shí)間的完善和發(fā)展,可能會達(dá)到幾千行甚至幾萬行。如果在初期沒有將模塊劃分好,等程序“長大”后再劃分就已經(jīng)晚了。這種“都放在一起”的模塊其實(shí)也很難很好地“生長”。

誤區(qū)2:把所有用到的附加功能都放在util模塊中。

(1)對錯(cuò)誤行為的描述:把程序所需要的各種輔助類的邏輯都放在一個(gè)叫作util的模塊中,有時(shí)候也叫作common。

(2)對錯(cuò)誤行為的反駁:這種做法也同樣違反了“單一目的”的原則。util模塊在開始建立的時(shí)候,可能很簡單。但是一段時(shí)間后,這個(gè)模塊會變得越來越“大”,越來越龐雜,最后成為一個(gè)非常難以維護(hù)的模塊。其實(shí),可以根據(jù)功能將util模塊進(jìn)一步劃分為多個(gè)功能單一的模塊。比如,將做文件處理有關(guān)的邏輯整合為file_util。

誤區(qū)3:從“過程”的角度出發(fā)考慮模塊的劃分。

(1)對錯(cuò)誤行為的描述:很多軟件工程師在劃分程序的模塊時(shí),是從程序執(zhí)行的過程來考慮的。假如程序中有A、B、C三大處理環(huán)節(jié),則相應(yīng)地會劃分為三個(gè)模塊。也有一些軟件工程師會將“系統(tǒng)初始化”作為一個(gè)獨(dú)立的模塊,而對于程序中的“數(shù)據(jù)”,則沒有建立獨(dú)立的模塊,其定義和實(shí)現(xiàn)混雜在這些過程類的模塊中。

(2)對錯(cuò)誤行為的反駁:應(yīng)該首先從“數(shù)據(jù)”的角度出發(fā)來考慮。具體方法將在本章2.7.3節(jié)“劃分模塊的方法”中介紹。

6. 題外話:C語言是面向過程的嗎

我們常聽到一種說法,C語言是面向過程的,C++語言是面向?qū)ο蟮摹_@種說法正確嗎?

20多年前我在清華大學(xué)讀書的時(shí)候,當(dāng)時(shí)計(jì)算機(jī)系的蔣維杜老師告訴我:

“C語言不是面向過程的,而是‘基于對象’(Object Based)的。和‘面向?qū)ο蟆∣bject Oriented)相比,‘基于對象’不支持‘繼承’和‘多態(tài)’。”

而使用C語言,也可以實(shí)現(xiàn)對數(shù)據(jù)的封裝。

在多次現(xiàn)場交流中,我會問一個(gè)問題:C語言中static關(guān)鍵字的作用是什么。大多數(shù)觀眾都能說出static是用于定義“靜態(tài)局部變量”這一作用。例如,在以下程序中,num被定義為“靜態(tài)局部變量”,以后在每次調(diào)用函數(shù)時(shí)就不再重新賦初值,而是保留上次函數(shù)調(diào)用結(jié)束時(shí)的值。

其實(shí),這樣的用法在真實(shí)代碼實(shí)現(xiàn)中并不常見。而我個(gè)人的觀點(diǎn)是,這種編寫方式甚至是不利于程序的維護(hù)的。一個(gè)函數(shù)的實(shí)現(xiàn)應(yīng)該是盡量“無狀態(tài)”的。

而static的另外一個(gè)作用卻很少有觀眾能回答上來,那就是當(dāng)它放在全局變量的前面時(shí),可以限制這個(gè)變量的訪問范圍。這個(gè)變量僅能被同一個(gè).c文件內(nèi)的代碼訪問,其他 .c文件是無法直接訪問這個(gè)變量的。

在下面的例子中,使用static裝飾了一個(gè)變量pTable,這個(gè)變量指向一個(gè)數(shù)據(jù)表。之后分別定義了一個(gè)“讀接口”set()和一個(gè)“寫接口”get()。這個(gè)模塊外部只能通過這兩個(gè)接口才能訪問這個(gè)數(shù)據(jù)表,于是就實(shí)現(xiàn)了對數(shù)據(jù)的封裝。

我個(gè)人一直對“面向?qū)ο蟆彼峁┑摹岸鄳B(tài)”和“繼承”這兩種超級能力抱有非常謹(jǐn)慎和小心的態(tài)度。我也曾經(jīng)花了不少時(shí)間去學(xué)習(xí)C++語言的各種復(fù)雜能力,但是后來卻發(fā)現(xiàn)很多能力在工程實(shí)踐中不一定是必需的,這些復(fù)雜的能力也給軟件維護(hù)帶來了困難。從軟件可維護(hù)性的角度來看,如果能夠?qū)崿F(xiàn)同樣的目的,方法應(yīng)該是越簡單越好,有時(shí)甚至要通過編程規(guī)范等手段來限制或禁止一些復(fù)雜方法的使用。

“繼承”里面隱含著關(guān)于軟件設(shè)計(jì)的一個(gè)巨大矛盾:軟件到底是被一次設(shè)計(jì)出來的,還是被逐步發(fā)展出來的?如果要設(shè)計(jì)超過兩層的繼承關(guān)系,需要在早期就對多個(gè)類之間的關(guān)系有比較清楚的認(rèn)識和設(shè)計(jì)。而在軟件的實(shí)踐中,在大多數(shù)情況下我們的認(rèn)識是隨著軟件的發(fā)展而逐步深化的,不太可能在設(shè)計(jì)早期就能夠看得這么清楚。而復(fù)雜的繼承關(guān)系,對于軟件后期的維護(hù)調(diào)整也是非常大的挑戰(zhàn)。類的“繼承”層級最好不要超過三層,而在大多數(shù)場景下只用兩層就可以了。

主站蜘蛛池模板: 高阳县| 芒康县| 汝阳县| 巴楚县| 昌宁县| 嘉黎县| 文昌市| 桃源县| 东至县| 冕宁县| 攀枝花市| 新建县| 凌源市| 成安县| 台南市| 蓬安县| 樟树市| 革吉县| 玉门市| 会理县| 和林格尔县| 甘谷县| 建宁县| 长宁县| 漳浦县| 谢通门县| 桦川县| 秦安县| 沐川县| 库车县| 泗洪县| 龙海市| 保靖县| 商南县| 五家渠市| 濉溪县| 湖南省| 名山县| 沾化县| 霞浦县| 长春市|