一文帶你輕松掌握多種編程范式
- 前言
- 結構化程式的設計
- 基于物件的程式設計
- 面向物件的程式設計
- 基于介面的程式設計
- 基于介面編程的模板實作
前言
?編程范式有多種,主要有結構化的程式設計思想、基于物件的程式設計思想、面向物件的程式設計思想、基于介面的程式設計思想,那么這些范式各是什么意思呢?別著急,我們通過一個加法器的例子來逐一說明,
結構化程式的設計
?我們來實作一個加法器,在這個加法器中已經保存了被加數,現在需要傳遞加數到加法器,如果你是一個C語言開發,第一反應多半是,這個很簡單啊,用一個結構體來保存被加數,然后再外帶一個加法函式就行了啊

?代碼中,結構體Augend保存了加法器的被加數,具體而言,就是由iAugend保存,第9至12行給出了加法函式的定義,該函式接收兩個引數,一是Augend結構體的指標,二是加數iAddend,
? 但這個時候老板來了,他對你說,這個加法器要修改一下,現在需要給被加數添加一個權重值,而且以前的加法器要保留,因為還有一部分代碼要保留它,沒辦法,拿人家的手短,吃人家的嘴軟,繼續當“碼農”吧,既然有一部分代碼要用到老的加法器,那么老的加法器我們還是保留的,這樣一來,就可以按照新的思路來開發新的加法器了,具體的方法如下

?可以看到代碼思路同上一個代碼是完全一致的,不同的只是結構體和函式名稱,很顯然,WeightAugend保存了被加數和權重,而WeightAdd則是帶權重的加法函式,好了,現在我們分析一下按照結構化程式設計思想實作的加法器有什么缺陷?學過面向物件的肯定會一口就能說出來,資料和操作這個資料的函式或方法沒有封裝在一起,確切一點就是,這個加法器沒有把被加數、權重以及操作它們的加法運算封裝在一起,另外一個缺陷是什么呢?因為引入帶權重的加法器之后,需要對部分老代碼進行修改,顯然沒有做到代碼封閉,即沒有實作這一變化點的封裝,
基于物件的程式設計
?在物件的世界,任何東西都可以被當成物件,那么按照這個說法,我們需要實作的這個加法器,顯然也是個物件了,用過C++的同學第一反應肯定就是撰寫一個加法器的類,用一個資料成員保存被加數,然后再寫一個public的加法方法就好了,一般就寫成如下這樣

?為了防止隱式型別轉換在這里面使用了explicit,同樣的,故事還沒有完,需要實作帶權重的加法運算,但是仍用老代碼來實作Adder類,沒辦法了,我們只有再實作一個帶權重的加法器,依葫蘆畫瓢,WeightingAdder類出爐了,類宣告如下圖:

?WeightingAdder類提供了兩個資料成員以分別保存被加數和權重,并提供了帶權重的Add方法,現在我們來比較一下基于物件方法和結構化的差異,那就是封裝,資料和操作這個資料的方法被封裝在了一起,那么在這里,便是被加數或權重、加法運算或帶權重的加法運算,被封裝在了類Adder或WeightingAdder中,只要實體化一個物件來,你就能使用其加法方法,
?一個實際專案中,往往都會有很多的.h檔案和.c檔案來有效組織代碼,如果按照這個思路,通常我們會把結構體Augend的定義放在一個.h檔案中,而Add函式的定義和宣告,則分別放在了一個.c和.h檔案中,這會給加法器的使用者和實作著,帶來一點小麻煩,什么麻煩呢?加法者的使用者,需要不停地查看Augend的頭檔案和Add函式的頭檔案,因為他需要使用結構體的成員,呼叫Add函式;而實作者,也需要不斷地查看Augend頭檔案,如果結構體成員、操作它的函式比較多的話,那這個麻煩比較大,怎么辦呢?很自然地想法就是把Augend結構體和Add函式放在一起好了,這樣管理也比較方便,

?上圖代碼中,使用結構體Adder代表加法器,它包含了兩個欄位,一是被加數iAugend,另一個則是函式指標,該函式指標需要指向實際的加法運算函式,顯然這樣一來,就把結構化程式中的結構Augend和Add函式放在了一起,那么這個加法器的使用如下,

