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