>>>>1.5.2 建立抽象
抽象化的目的是使調(diào)用者無(wú)需知道模塊的內(nèi)部細(xì)節(jié),只需要知道模塊或函數(shù)的名字,因此將其稱為黑盒化。調(diào)用者只需要知道黑盒子的輸入和輸出,而過(guò)程的細(xì)節(jié)是隱藏的。由于建立了一個(gè)由黑盒子組成的系統(tǒng),因此復(fù)雜的結(jié)構(gòu)就被黑盒子隱藏起來(lái)了,則理解系統(tǒng)的整體結(jié)構(gòu)就變得更容易了。
從概念的視角來(lái)看,建立抽象關(guān)注的不是如何實(shí)現(xiàn),而是函數(shù)要做什么,過(guò)早地關(guān)注實(shí)現(xiàn)細(xì)節(jié),將實(shí)現(xiàn)細(xì)節(jié)隱藏起來(lái),進(jìn)而幫助我們構(gòu)建更易于修改的軟件。因此,我們首先應(yīng)該選擇一個(gè)具有描述性的符合需求的名字,雖然可以選擇的名字有swapByte、swapWord和swap,但swap更簡(jiǎn)潔更貼切。其次,可以用一句話概念性地描述swap的數(shù)據(jù)抽象——swap是實(shí)現(xiàn)兩個(gè)數(shù)據(jù)交換的函數(shù)。
顯然,調(diào)用者僅需一般性地在概念層次上與實(shí)現(xiàn)者交流,因?yàn)檎{(diào)用者的意圖是如何使用swap()實(shí)現(xiàn)兩個(gè)數(shù)據(jù)的交換,所以無(wú)需準(zhǔn)確地知道實(shí)現(xiàn)的細(xì)節(jié)。而具體如何完成數(shù)據(jù)的交換,這是在實(shí)現(xiàn)層次進(jìn)行的。由此可見,將模塊的目的與實(shí)現(xiàn)分離的抽象揭示了問(wèn)題的本質(zhì),并沒有提供解決方案。只說(shuō)明需要做什么,并不會(huì)指出如何實(shí)現(xiàn)某個(gè)模塊。只要概念不變,調(diào)用者與實(shí)現(xiàn)細(xì)節(jié)的變化就徹底隔離了。當(dāng)某個(gè)模塊完成編碼后,只要說(shuō)明該模塊的目的和參數(shù)就可以使用它,無(wú)需知道具體的實(shí)現(xiàn)。
函數(shù)抽象對(duì)團(tuán)隊(duì)項(xiàng)目非常重要,因?yàn)樵趫F(tuán)隊(duì)中必須使用其他成員編寫的模塊。比如,編程語(yǔ)言本身自帶的庫(kù)函數(shù),由于已經(jīng)被預(yù)編譯,因此無(wú)法訪問(wèn)它的源代碼。同時(shí)庫(kù)函數(shù)不一定是用C編寫的,因此只要知道其調(diào)用規(guī)范,就可以在程序中毫無(wú)顧忌地使用這個(gè)函數(shù)。實(shí)際上,在使用scanf()函數(shù)的過(guò)程中,我們考慮過(guò)scanf()是如何實(shí)現(xiàn)的嗎?無(wú)關(guān)緊要。盡管不同系統(tǒng)實(shí)現(xiàn)scanf()的方法可能不一樣,但其中的不同對(duì)于程序員來(lái)說(shuō)是透明的。
>>>>1.5.3 建立接口
接口是由公開訪問(wèn)的方法和數(shù)據(jù)組成的,接口描述了與模塊交互的唯一途徑。最小化的接口只包含對(duì)于接口的任務(wù)非常重要的參數(shù),最小化的接口便于學(xué)習(xí)如何與之交互,且只需要理解少量的參數(shù),同時(shí)易于擴(kuò)展和維護(hù),因此設(shè)計(jì)良好的接口是一項(xiàng)重要的技能。
>>>1. 函數(shù)調(diào)用
(1)傳值調(diào)用
如何調(diào)用swap()函數(shù)呢?實(shí)參將值從主調(diào)函數(shù)傳遞給被調(diào)函數(shù),也許其調(diào)用形式是下面這樣的:
swap(a, b);
從黑盒視角來(lái)看,形參和其它局部變量都是函數(shù)私有的,聲明在不同函數(shù)中的同名變量是完全不同的變量,而且函數(shù)無(wú)法直接訪問(wèn)其它函數(shù)中的變量,這種限制訪問(wèn)保護(hù)了數(shù)據(jù)的完整性,黑盒發(fā)生了什么對(duì)主調(diào)函數(shù)是不可見的。
一個(gè)變量的有效范圍稱作它的作用域,變量的作用域指可以通過(guò)變量名稱引用變量的區(qū)域,在函數(shù)內(nèi)部聲明的變量只在該函數(shù)內(nèi)部有效。當(dāng)主調(diào)函數(shù)調(diào)用子函數(shù)時(shí),主函數(shù)內(nèi)聲明的變量在子函數(shù)內(nèi)無(wú)效,子函數(shù)內(nèi)聲明的變量也只在該子函數(shù)內(nèi)部有效。
由于傳遞給函數(shù)的是變量的替身,因此改變函數(shù)參數(shù)對(duì)原始變量沒有影響。當(dāng)變量傳遞給函數(shù)時(shí),變量的值被復(fù)制給函數(shù)參數(shù)。由此可見,通過(guò)“傳值調(diào)用”方式交換a、b的值,無(wú)法改變主調(diào)函數(shù)相應(yīng)變量的值。
(2)傳址調(diào)用
如果希望通過(guò)被調(diào)函數(shù)將更多的值傳回主調(diào)函數(shù)而改變主調(diào)函數(shù)中的變量,則使用“傳址調(diào)用”——將&a、&b作為實(shí)參傳遞給形參。其調(diào)用形式如下:
swap(&a, &b);
利用指針作為函數(shù)參數(shù)傳遞數(shù)據(jù)的本質(zhì),就是在主調(diào)函數(shù)和被調(diào)函數(shù)中,通過(guò)不同的指針指向同一內(nèi)存地址訪問(wèn)相同的內(nèi)存區(qū)域,即它們背后共享相同的內(nèi)存,從而實(shí)現(xiàn)數(shù)據(jù)的傳遞和交換。
>>>2.函數(shù)原型
函數(shù)原型是C語(yǔ)言的一個(gè)強(qiáng)有力的工具,它讓編譯器捕獲在使用函數(shù)時(shí)可能出現(xiàn)的許多錯(cuò)誤或疏漏。如果編譯器沒有發(fā)現(xiàn)這些問(wèn)題,就很難察覺出來(lái)。函數(shù)原型包括函數(shù)返回值的類型、函數(shù)名和形參列表(參數(shù)的數(shù)量和每個(gè)參數(shù)的類型),有了這些信息,編譯器就可以檢查函數(shù)調(diào)用與函數(shù)原型是否匹配?比如,參數(shù)的數(shù)量是否正確?參數(shù)的類型是否匹配?如果類型不匹配,編譯器會(huì)將實(shí)參的類型轉(zhuǎn)換成形參的類型。
(1)函數(shù)形參
通過(guò)程序清單 1.15可以看出,其相同的處理部分是2個(gè)int類值的交換代碼,因此可以將數(shù)據(jù)交換代碼移到swap()函數(shù)的實(shí)現(xiàn)中,其可變的數(shù)據(jù)由外部傳進(jìn)來(lái)的參數(shù)應(yīng)對(duì)。由于&a是指向int類型變量a的指針,&b是指向int類型變量b的指針,因此必須將p1、p2形參聲明為指向int *類型的指針變量,即必須將存儲(chǔ)int類型值變量的地址作為實(shí)參賦給指針形參,實(shí)參與形參才能匹配。其函數(shù)原型進(jìn)化如下:
swap(int *p1, int *p2);
(2)返回值的類型
聲明函數(shù)時(shí)必須聲明函數(shù)的類型,帶返回值的函數(shù)類型應(yīng)該與其返回值類型相同,而沒有返回值的函數(shù)應(yīng)該聲明為void。類型聲明是函數(shù)定義的一部分,函數(shù)類型指的是返回值的類型,不是函數(shù)參數(shù)的類型。
雖然可以使用return返回值,但return只能返回一個(gè)值給主調(diào)函數(shù)。比如,如果返回值為整數(shù),則函數(shù)返回值的類型為int。當(dāng)返回值為int類型時(shí),如果返回值為負(fù)數(shù),則表示失??;如果返回值為非負(fù)數(shù),則表示成功。當(dāng)返回值為bool類型時(shí),如果返回值為false,則表示失敗,如果返回值為true,則表示成功。當(dāng)返回值為指針類型時(shí),如果返回值為NULL,則表示失敗,否則返回一個(gè)有效的指針。
如果利用指針作為參數(shù)傳遞給函數(shù),不僅可以向函數(shù)傳入數(shù)據(jù),而且還可以從函數(shù)返回多個(gè)值。因?yàn)楹瘮?shù)的調(diào)用者和函數(shù)都可以使用指向同一內(nèi)存地址的指針,即使用同一塊內(nèi)存,所以使用指針作為函數(shù)參數(shù)時(shí)就是對(duì)同一數(shù)據(jù)進(jìn)行讀寫操作。這樣不僅可以傳入數(shù)據(jù),還可以通過(guò)在函數(shù)內(nèi)部修改這些數(shù)據(jù),將函數(shù)的結(jié)果傳出給調(diào)用者。
當(dāng)函數(shù)的實(shí)參是指針變量時(shí),有時(shí)希望函數(shù)能通過(guò)指針指向別處的方式改變此變量,則需要使用指向指針的指針作為形參。
由于swap()無(wú)返回值,因此swap()返回值的類型為void,其函數(shù)原型如下:
void swap(int *p1, int *p2);
其被解釋為swap是返回void的函數(shù)(參數(shù)是int *p1,int *p2)。
這是一個(gè)不斷迭代優(yōu)化的過(guò)程,用戶只需要知道“函數(shù)名、傳入函數(shù)的參數(shù)和函數(shù)返回值的類型”,就知道如何有效地調(diào)用相應(yīng)的函數(shù)。
>>>3.依賴倒置原則
在面向過(guò)程編程中,通常的做法是高層模塊調(diào)用低層模塊,其目的之一就是要定義子程序?qū)哟谓Y(jié)構(gòu)。當(dāng)高層模塊依賴于低層模塊時(shí),對(duì)低層模塊的改動(dòng)會(huì)直接影響高層模塊,從而迫使它們依次做出修改。如果高層模塊獨(dú)立于低層模塊,則高層模塊更容易重用,這就是分層架構(gòu)設(shè)計(jì)的核心原則,即依賴倒置原則(Dependence Inversion Principle,DIP):
● 高層模塊不應(yīng)該依賴低層模塊,兩者都應(yīng)該依賴于抽象接口;
● 抽象接口不應(yīng)該依賴于細(xì)節(jié),細(xì)節(jié)應(yīng)該依賴抽象接口。
當(dāng)在分層架構(gòu)中使用依賴倒置原則時(shí),將會(huì)發(fā)現(xiàn)“不再存在分層”的概念了。無(wú)論是高層還是低層,它們都依賴于抽象接口,好像將整個(gè)分層架構(gòu)推平一樣。
其實(shí)從“Hello World”程序開始,我們就已經(jīng)在使用stdio.h包含的“抽象接口”了,即以后凡是用#include文件的擴(kuò)展名叫.h(頭文件)。如果源代碼中要用到stdio標(biāo)準(zhǔn)輸入輸出函數(shù)時(shí),那么就要包含這個(gè)頭文件,比如,“scanf("%d",&i);”函數(shù),其目的是告訴編譯器要使用stdio庫(kù)。庫(kù)是一種工具的集合,這些工具是由其它程序員編寫的,用于實(shí)現(xiàn)特定的功能。盡管實(shí)現(xiàn)者無(wú)需關(guān)心用戶將如何使用庫(kù),且不會(huì)直接開放源代碼給用戶使用,但必須給用戶提供調(diào)用函數(shù)所需要的信息。顯然只要將頭文件開放給用戶,即可讓用戶了解接口的所有細(xì)節(jié),詳見程序清單 1.16。
程序清單1.16swap數(shù)據(jù)交換接口(swap.h)
1 #ifndef _SWAP_H
2 #define _SWAP_H
3 //前置條件:實(shí)參必須是int類型變量的地址
4 //后置條件:p1、p2作為輸出參數(shù),改變主調(diào)函數(shù)中相應(yīng)的變量
5 void swap(int *p1, int *p2);
6 //調(diào)用形式:swap(&a, &b)
7 #endif
其中,每個(gè)頭文件都指出了一個(gè)用戶可見的外部函數(shù)接口,主要包括函數(shù)名、所需的參數(shù)、參數(shù)的類型和返回結(jié)果的類型。其中,swap是庫(kù)的名字,程序清單 1.16(1~2)與(8)是幫助編譯器記錄它所讀取的接口,當(dāng)寫一個(gè)接口時(shí),必須包含#ifndef、#define和#ednif。#include行部分僅當(dāng)接口本身需要其它庫(kù)時(shí)才使用,它由標(biāo)準(zhǔn)的#include行組成。程序清單 1.16(6)接口項(xiàng)表示庫(kù)輸出的函數(shù)的原型、常量和類型等。不管你是否理解,這些行是接口的模板文件,這就是信息隱藏。
>>>4.前/后置條件
處理信息隱藏還涉及到另一個(gè)技術(shù),那就是使用前置條件和后置條件描述函數(shù)的行為。在編寫一個(gè)完整的函數(shù)定義時(shí),需要描述該函數(shù)是如何執(zhí)行計(jì)算的。但在使用函數(shù)時(shí),只需考慮該函數(shù)能做什么,無(wú)需知道是如何完成的。當(dāng)不知道函數(shù)是如何實(shí)現(xiàn)時(shí),就是在使用一種名為過(guò)程抽象的信息隱藏形式,它抽象掉的是函數(shù)如何工作的細(xì)節(jié)。計(jì)算機(jī)科學(xué)家使用“過(guò)程”表示任意指令集,因此使用術(shù)語(yǔ)過(guò)程抽象。過(guò)程抽象是一種強(qiáng)大的工具,使得我們一次只考慮一個(gè)而不是所有的函數(shù),從而使問(wèn)題求解簡(jiǎn)單化。
為了使描述更準(zhǔn)確,則需要遵循固定的格式,它包含兩部分信息:函數(shù)的前置條件和后置條件。前置條件就是調(diào)用該函數(shù)必須成立的條件,當(dāng)函數(shù)被調(diào)用時(shí),該語(yǔ)句給出要求為真的條件。除非前置條件為真,否則無(wú)法保證函數(shù)能正確執(zhí)行。在調(diào)用swap()函數(shù)時(shí),實(shí)參必須是int類型變量的地址,這是調(diào)用者的職責(zé)。通常在函數(shù)開始處檢查是否滿足?如果不滿足,說(shuō)明調(diào)用代碼有問(wèn)題,拋出一個(gè)異常。
后置條件就是該操作完成后必須成立的條件,當(dāng)函數(shù)調(diào)用時(shí),如果函數(shù)是正確的,而且前置條件為真,那么該函數(shù)調(diào)用將可以執(zhí)行完成。當(dāng)函數(shù)調(diào)用完成后,后置條件為真。如果不滿足后置條件,則說(shuō)明業(yè)務(wù)邏輯有問(wèn)題。
當(dāng)滿足調(diào)用swap()函數(shù)的前置條件時(shí),必須同時(shí)確保其結(jié)束時(shí)滿足它的后置條件,其后置條件是被調(diào)函數(shù)將返回值傳回主調(diào)函數(shù),改變主調(diào)函數(shù)中變量的值。
前后置條件不只是概括地描述函數(shù)的行為,聲明這些條件應(yīng)該是設(shè)計(jì)任何函數(shù)的第一步。在開始考慮某個(gè)函數(shù)的算法和代碼之前,應(yīng)該寫出該函數(shù)的原型,其中包括函數(shù)的返回類型、名稱和參數(shù)列表,最后緊跟一個(gè)分號(hào)。直接來(lái)自于用戶的輸入不能作為前置條件,通常前/后置條件都可以轉(zhuǎn)化為assert語(yǔ)句。編寫函數(shù)原型時(shí),應(yīng)該以注釋的形式描述該函數(shù)的前置條件和后置條件。
事實(shí)上,前置條件和后置條件在使用函數(shù)的程序員和編寫函數(shù)的程序員之間形成了一個(gè)契約,也就是為什么需要這個(gè)函數(shù)?接口通過(guò)前置條件和后置條件以契約的形式表達(dá)需求,承諾在滿足前置條件時(shí)開始,按照程序的流程運(yùn)行,系統(tǒng)就能到達(dá)后置條件。
雖然注釋是一種很好的溝通形式,但在代碼可以傳遞意圖的地方不要寫注釋。因?yàn)榇a解釋做了什么,再注釋也沒有什么用處,相反注釋要說(shuō)明為什么會(huì)這樣寫代碼?
>>>5. 開閉原則
接口僅需指明用戶調(diào)用程序可能調(diào)用的標(biāo)識(shí)符,應(yīng)盡可能地將算法以及一些與具體的實(shí)現(xiàn)細(xì)節(jié)無(wú)關(guān)的信息隱藏起來(lái),這樣用戶在調(diào)用程序時(shí)也就不必依賴特定的實(shí)現(xiàn)細(xì)節(jié)了。當(dāng)接口一旦發(fā)布后,也就不能改變了,因?yàn)楦淖兘涌趧?shì)必引起用戶程序的改變。如果此前定義的接口滿足不了需求,怎么辦?只能擴(kuò)展新的接口,但不能修改或廢除原有的接口,這就是“對(duì)修改關(guān)閉,對(duì)擴(kuò)展開放”的開閉原則(Open-Closed Princple,OCP)。顯然,依賴倒置原則更加精確的定義就是面向接口的編程,它是實(shí)現(xiàn)開閉原則的重要途徑。如果DIP依賴倒置原則沒有實(shí)現(xiàn),就別想實(shí)現(xiàn)對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉。
-
嵌入式
+關(guān)注
關(guān)注
5087文章
19145瀏覽量
306134 -
接口
+關(guān)注
關(guān)注
33文章
8645瀏覽量
151399
原文標(biāo)題:周立功:設(shè)計(jì)良好的程序接口需注意的5個(gè)事項(xiàng)
文章出處:【微信號(hào):ZLG_zhiyuan,微信公眾號(hào):ZLG致遠(yuǎn)電子】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論