?在該代碼中,首先24行實體化了adder出來,然后,將Add函式的地址,賦予了pFuncAdd欄位,并進行了被加數的初始化操作,而地29行,則通過pFuncAdd欄位呼叫了加法函式,其實就已經有點像成員函式呼叫了,adder就是物件,pFuncAdd就是成員函式了,但是隨著這種組織結構體和它的操作函式的方式的廣泛應用,很快就會發現一個問題,太麻煩了!只要有一個函式指標欄位,通常就得定義一個函式指標型別,就得在結構體的每一個實體創建后完成函式指標的賦值操作,即系結某個具體的函式,不僅太麻煩還容易出錯,怎么辦呢?交給編譯器算了,把函式的定義或宣告都放在結構體里面,編譯器自己決議,結構體實體創建后,讓編譯器自動系結函式,
?基于物件的方法,同結構化方法的差異,基于物件的方法能夠實作封裝,而這正是結構化方法所欠缺的,之前結構化程式設計思想是分析過,當新增帶權重的加法器時,破壞了代碼的封閉性,即沒能封裝這一變化點,但是,基于物件的思想也不能封裝這一變化點,原本使用的Adder類的老代碼,若要改成使用WeightingAdder類,則必須修改物件創建時的型別、引數傳遞時的型別等,這顯然破壞了代碼的封閉性,由此可見,基于物件的方法,僅在結構化方法的基礎上,彌補了封裝的缺陷,但依然遺留了變化點不能很好封裝的問題,
面向物件的程式設計
?基于物件和面向物件的區別是什么呢?從技術實作角度上來將,很簡單,面向物件多了繼承和虛函式,而這又有什么好處呢?先上代碼

?同之前的類相比大致相同,其區別主要是下面幾個方面:
- Add函式變成了虛函式,這個意圖很明顯,就是希望派生類重寫它
- m_iAugend資料成員訪問權限變成了protected,這也是保證派生類能訪問它
- 增加了一個虛解構式,為什么增加呢?因為如果Adder沒有虛解構式,當delete一個型別為Adder的物件指標,但該指標實際指向的其派生類物件時,則派生類的解構式不會被呼叫,
? 面向物件版本的Adder寫好了,但是老板說了還需要寫帶權重的加法運算,不過思路清晰多了,讓WeightingAdder從Adder派生吧,代碼如下

?很簡單,主要是重寫了虛函式Add,以及引入了權重資料成員,事情看起來已經很完美了,不僅資料和方法封裝在一起,而且代碼的封閉性也做到了,真正是這樣嗎?當然不是,請大家繼續往下看,
基于介面的程式設計
?假設現在需求又改變了,要求普通加法器的被加數必須是非負整數,而帶權重的加法器的被加數,可以是非負的整數,也可以是負的整數,現在Adder類的被加數m_iAugend是int型別的,沒法保障一定是非負的整數,如果把它改成unsigned int,對帶權重的加法器而言,又不能滿足其需求,怎么辦呢?其實一致一來,都存在一個誤區,總是認為應該在普通加法器的基礎上擴展帶權重的加法器,實際上,這兩者除了都是加法器外,大家沒有繼承關系,都是平等的,所以在這里可以考慮這兩個類是兄弟,都是同一個基類加法器,該加法器都提供了加法運算,也就是Add函式,代碼如下

?實際上,IAdder是一個介面類,它規定了要成一個加法器所需要實作的介面,即純虛函式Add,顯然Adder類和WeightingAdder類都需要從IAdder類派生,并重寫Add函式,下圖是這兩個類的實作


?Adder類的被加數m_iAugend是unsigned int型別,滿足被加數為負整數的要求,而這并不影響WeightingAdder類的被加數,同面向物件的方法相比,都封裝資料和操作資料的函式,其次,兩類方法都能封裝既有普通加法器,又有帶權重加法器這一變化點,簡要說明基于介面的方法如何做到這一點的,
void func(IAdder *pAdder)
{
...
int i = pAdder->Add(5);
}
?同上一節類似,函式func接受IAdder型別的指標,并它通過Adder方法,因此,無論是想使用Adder類的物件還是WeightingAdder類的物件,都只需要向func函式傳遞指標即可,顯然,func函式不用修改一句代碼就能既用Adder類,又使用WeightingAdder類,達到了封裝變化點的要求,但是遇到“普通加法器的被加數,必須是非負的整數,而帶權重的加法器的被加數可以是非負整數,也就是負整數”這一變化點時,面向物件的方法出問題了,根本原因在于繼承,

