福州江閩儀器技術有限公司
産品搜索:

技術交流

周立功(gong):設計良好的程序(xù)接口需注意的5個(ge)事項
來源:   發布時(shí)間:2025-12-16   浏覽量:66

周立功(gōng)教授數年之心血(xuè)之作《程序設計與(yu)數據結構》,電子版(ban)已無償性分享到(dào)電子工程師與高(gāo)校群體。書本内容(róng)公開後,在電子行(háng)業掀起一片學習(xi)熱潮。經周立功教(jiāo)授授權,特對本書(shū)内容進行連載,願(yuàn)共勉之。

>>>> 1.5.2 建立(li)抽象

抽象化的目(mù)的是使調用者無(wú)需知道模塊的内(nei)部細節,隻需要知(zhī)道模塊或函數的(de)名字,因此将其稱(chēng)爲黑⭐盒化。調用者(zhě)隻需要⭐知道黑盒(hé)子的輸入和輸出(chū),而過程的細節是(shi)隐藏的。由于建立(lì)了一個由黑盒子(zi)組成的系統,因此(cǐ)複雜的結構就被(bèi)黑盒子隐藏起來(lai)了,則理解系統的(de)整體結構就變得(de)更容易了。

從概念(nian)的視角來看,建立(lì)抽象關注的不是(shi)如何實🔱現,而是函(hán)數🌂要做什麽,過早(zǎo)地關注實現細節(jiē),将實現細節隐藏(cáng)起來,進而幫助我(wǒ)們構建更易于修(xiu)改🏒的軟件。因此,我(wǒ)們首先🏃‍♀️應該選擇(ze)一個具有描述性(xìng)的符合需求的名(ming)字,雖然可以選擇(ze)的名字有swapByte、swapWord和swap,但swap更(gèng)簡潔更貼切。其次(cì),可以用一句話概(gài)念性地描述swap的數(shù)據抽象——swap是實現兩(liǎng)個數據交換的函(hán)數。

顯然,調用者僅(jǐn)需一般性地在概(gài)念層次上與實現(xiàn)者交流,因📧爲調用(yong)者的意圖是如何(he)使用swap()實現兩個❤️數(shù)據的交換,所💛以無(wú)需準确地知道實(shí)現的細節。而具體(tǐ)如何完成數據的(de)交換,這是在實現(xian)層次進行的。由此(cǐ)可🤞見,将模塊的目(mù)的與實現分離的(de)抽象揭示了問題(tí)的😘本質,并沒有提(tí)供解決方⛷️案。隻說(shuo)明需要做什麽,并(bing)不會指出如何實(shi)現某個模塊。隻🔱要(yào)概念不變,調用者(zhě)與實現細🐉節的變(bian)化就徹底隔離了(le)⛷️。當某個模塊完成(cheng)編👉碼後,隻要說明(míng)該模塊的目的和(hé)參數就可以使用(yong)它,無需知道具體(ti)的實現。

函數抽象(xiàng)對團隊項目非常(chang)重要,因爲在團隊(duì)中必須使用其他(ta)成員編寫的模塊(kuài)。比如,編程語言本(ben)身🔴自帶⛷️的庫🛀🏻函數(shu),由于已經被預編(biān)譯,因此無法訪問(wen)它的源✊代碼。同時(shi)庫函數不一定是(shi)用C編寫的,因此隻(zhī)要知道其調用規(guī)範,就可以在程序(xù)中毫無顧忌地使(shǐ)用這個函數。實際(jì)上🔴,在使用scanf()函數的(de)過程中,我們考慮(lü)過scanf()是如何實現的(de)嗎?無關緊要。盡管(guan)不同系統實現scanf()的(de)💔方法可能不一樣(yàng),但其中的不同對(duì)于程序員來說是(shi)透明的。

>>>> 1.5.3 建立接口(kou)

