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