?上圖左邊的類圖是面向物件的類圖,可以看到,面向物件中,WeightingAdder類從Adder類繼承而來,繼承關系是一種強耦合關系,這表現在派生類既耦合于基類的介面,又耦合于基類的實作,而基于介面的設計中,Adder類和WeightingAdder類從父子關系變成了兄弟關系,均從IAdder繼承而來,都是繼承關系,為什么它能解決面向物件方法所不能解決的問題呢?因為此繼承非彼繼承,雖然在基于介面的方法中用到了繼承,但是不是強耦合關系,Adder類和Weighting類都繼承于基類IAdder類,顯然它們都會耦合于基類的介面和實作,但問題是IAdder類實質只有一個純虛函式Add,并沒有任何實作細節,甚至連資料成員都沒有,因此耦合于其實作的說法是沒有意義,所以其耦合度低于面向物件的類結構,這要是基于介面的方法,能封裝型別變化點的原因,
基于介面編程的模板實作
?前面已經利用虛函式,實作了加法器的基于介面的版本,而在本節中,我們利用模板來實作加法器的這一版本,下面直接上代碼

?代碼中可以看到,同之前介面類相比多了一個template的帽子,變成了一個類模板,嗯,或者是模板類,類模板和模板類的區別在這里強調一下,**模板類實際上是類模板實體化的一個產物,**很顯然,這里IAdder是個類模板,只要給它不同的模板引數T,就能實體化若干模板類出來,在IAdder實作中,最關鍵的一點就是第19行和第20行,將this指標強轉成型別T的指標,然后再呼叫AddImpl函式,那么這個型別T是什么?AddImpl又從何而來呢?請繼續往下看代碼,


?上圖中分別給出了普通加法器Adder和帶權加法器WeightingAdder的實作;并且在這兩個類中,都給出了函式AddImpl,從這兩個函式可以看出,它們實作了具體的加法運算,IAdder類模板中的Add函式,呼叫了型別T的AddImpl函式,看來這個AddImpl函式同Adder或WeightingAdder中的AddImpl函式有點關系,什么關系呢?我們通過模板半數演繹來講解,
假設按照下面方式使用加法器
Adder add(5);
adder.Add(4);
?第一行我們實體化了Adder類的一個物件出來,而Adder類的基類即IAdder< Adder>,此時模板引數T實際上就是Adder,而物件adder呼叫Add函式時,Add函式的代碼相當于
int Add(int iAddend)
{
Adder *pThis = (Adder*)(this);
pThis->AddImpl(iAddend);
?可以看出,上述代碼中的T被Adder替換掉了,因為此時模板引數T就是Adder,代碼中將this指標,強轉成Adder型別的指標,那么,這個轉換安全嗎?當然,當前的物件即adder,其型別是Adder,因此此時的this指標實際指向的就是Adder類的物件,自己轉成自己,當然是安全的,這種方法能封裝變化點嗎?先看個例子
template<typename T>
void f(IAdder<T> *pAdder)
{
std::cout << pAdder->Add(4) << std::endl;
}
?當出現需求變化時,例如要增加帶權重的加法器時,即可從IAdder中派生一個WeightingAdder類,并讓模板T等于WeightingAdder,對于上面的f函式而言,若要使用Adder加法器,給它傳遞Adder的物件指標即可,同樣,若要使用WeightingAdder加法器,給它傳遞WeightingAdder物件的指標即可,顯然,f函式的代碼不用做任何修改,因此也實作了變化點,
? 基于模板這種方法所實作的多型,同之前利用虛函式實作多型的區別,主要在于前者是靜態的,由編譯器確定,模板是動態的,運行期才能確定,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/234833.html
標籤:AI