接口是由公開訪(fang)問的方法和數據(jù)組成的,接口描述(shu)了與😘模塊交互的(de)唯一途徑。最小化(huà)的接口隻包含對(dui)于接口的任務非(fēi)常🏃‍♀️重要的參數,最(zui)小化的接口便于(yu)學習如何與之交(jiao)互,且隻需要理解(jie)少量的參數,同時(shi)易于擴展和維護(hu),因此設計良好的(de)接口是一項重要(yao)的技能。

(1)傳值調用

如何調(diào)用swap()函數呢?實參将(jiang)值從主調函數傳(chuán)遞給被調函數,也(ye)㊙️許其調用形式是(shì)下面這樣的:
swap(a, b);

從黑(hei)盒視角來看,形參(can)和其它局部變量(liang)都是函數私有的(de),聲明在不同函數(shù)中的同名變量是(shì)完全不同的☁️變量(liang)🤩,而且函數無法直(zhi)接訪問其它函數(shu)中的變量,這種限(xian)制訪問保護了數(shù)🌈據的完整性,黑盒(hé)發生了什麽對主(zhu)調函數是不可見(jian)的。

一個變量的有(yǒu)效範圍稱作它的(de)作用域,變量的作(zuò)用域指可以通過(guò)變量名稱引用變(bian)量的區域,在函數(shu)内✉️部聲明的變量(liang)隻在⛱️該函數内部(bù)有效。當主調函數(shù)調用子函🏃數時,主(zhu)函數内聲明的變(biàn)量在子函數内無(wu)效🏃,子函數内聲明(míng)的變量也隻在該(gai)子函數🈚内部有效(xiào)。

(2)傳址(zhǐ)調用

如果希望通(tong)過被調函數将更(gèng)多的值傳回主調(diào)函數而改變主‼️調(diào)函數中的變量,則(zé)使用“傳址調用”——将(jiang)&a、&b作爲實參✔️傳遞😄給(gěi)形參。其調用形式(shi)如下:
swap(&a, &b);

利用指針作(zuo)爲函數參數傳遞(di)數據的本質,就是(shì)在主🔞調函數♍和被(bei)調函數中,通過不(bu)同的指針指向同(tong)一内存地址訪問(wen)相同的内存區域(yu),即它們背後共享(xiang)相同的内存,從而(ér)實💜現數據👨‍❤️‍👨的傳遞(dì)和交換。

>>> 2. 函數原型(xíng)

函數原型是C語言(yán)的一個強有力的(de)工具,它讓編譯器(qì)捕獲在使用函數(shù)時可能出現的許(xǔ)多錯誤或疏漏。如(rú)果編譯器沒有發(fā)現這些問題,就很(hen)難察覺出來。函數(shu)原型包括函數返(fan)回值的類型、函數(shu)名和形參列表(參(cān)數的數量和每個(gè)參數的類型),有了(le)這些㊙️信息,編譯器(qi)就可以檢查函數(shu)調用與函數原型(xing)是否匹配?比如,參(can)⛹🏻‍♀️數的數量是否正(zheng)确?參數的類型是(shi)否匹配?如果類型(xíng)不匹配,編譯器會(hui)将實參的類型🍉轉(zhuǎn)🔴換成形參的類型(xing)。

(1)函數形參

通過程(cheng)序清單 1.15可以看出(chu),其相同的處理部(bù)分是2個int類值⭐的交(jiāo)換代碼,因此可以(yǐ)将數據交換代碼(ma)移到🤩swap()函數的實現(xiàn)中,其可🐅變的👉數據(ju)由外部傳進來的(de)參數應對。由于&a是(shì)指向int類☂️型變量a的(de)指針,&b是指向int類型(xíng)變量b的指針,因此(ci)必須将p1、p2形參聲明(míng)爲指向int *類型的指(zhi)針變量,即必須将(jiāng)存儲int類型值變量(liang)的地址作爲實參(can)賦給指針形參,實(shi)參與形參才能匹(pi)配。其函數原型進(jin)化如下:
swap(int *p1, int *p2);

(2)返回值的(de)類型

