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

