- 總部(bu):福州市八(ba)一七中路(lu)茶亭國際(ji)
- 電話:0591-83275886
- E-mail:[email protected]
- http://qigi.cc

技(jì)術交流
第一(yi)章爲程序(xu)設計基礎(chǔ),本文爲1.5.2/1.5.3共(gòng)性與可變(bian)性分析:建(jian)立抽象和(hé)建立接口(kou)。
>>>> 1.5.2 建立抽象(xiang)
抽象化的(de)目的是使(shi)調用者無(wu)需知道模(mo)塊的内部(bù)細節,隻☔需(xu)要知道模(mó)塊或函數(shu)的名字,因(yin)此将其稱(cheng)爲黑盒化(huà)。調用者隻(zhī)需要知道(dào)黑盒子的(de)輸入和輸(shu)出,而過程(cheng)的細節是(shi)隐藏的。由(yóu)于建立了(le)一個由黑(hēi)✨盒子組成(cheng)的系統,因(yīn)✊此複雜的(de)結構就被(bei)黑盒子隐(yǐn)🌈藏起來了(le),則理解系(xì)統的整體(ti)結構就變(biàn)得更容易(yì)了。
從概念(nian)的視角來(lái)看,建立抽(chōu)象關注的(de)不是如何(he)實現,而是(shì)函數要做(zuò)什麽,過早(zao)地關注實(shí)現細節,将(jiāng)實現細節(jiē)隐藏起♍來(lái),進🚶而幫助(zhù)我們構建(jian)更易于修(xiu)改的軟件(jiàn)。因此,我們(men)首先應該(gāi)選擇一個(ge)具有描述(shù)性的符合(he)需求的名(ming)字,雖然可(ke)以選擇的(de)名💃字有swapByte、swapWord和(he)swap,但swap更簡潔(jié)更貼切。其(qi)次,可以用(yong)一句話概(gai)念性地描(miáo)述swap的數據(ju)抽象——swap是實(shí)現兩個數(shu)據交換的(de)函數。
顯然(ran),調用者僅(jin)需一般性(xìng)地在概念(niàn)層次上與(yǔ)實現者交(jiao)流,因爲調(diao)用者的意(yi)圖是如何(hé)使用swap()實現(xiàn)兩個🛀🏻數據(ju)🌈的交🐆換,所(suǒ)以無需準(zhun)确地知道(dao)實現的細(xi)節。而具體(tǐ)如何完成(chéng)數據的交(jiao)換,這是在(zài)實現層次(cì)進行的。由(yóu)此可見,将(jiāng)模塊💃🏻的目(mu)的與實現(xiàn)分離的抽(chou)象揭示了(le)問題的本(běn)質,并沒有(you)提供解決(jué)方案。隻說(shuo)明需要做(zuò)什麽,并不(bú)會指出如(rú)何實‼️現某(mou)個模塊。隻(zhī)要概念不(bu)變,調用者(zhě)與♋實現細(xì)節的變化(hua)就徹底隔(ge)離了。當某(mǒu)個模塊完(wan)成編碼後(hòu),隻要說明(míng)該模📞塊的(de)目的和參(cān)數就可以(yǐ)使用它,無(wú)需知道具(jù)體的實現(xiàn)。
函數抽象(xiang)對團隊項(xiàng)目非常重(zhòng)要,因爲在(zài)團隊中必(bì)須使用其(qí)☀️他成員編(biān)寫的模塊(kuai)。比如,編程(cheng)語言本身(shen)自帶⭐的庫(kù)函數,由于(yu)已經被預(yu)編譯,因此(cǐ)無法訪問(wèn)它的源代(dài)碼。同🌈時庫(kù)函數不一(yi)💔定是用C編(biān)寫的,因此(cǐ)隻要知道(dao)其調用規(gui)範,就可以(yǐ)在程序中(zhong)🤩毫無顧忌(jì)💜地使用這(zhè)個函數。實(shí)際上😍,在使(shi)用scanf()函數的(de)過程中,我(wǒ)們考慮過(guò)scanf()是🤞如何實(shi)現的嗎?無(wu)關緊要。盡(jìn)管不同系(xì)統實現scanf()的(de)方法可能(néng)不一樣,但(dàn)其中🌍的不(bu)同對于程(cheng)💚序員來說(shuo)是透明的(de)。
>>>> 1.5.3 建立接口(kǒu)
>>> 1. 函數調用(yòng)
(1)傳值調用(yòng)
如何調用(yòng)swap()函數呢?實(shí)參将值從(cong)主調函數(shù)傳遞給被(bèi)調🈲函數,也(ye)許其調用(yong)形式是下(xia)面這樣的(de):
swap(a, b);
從黑盒視(shi)角來看,形(xing)參和其它(tā)局部變量(liàng)都是函數(shù)私有的,聲(sheng)✊明在不同(tong)函數中的(de)同名變量(liang)是完全不(bu)同的🔱變量(liang),而且函數(shù)無法直接(jie)訪問其它(tā)函數中的(de)💃🏻變量,這🌈種(zhǒng)限制訪問(wèn)保護了數(shu)💘據的完整(zheng)性,黑盒發(fa)生了什麽(me)對主調函(hán)數是不可(kě)見的。
一個(ge)變量的有(you)效範圍稱(cheng)作它的作(zuò)用域,變量(liang)的作用域(yu)指可以通(tōng)過變量名(míng)稱引用變(biàn)量的區域(yù),在函數内(nèi)部聲明的(de)⛱️變量隻在(zài)該函數内(nèi)部有效。當(dāng)主調函數(shu)調用子函(han)數時,主㊙️函(han)數内🈲聲明(ming)的變量在(zài)子函數内(nèi)無效👄,子函(hán)數内聲明(ming)的變量也(yě)隻在該子(zǐ)函數内部(bù)有效。
由于(yu)傳遞給函(han)數的是變(bian)量的替身(shen),因此改變(biàn)函數參數(shu)對原始變(biàn)量沒有影(yǐng)響。當變量(liàng)傳遞給函(hán)數時,變量(liàng)的值🔞被複(fú)制給函數(shù)參數。由此(cǐ)可見,通過(guò)“傳值調用(yong)”方式交換(huàn)a、b的值,無法(fǎ)改變主調(diào)函數相應(yīng)變量的值(zhi)。
(2)傳址調用(yòng)
如果希望(wàng)通過被調(diào)函數将更(geng)多的值傳(chuán)回主調函(han)數而♈改變(bian)主調函數(shu)中的變量(liàng),則使用“傳(chuán)址調用”——将(jiang)&a、&b作爲實參(can)傳遞給形(xing)參。其調用(yong)形式如下(xià):
swap(&a, &b);
利用指針(zhen)作爲函數(shu)參數傳遞(dì)數據的本(běn)質,就是在(zài)㊙️主調函數(shù)和被調函(han)數中,通過(guo)不同的指(zhǐ)針指向同(tóng)🍓一内存地(di)址訪問相(xiàng)同的🙇🏻内存(cun)區域,即它(ta)們背後共(gong)👅享相同的(de)内存,從而(er)實現數據(ju)的傳遞和(hé)交換。
>>> 2. 函數(shù)原型
(1)函數(shu)形參
(2)返回值的(de)類型
聲明(ming)函數時必(bì)須聲明函(hán)數的類型(xíng),帶返回值(zhi)的函數類(lei)型應該👄與(yu)其返回值(zhi)類型相同(tóng),而沒有返(fan)回值的函(han)🔴數應該聲(shēng)明爲void。類型(xíng)聲明是函(han)數定義的(de)一部分,函(hán)數類型指(zhi)的是返回(huí)值的😘類型(xing),不是函數(shu)參數的類(lèi)型。
雖然可(ke)以使用return返(fan)回值,但return隻(zhī)能返回一(yī)個值給主(zhu)調函數。比(bi)如,如果返(fan)回值爲整(zhěng)數,則函數(shu)返回值的(de)類型爲int。當(dang)返📞回值爲(wèi)int類型時🌐,如(rú)果返回值(zhi)爲負數,則(ze)表示失敗(bai);如果返回(hui)值爲🥰非負(fù)數,則表示(shì)成功。當返(fan)回值爲bool類(lèi)型時,如果(guǒ)返㊙️回值爲(wèi)false,則表示失(shi)敗,如果返(fǎn)回值爲true,則(zé)表示☁️成功(gōng)。當返回值(zhi)爲指針類(lei)型時,如果(guo)返🌈回值爲(wei)NULL,則表🏒示失(shī)敗,否則返(fan)回一個有(you)效的指針(zhen)。
如果利用(yong)指針作爲(wèi)參數傳遞(di)給函數,不(bu)僅可以向(xiàng)函數傳入(rù)數據,而且(qiě)還可以從(cóng)函數返回(huí)多個值。因(yin)爲函數的(de)🏃調用💜者和(he)函數都可(ke)以使用指(zhi)向同一内(nei)存地址✔️的(de)指針,即使(shǐ)用同一塊(kuài)内存,所🈲以(yi)使用指針(zhēn)作爲函數(shù)⭐參數時就(jiù)🆚是對同一(yī)數據進行(hang)讀寫操作(zuo)。這樣不僅(jǐn)可以傳入(ru)數據,還可(kě)以通過在(zai)函數内部(bù)修改這些(xie)數據,将函(han)數的結果(guo)傳出給調(diào)用者。
當函(hán)數的實參(cān)是指針變(biàn)量時,有時(shi)希望函數(shu)能通過㊙️指(zhi)針指向别(bié)處的方式(shì)改變此變(biàn)量,則需要(yào)使用指向(xiang)指針的指(zhi)針作🐇爲形(xing)參。
由于swap()無(wu)返回值,因(yīn)此swap()返回值(zhi)的類型爲(wèi)void,其函數原(yuan)型如下:
void swap(int *p1, int *p2);
其(qí)被解釋爲(wei)swap是返回void的(de)函數(參數(shu)是int *p1,int *p2)。
這是一(yī)個不斷叠(die)代優化的(de)過程,用戶(hù)隻需要知(zhi)道🈲“函數名(míng)、傳入函數(shù)的參數和(hé)函數返回(huí)值的類型(xing)”,就知道如(ru)何有效地(di)調用相應(ying)的函數。
>>> 3. 依(yi)賴倒置原(yuan)則
在面向(xiang)過程編程(chéng)中,通常的(de)做法是高(gao)層模塊調(diao)用低🌍層模(mo)塊,其㊙️目的(de)之一就是(shì)要定義子(zi)程序層次(cì)結構。當高(gāo)層模塊🤟依(yī)賴于低♊層(ceng)模塊時,對(dui)低層模塊(kuai)的改動會(huì)直接影響(xiǎng)高層模塊(kuài),從而迫使(shi)它們依次(ci)做出修改(gǎi)。如果高層(céng)模🔱塊獨立(li)于低層模(mó)塊🤩,則高層(céng)模🚶塊更容(róng)易重用,這(zhe)就是分層(ceng)架構設計(jì)的核心原(yuan)則,即依賴(lai)倒置原則(zé)(Dependence Inversion Principle,DIP):
● 高層模塊(kuài)不應該依(yi)賴低層模(mo)塊,兩者都(dou)應該依賴(lài)于抽象接(jie)口;
● 抽象接(jie)口不應該(gai)依賴于細(xì)節,細節應(ying)該依賴抽(chōu)象💜接口。
當(dāng)在分層架(jia)構中使用(yong)依賴倒置(zhì)原則時,将(jiang)會發現“不(bu)📐再存⭐在分(fen)層”的概念(nian)了。無論是(shi)高層還是(shi)低層,它🤞們(men)都依賴于(yú)抽🛀🏻象接🌐口(kou),好像将整(zhěng)個分層架(jia)構推平一(yi)樣。
其實從(cóng)“Hello World”程序開始(shǐ),我們就已(yi)經在使用(yòng)stdio.h包含的“抽(chōu)象接口”了(le),即🙇♀️以後凡(fán)是用#include文件(jiàn)的擴展名(ming)叫.h(頭文件(jiàn))。如果源代(dài)碼中要用(yòng)到stdio标準輸(shū)入輸出函(han)數時,那麽(me)就要包含(han)這個頭文(wén)件,比如,“scanf("%d",&i);”函(hán)數,其目的(de)是告訴編(bian)譯器要使(shǐ)用stdio庫。庫是(shì)一種工具(ju)的集合,這(zhè)些工具是(shì)由其它程(cheng)序員編寫(xiě)的,用于實(shi)現特定的(de)功能。盡管(guan)實現者無(wú)需關心用(yòng)戶将如何(he)使用庫,且(qiě)不會直🏃♂️接(jiē)開⛷️放源代(dai)碼給用戶(hu)使用,但必(bi)須給用戶(hù)提供調用(yòng)函數所需(xu)要的信息(xi)。顯然隻要(yao)将頭文件(jian)開放給用(yòng)戶🏃♀️,即可讓(ràng)用戶了解(jiě)接口的所(suǒ)有細節,詳(xiáng)見程序清(qīng)單 1.16。
程序清(qing)單 1.16 swap數據交(jiao)換接口(swap.h)
1 #ifndef _SWAP_H
2 #define _SWAP_H
3 // 前(qián)置條件:實(shí)參必須是(shì)int類型變量(liang)的地址
4 // 後(hòu)置條件:p1、p2作(zuò)爲輸出參(cān)數,改變主(zhu)調函數中(zhong)相應的變(biàn)量
5 void swap(int *p1, int *p2);
6 // 調用形(xing)式:swap(&a, &b)
7 #endif
其中,每(měi)個頭文件(jian)都指出了(le)一個用戶(hu)可見的外(wai)部函數接(jie)口🧑🏽🤝🧑🏻,主要包(bao)括函數名(ming)、所需的參(cān)數、參數的(de)類型和返(fǎn)回結果的(de)類型。其中(zhong),swap是庫的名(ming)字,程序清(qing)單 1.16(1~2)與(8)是幫(bang)助編譯器(qi)記錄它所(suǒ)讀取的接(jiē)口,當寫一(yī)個接口時(shí),必須包含(han)#ifndef、#define和🌈#ednif。#include行部分(fèn)僅當接口(kou)本身需要(yao)其它庫時(shi)才使用,它(ta)由标準♍的(de)#include行組成。程(cheng)序清單 1.16(6)接(jie)口項表示(shì)庫輸出的(de)函數的原(yuán)型、常量和(hé)類型等。不(bú)管你是否(fou)理解,這些(xiē)行是接🌐口(kǒu)的模闆文(wen)件,這就是(shi)信息隐藏(cáng)。
>>> 4. 前/後置條(tiao)件
處理信(xin)息隐藏還(hai)涉及到另(lìng)一個技術(shù),那就是使(shǐ)用前置條(tiao)☂️件和後置(zhì)條件描述(shù)函數的行(hang)爲。在編寫(xie)一個完整(zheng)的函數定(dìng)義時,需要(yao)描述該函(han)數是如何(he)執行計算(suàn)的。但在🚶♀️使(shi)用函數時(shi),隻需考慮(lǜ)該函數能(néng)做什麽,無(wu)需知道是(shì)如何完成(cheng)的。當不知(zhi)道🈲函數是(shì)如😍何實現(xiàn)時,就是在(zài)使用一種(zhǒng)名爲過程(cheng)抽象的信(xìn)息🎯隐藏形(xíng)式,它抽🌂象(xiang)掉的是函(hán)數如何工(gōng)作的細節(jie)。計算機科(kē)學家使用(yong)“過程”表示(shì)任意指令(ling)集,因此使(shǐ)用術語過(guo)程抽象。過(guo)程抽象是(shì)一種強大(da)💜的工具,使(shǐ)得🥵我們一(yī)次隻考慮(lǜ)一個而不(bu)是所有的(de)函數,從🌈而(ér)使問題求(qiú)解簡單化(hua)。
爲了使描(miáo)述更準确(què),則需要遵(zūn)循固定的(de)格式,它包(bāo)含♋兩部分(fèn)信息:函數(shu)的前置條(tiáo)件和後置(zhi)條件。前置(zhì)條件就是(shì)調用該函(han)🛀數必須成(cheng)立的條件(jiàn),當函數被(bèi)調用時,該(gai)語句給出(chū)要求爲真(zhēn)的條件。除(chú)非前置條(tiáo)件爲真,否(fou)則無法保(bǎo)⛹🏻♀️證函數能(neng)正确執行(háng)。在調用swap()函(hán)數時,實參(can)必須是int類(lèi)型變量的(de)地址,這是(shi)調用者的(de)職責。通常(cháng)在函數開(kāi)始處檢查(chá)是否滿足(zú)?如果不滿(man)足,說明調(diao)用代碼有(you)問題,抛出(chu)一個異常(chang)。
後置條件(jiàn)就是該操(cao)作完成後(hou)必須成立(li)的條件,當(dang)函數調用(yong)💜時🌈,如果函(hán)數是正确(què)的,而且前(qian)置條件爲(wèi)真,那麽該(gai)函數調用(yòng)🏃♂️将可🏃♂️以執(zhí)行完成。當(dang)函數調用(yòng)完成後🐇,後(hou)置條件爲(wei)真🔞。如果不(bú)滿足後置(zhì)條件,則說(shuo)🔴明業務邏(luó)輯有問題(ti)。
當滿足調(diào)用swap()函數的(de)前置條件(jian)時,必須同(tong)時确保其(qi)結束⚽時滿(man)足它的後(hòu)置條件,其(qí)後置條件(jian)是被調函(hán)🏃♂️數将✍️返回(hui)值傳回主(zhǔ)調函數,改(gai)變主調函(hán)數中變量(liang)的值。
前後(hòu)置條件不(bú)隻是概括(kuò)地描述函(hán)數的行爲(wei),聲明這些(xiē)條⚽件👉應該(gai)是設計任(ren)何函數的(de)第一步。在(zài)開始考慮(lü)某個函數(shu)的算法和(he)代碼之前(qián),應該寫出(chū)該函數的(de)原型,其中(zhong)包括函數(shu)的返回類(lèi)型、名稱和(he)參數🎯列表(biao),最後緊跟(gen)一個分号(hao)。直接☎️來自(zì)于用戶的(de)輸入不能(neng)作爲前置(zhi)條件,通常(cháng)前/後置條(tiáo)件都可以(yǐ)轉化🔞爲assert語(yu)句。編寫函(han)數原型時(shí),應該以注(zhù)釋的形式(shi)💜描述該函(hán)數的✂️前置(zhì)條件和後(hòu)置條件。
事(shì)實上,前置(zhì)條件和後(hòu)置條件在(zài)使用函數(shu)的程序員(yuán)和編寫函(han)數的程序(xù)員之間形(xing)成了一個(ge)契約,也就(jiu)是爲什麽(me)需要這個(ge)函數?接口(kǒu)通過前置(zhi)條件和後(hou)置條件以(yǐ)契㊙️約的形(xíng)式表達🔞需(xū)求,承諾在(zai)滿足前置(zhi)條件時開(kai)始,按照程(chéng)序的流程(chéng)運行,系統(tǒng)就能到達(dá)後置條件(jian)。
雖然注釋(shi)是一種很(hěn)好的溝通(tong)形式,但在(zài)代碼可以(yi)💃傳遞意圖(tú)的地方不(bu)要寫注釋(shì)。因爲代碼(ma)解釋做了(le)什麽,再注(zhù)釋也🌏沒有(yǒu)什麽用處(chu),相反注釋(shì)要說明爲(wèi)什麽會這(zhe)樣寫代碼(ma)?
>>> 5. 開閉原則(ze)
接口僅需(xū)指明用戶(hu)調用程序(xù)可能調用(yong)的标識符(fú),應盡可🏃🏻能(néng)地将算法(fa)以及一些(xiē)與具體的(de)實現細節(jiē)🐉無關的信(xìn)息隐藏起(qǐ)🔞來,這樣用(yòng)戶在調用(yòng)程序時也(ye)🌏就不必依(yi)賴特定的(de)實現細節(jiē)了。當接口(kou)一旦發布(bu)後,也就不(bú)能改變了(le),因爲改變(biàn)接口勢必(bi)⁉️引起用戶(hù)程序的改(gǎi)變。如果此(ci)前定義的(de)接口滿足(zú)不了需求(qiu),怎麽辦?隻(zhi)能擴展新(xin)的接口,但(dan)🚶♀️不能修改(gǎi)或廢除原(yuán)有的接口(kou),這就是“對(duì)修改關閉(bì),對擴展開(kāi)放”的開閉(bì)原則(Open-Closed Princple,OCP)。顯然(rán),依賴倒置(zhì)原則更加(jiā)精确的定(ding)義就是面(mian)向接口的(de)編程,它是(shì)實現開閉(bi)原則的重(zhong)要途徑。如(ru)果DIP依賴倒(dǎo)置原則沒(méi)有實現,就(jiu)别想實現(xiàn)對擴展開(kāi)放,對修改(gai)關閉。