聲明函數時(shí)必須聲明函數的(de)類型,帶返回值的(de)函數類型應該與(yǔ)其返回值類型相(xiang)同,而沒有返回值(zhi)的函數應💁該聲明(míng)爲🌂void。類型聲明是函(hán)數定義的一部分(fen),函數類型指👅的是(shi)返回值的類型,不(bu)是函數參數的類(lei)型。

雖然可以使用(yong)return返回值,但return隻能返(fan)回一個值給主調(diào)函數⭐。比如,如果返(fan)回值爲整數,則函(hán)數返回值的類型(xing)爲int。當返⚽回值爲int類(lei)型時,如果返回值(zhi)爲負數,則表示失(shī)敗;如果返🍓回值爲(wei)非負數,則表示成(cheng)功。當返回值爲bool類(lèi)型時,如果返回值(zhi)爲false,則表示失敗,如(rú)果返回值爲true,則表(biao)示🔴成功。當返回值(zhí)爲指針類型時,如(rú)果返🏒回值爲NULL,則表(biao)🔞示失敗,否則返回(huí)💛一個有效的指㊙️針(zhen)。

如果利用指針作(zuò)爲參數傳遞給函(hán)數,不僅可以向函(han)數😄傳入🈲數據,而且(qie)還可以從函數返(fǎn)回多個值。因爲函(han)數的調用者和函(hán)數☎️都可以使用指(zhi)向同一内🧑🏾‍🤝‍🧑🏼存地址(zhǐ)的指針,即使用同(tóng)一塊内存,所✨以使(shǐ)用指針作爲函數(shù)🤞參數時就🈲是對同(tóng)一數據進行🛀🏻讀寫(xie)操作🈲。這樣不僅可(ke)以傳入數據,還可(ke)以通過在函數内(nei)部👄修改這些數據(ju),将函數的結果傳(chuán)出給調用者。

當函(han)數的實參是指針(zhen)變量時,有時希望(wang)函數能通過🌈指針(zhen)指向⛱️别處的方式(shi)改變此變量,則需(xū)要使用指向🚩指針(zhēn)的指針作爲形參(cān)。

由于swap()無返回值,因(yīn)此swap()返回值的類型(xíng)爲void,其函數原型⁉️如(rú)下:
void swap(int *p1, int *p2);

其被解釋爲swap是(shi)返回void的函數(參數(shù)是int *p1,int *p2)。

這是一個不斷(duàn)叠代優化的過程(cheng),用戶隻需要知道(dào)“函數🔞名🍓、傳入函數(shù)的參數和函數返(fǎn)回值的類型”,就知(zhī)道如何🔅有效地調(diao)用相應的函數。

>>> 3.  依(yi)賴倒置原則

在面(miàn)向過程編程中,通(tong)常的做法是高層(ceng)模塊調用低層📞模(mo)塊,其目的之一就(jiù)是要定義子程序(xù)層次結構。當高層(ceng)模塊💔依賴于低👌層(céng)模塊時,對低層模(mó)塊的改動會❓直接(jiē)影響高層模塊,從(cóng)而迫使它們依次(cì)做出修改。如果高(gāo)層模塊獨立于🔞低(di)層模塊,則高層模(mo)塊更容易重😘用,這(zhe)就是分層架構設(she)計‼️的核心原則,即(jí)依賴倒置原則(Dependence Inversion Principle,DIP):

● 高(gāo)層模塊不應該依(yī)賴低層模塊,兩者(zhe)都應該依賴于抽(chōu)象接口;

● 抽象接口(kou)不應該依賴于細(xì)節,細節應該依賴(lài)抽象接口。

當在分(fen)層架構中使用依(yi)賴倒置原則時,将(jiāng)會發現“不再存在(zài)📱分層”的概念了。無(wú)論是高層還是低(dī)層,它們都依賴于(yu)抽象接口,好像将(jiāng)整個分層架構推(tui)平一樣。

其實從“Hello World”程(chéng)序開始,我們就已(yi)經在使用stdio.h包含的(de)“抽象接✍️口”了,即以(yǐ)後凡是用#include文件的(de)擴展名叫.h(頭文件(jian))。如果源代碼中要(yao)用到stdio标準輸入輸(shu)出函數時,那麽就(jiu)要包含這個頭🏃文(wen)件,比如,“scanf("%d",&i);”函數,其目(mu)的是告訴編譯器(qì)要使用stdio庫。庫是一(yī)♻️種工具的集合,這(zhè)些工具是由✉️其它(ta)程序員編寫的,用(yong)于實現特定的🧑🏾‍🤝‍🧑🏼功(gong)能。盡管實現者無(wú)需關🌍心用戶将如(ru)何使用庫,且不會(huì)直接開♻️放源代碼(ma)給用戶🐪使用,但必(bì)須給用戶提供調(diào)用函數所需要的(de)信息。顯然隻要将(jiāng)頭文件開放給用(yong)戶,即可讓用✂️戶了(le)解接口的所有細(xi)節,詳見程序🈲清單(dān) 1.16。

程序清單 1.16 swap數據交(jiao)換接口(swap.h)
1     #ifndef  _SWAP_H
2     #define  _SWAP_H
3     // 前置條件(jian):實參必須是int類型(xíng)變量的地址
4     // 後置(zhì)條件:p1、p2作爲輸出參(can)數,改變主調函數(shù)中相應的👅變量
5     void swap(int *p1, int *p2);
6     // 調(diao)用形式:swap(&a, &b)
7     #endif

其中,每個(gè)頭文件都指出了(le)一個用戶可見的(de)外部函數接口,主(zhu)要包括函數名、所(suo)需的參數、參數的(de)類❤️型和返回結果(guo)的類型。其中,swap是庫(kù)的名字,程序清單(dān) 1.16(1~2)與(8)是幫助編譯🥰器(qì)記錄它所讀取的(de)接口,當寫一個接(jie)口時,必須包含#ifndef、#define和(hé)#ednif。#include行部分僅當接口(kǒu)本身需要其📐它庫(ku)時才使用,它由标(biao)準的#include行組成。程序(xù)清單 1.16(6)接口🈚項表示(shì)庫輸出的函數的(de)原型、常量和類型(xing)等。不管你是否理(lǐ)解,這些行是接口(kou)的模闆文件,這就(jiu)是信息隐藏。

>>> 4. 前/後(hou)置條件

處理信息(xi)隐藏還涉及到另(ling)一個技術,那就是(shì)使用前置條件和(hé)後置條件描述函(han)數的行爲。在編寫(xiě)一個完📧整的函數(shu)定義🚩時,需✉️要描述(shù)該函數是如何執(zhi)行計算的。但在🔅使(shi)用函數時,隻需考(kǎo)慮🏃‍♂️該函數能做什(shí)麽,無需知道是如(ru)何完成的。當不知(zhī)道函數是如何實(shí)現時,就是在使用(yòng)一種名爲過程抽(chōu)象的信息☔隐藏形(xíng)式,它抽象掉的是(shi)函數☔如何工作的(de)細🈚節。計算機科學(xue)家使用🔞“過程”表⁉️示(shì)任意指令集,因此(ci)使用術語過程抽(chou)象。過程抽象是一(yi)種強大🐉的工具,使(shǐ)得我們一次隻考(kao)慮一個而不是所(suo)有的函數,從而使(shi)🛀問題求解簡單化(huà)。

爲了使描述更準(zhun)确,則需要遵循固(gù)定的格式,它包含(han)👉兩☂️部分信息:函數(shu)的前置條件和後(hou)置條件。前置條件(jiàn)就是調用該函數(shu)必須成立的條件(jian),當函數被調用時(shí),該語句給出要求(qiu)爲真的條件。除非(fei)前置條件爲真,否(fou)則無法保證函數(shu)能正确執行☁️。在調(diao)用swap()函🏃數時,實參必(bi)須是int類型變量的(de)地址,這是調用者(zhě)的職責。通常在函(han)數開始處檢查是(shi)否滿足?如果不滿(man)足,說明調用代碼(mǎ)有問題,抛出一個(ge)異常。

後置條件就(jiu)是該操作完成後(hou)必須成立的條件(jian),當函數調✨用時,如(ru)果函數是正确的(de),而且前置條件爲(wèi)真,那麽該函數調(diào)用将可🐉以執行完(wán)成。當函數調用🏃🏻完(wan)成後,後置條🈲件爲(wei)真。如果不滿足後(hou)置條件,則說明業(ye)務邏輯有問題。

當(dāng)滿足調用swap()函數的(de)前置條件時,必須(xu)同時确保其結束(shu)時滿足它的後置(zhì)條件,其後置條件(jiàn)是被調函數将♻️返(fǎn)回值傳回主調函(han)數,改變主調函數(shù)中變量的值。

前後(hou)置條件不隻是概(gai)括地描述函數的(de)行爲,聲明這些⚽條(tiáo)🌈件♍應該是設計任(rèn)何函數的第一步(bu)。在開始考慮某個(ge)函數的算法和代(dai)碼之前,應該寫出(chū)該函數的原型,其(qí)中包括函數的返(fǎn)回類型、名稱和參(cān)數列表,最後🌏緊跟(gēn)一個🥰分号。直接來(lái)自于用戶的輸入(rù)不能作爲前置條(tiáo)件,通常前/後置條(tiao)件都可以轉化⭐爲(wèi)assert語句。編寫🏃‍♂️函數原(yuán)型時,應該🈲以注釋(shi)的形式描述該函(han)數的前置條🏒件和(he)後置條件。

事實上(shàng),前置條件和後置(zhi)條件在使用函數(shù)的程序員和編寫(xiě)函數的程序員之(zhi)間形成了一個契(qì)約,也就是爲什💜麽(me)需要這個函數?接(jie)口通過前置條件(jian)和後置條件以契(qì)約的形式表達需(xū)求,承諾在滿足前(qián)置條件時開始,按(àn)照程序的流程運(yùn)行,系統就能到達(da)後置條件。

雖然注(zhù)釋是一種很好的(de)溝通形式,但在代(dài)碼可以傳遞♉意圖(tú)的💞地方不要寫注(zhù)釋。因爲代碼解釋(shì)做了什麽,再注🏃🏻‍♂️釋(shi)也沒有什麽用處(chù),相反注釋要說明(ming)爲什麽會這樣寫(xiě)代碼?

>>> 5. 開閉原則

接(jiē)口僅需指明用戶(hu)調用程序可能調(diào)用的标識符,應盡(jin)可能地将算法以(yi)及一些與具體的(de)實現細節無關的(de)信💜息隐藏起❄️來,這(zhe)樣用戶在調用程(cheng)序時也就不必依(yi)賴特🐇定的實現細(xi)節了。當接口一旦(dàn)發布後,也就不能(neng)改變了,因爲改變(bian)接口勢必引起用(yong)戶程序的改變♊。如(ru)果此前定義的接(jiē)口滿足不了需求(qiú)🤩,怎麽辦?隻能擴展(zhan)新的接口,但👉不能(neng)修改或廢除原有(you)的接口,這就是“對(duì)修改關閉,對擴展(zhan)開放”的開閉原則(zé)(Open-Closed Princple,OCP)。顯然,依賴倒置原(yuán)則更加精确的定(ding)義就是面向接口(kou)的編程,它是實現(xian)開閉🌍原則的重要(yào)途徑。如果DIP依賴倒(dao)置原則沒有實現(xian),就别想實現對擴(kuò)展開🔅放,對修改關(guan)閉。

总(zǒng) 公 司急 速 版WAP 站H5 版(bǎn)无线端AI 智能3G 站4G 站(zhan)5G 站6G 站
 
·
·