您好,登錄后才能下訂單哦!
面向?qū)ο蟮娜筇匦裕悍庋b、繼承和多態(tài)。這是任何一本面向?qū)ο笤O(shè)計(jì)的書里都會介紹的,但鮮有講清楚的,新手看了之后除了記住幾個(gè)概念外,并沒真正了解他們的意義。前幾天在youtube上看了Bob大叔講解的SOLID原則,其中有一段提到面向?qū)ο蟮娜筇匦?,收獲很多,但是我并不完全贊同他的觀點(diǎn),這里談?wù)勎业南敕ǎ?/p>
封裝
『封裝』第一層含義是信息隱藏。這是教科書里都會講解的,把類或模塊的實(shí)現(xiàn)細(xì)節(jié)隱藏起來,對外只提供最小的接口,也就是所謂的『最小知識原則』。有個(gè)共識,正常的程序員能理解的代碼在一萬行左右。這是指在理解代碼的實(shí)現(xiàn)細(xì)節(jié)的情況下,正常的程序員能理解的代碼的規(guī)模。比如一個(gè)文件系統(tǒng),F(xiàn)AT、NTFS、EXT4和YAFFS2等,它們的實(shí)現(xiàn)是比較復(fù)雜的,少則幾千行代碼,多則幾萬行,要理解它們的內(nèi)部實(shí)現(xiàn)是很困難的,但是如果屏蔽它們的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),只是要了解它們對外的接口,那就非常容易了。
關(guān)于『封裝』的這一層含義,Bob大叔提出了驚人的見解:『封裝』不是面向?qū)ο蟮奶匦?,面向過程的C語言比面向?qū)ο蟮腃++/Java在『封裝』方面做得更好!證據(jù)也是很充分:C語言把函數(shù)的分為內(nèi)部函數(shù)和外部函數(shù)兩類。內(nèi)部函數(shù)用static修飾,放在C文件中,外部函數(shù)放在頭文件中。你完全不知道內(nèi)部函數(shù)的存在,即使知道也沒法調(diào)用。而像在C++/Java中,通過public/protected/private/friend等關(guān)鍵字,把函數(shù)或?qū)傩苑殖刹煌牡燃?,這把內(nèi)部的細(xì)節(jié)暴露給使用者了,使用者甚至可以繞過編譯器的限制去調(diào)用私有函數(shù)。所以在信息隱藏方面,『封裝』不但不是面向?qū)ο蟮奶匦?,而且面向?qū)ο鬁p弱了『封裝』。
『封裝』的第二層含義是把數(shù)據(jù)和行為封裝在一起。我覺得這才是面向?qū)ο笾械摹悍庋b』的意義所在,而一般的教科書里并沒提及或強(qiáng)調(diào)。面向過程的編程中,數(shù)據(jù)和行為是分離的,面向?qū)ο蟮木幊虅t是把它們看成一個(gè)有機(jī)的整體。所以,從這一層含義來看,『封裝』確實(shí)是面向?qū)ο蟮摹禾匦浴弧?/p>
面向?qū)ο笫且环N思維方式,而不是表現(xiàn)形式。在C語言中,可以實(shí)現(xiàn)面向?qū)ο蟮木幊?,事?shí)上,幾乎所有C語言開發(fā)的大型項(xiàng)目,都是采用了面向?qū)ο蟮乃枷腴_發(fā)的。把C語言說成面向過程的語言是不公平的,是不是面向?qū)ο蟮木幊讨饕强粗笇?dǎo)思想,而不是編程語言。你用C++/Java可以寫面向過程的代碼,也可以用C語言寫面向?qū)ο蟮拇a。
繼承
類就是分類的標(biāo)準(zhǔn),也就是一類事物,一類具有相同屬性和行為對象的抽象。比如動物就是一個(gè)類,它描述了所有具有動物這個(gè)屬性的事物的集合。狗也是一個(gè)類,它具有動物所有的特性,我們說狗這個(gè)類繼承了動物這個(gè)類,動物是狗的父類,狗是動物的子類。在C語言中也可以模擬繼承的效果,比如:
struct Animal { ... };
struct Dog { struct Animal animal; ... }
struct Cat { struct Animal animal; ... }
因?yàn)镃語言也可以實(shí)現(xiàn)『繼承』,所以Bob大叔認(rèn)為『繼承』也不算不上是面向?qū)ο蟮摹禾匦浴弧5俏矣X得,C語言中實(shí)現(xiàn)『繼承』的方式,需要用面向?qū)ο蟮乃季S來思考才能理解,否則純粹從數(shù)據(jù)結(jié)構(gòu)的方式來看上面的例子,理解起來就會大相徑庭:animal是Dog的一個(gè)成員,所以Animal可以看成是Dog的一部分!Is a 變成了has a。只有在面向?qū)ο蟮乃枷胫?,說『繼承』才有意義,所以說『繼承』是面向?qū)ο蟮摹禾匦浴徊⒉粻繌?qiáng)。
在C語言里實(shí)現(xiàn)多重繼承更是非常麻煩了,記得glib里實(shí)現(xiàn)了接口的多重繼承,但是用起來還是挺別扭的,對新手來說更是難以理解。多重繼承在某些情況下,會對編譯器造成歧義,比菱形繼承結(jié)構(gòu):A是基類,B和C是它的兩個(gè)子類,D從B和C中繼承過來,如果B和C都重載了A的一個(gè)函數(shù),編譯器此時(shí)就沒法區(qū)分用B的還是C的了(當(dāng)然這是可以解決的)。
像Bob大叔說的,Java沒有實(shí)現(xiàn)多重繼承,并不是多重繼承沒有用。而是為了簡化編譯器的實(shí)現(xiàn),C#沒有實(shí)現(xiàn)多重繼承,則是因?yàn)镴ava沒有實(shí)現(xiàn)多重繼承:)
除了接口多重繼承是必不可少的,類的多重繼承在現(xiàn)實(shí)中也是很常見的。比如:狼和狗都是狗科動物的子類,貓和老虎都是貓科動物的子類。狗科動物和貓科動物都是動物的子類。但是貓和狗都是家畜,老虎和狼都是野生動物。貓不但要繼承貓科動物的特性,還繼承家畜的特性。類就是分類的標(biāo)準(zhǔn),而混用不同的分類標(biāo)準(zhǔn)是多重繼承的主要來源。多重繼承可以用其他方式實(shí)現(xiàn),比如traits和mixin。
不管是普通繼承,接口繼承,還是多重繼承,在面向?qū)ο蟮木幊陶Z言中,實(shí)現(xiàn)起來要更加容易和直觀,在面向過程的語言中,雖然可以實(shí)現(xiàn),但是比較丑陋,而且本質(zhì)是面向?qū)ο蟮乃伎挤绞?。所以『繼承』應(yīng)該稱得上是面向?qū)ο蟮摹禾匦浴涣恕=橛诶^承帶來的復(fù)雜性,現(xiàn)代面向?qū)ο蟮脑O(shè)計(jì)中,都推薦用組合來代替繼承實(shí)現(xiàn)重用。
多態(tài)
『多態(tài)』本來是面向?qū)ο笏枷胫凶钪匾男再|(zhì)(當(dāng)然也算不上是特有的性質(zhì)),但是教科書里都只是介紹了『多態(tài)』的表現(xiàn)形式,而沒有介紹它用途和價(jià)值?!憾鄳B(tài)』一般表現(xiàn)為兩種形式:
允許不同輸入?yún)?shù)的同名函數(shù)存在。這個(gè)性質(zhì)會帶來一定的便利,特別是對于構(gòu)造函數(shù)和操作符的重載。但這種『多態(tài)』是在編譯時(shí)就確定了的,所以只能算成一種語法糖,并沒有什么特別的意義。
子類可以重載父類中函數(shù)原型完全相同的同名函數(shù)。如果只看它的表現(xiàn)形式,在父類中存在的函數(shù),在不同的子類中可以被重新實(shí)現(xiàn),這看起來是吃飽了撐著。但是這種『多態(tài)』卻是軟件架構(gòu)的基礎(chǔ),幾乎所有的設(shè)計(jì)模式和方法都依賴這種特性。
隔離變化是軟件架構(gòu)設(shè)計(jì)的基本目標(biāo)之一,接口正是隔離變化最重要的手段。我們經(jīng)常說分離接口與實(shí)現(xiàn),針對接口編程,主要是因?yàn)榻涌诳梢愿綦x變化。如果沒有第二種『多態(tài)』,就沒有真正意義上的接口。面向?qū)ο笾械慕涌?,不僅是指模塊對外提供的一組函數(shù),而且特指在運(yùn)行時(shí)才綁定具體實(shí)現(xiàn)的一組函數(shù),在編譯時(shí)根本不知道這組函數(shù)是誰提供的。我們先把接口簡單的理解為,在基類中定義一組函數(shù),但是基類并沒有實(shí)現(xiàn)它們,而在具體的子類中去實(shí)現(xiàn)。這不就是『多態(tài)』的第二種表現(xiàn)形式么。
接口怎么能夠隔離變化呢?Bob大叔舉了一個(gè)非常好的例子:
#include <stdio.h> int main() { int c; while((c = getchar()) != EOF) { putchar(c); } return 0; }
這個(gè)程序和Hello world是一個(gè)級別的,你從鍵盤輸入一個(gè)字符,它就顯示一個(gè)字符。但是它卻蘊(yùn)含了『多態(tài)』最精妙的招式。比如說輸入吧,getchar是從標(biāo)準(zhǔn)輸入(STDIN)讀入一個(gè)字符,鍵盤輸入是缺省的標(biāo)準(zhǔn)輸入,但是鍵盤輸入只是眾多標(biāo)準(zhǔn)輸入(STDIN)中的一種。你可以從任何一個(gè)IO設(shè)備讀取數(shù)據(jù):從網(wǎng)絡(luò)、文件、內(nèi)存和串口等等,換成任何一種輸入,這個(gè)程序都不需要任何改變。
具體實(shí)現(xiàn)變了,調(diào)用者不需要修改代碼,而且它根本不用重新編譯,甚至不用重啟應(yīng)用程序。這就是接口的威力,也是『多態(tài)』的功勞。
上面的程序是如何做到的呢?IO設(shè)備的驅(qū)動是一套接口,它定義了打開、關(guān)閉、讀和寫等操作。對實(shí)現(xiàn)者來說,不管數(shù)據(jù)從哪里來,要到哪里去,只要實(shí)現(xiàn)接口中定義的函數(shù)即可。對使用者來說,完全不同關(guān)心它具體的實(shí)現(xiàn)方式。
『多態(tài)』不但是隔離變化的基礎(chǔ),也是代碼重用的基礎(chǔ)。公共函數(shù)的重用是有價(jià)值的,在面向過程的開發(fā)中也很容易做到這種重用。但現(xiàn)實(shí)中的重用沒那么簡單,就連一些大師也感嘆重用太難。比如,你可能需要A這個(gè)類,你把它拿過來時(shí),發(fā)現(xiàn)它有依賴B這個(gè)類,B這個(gè)類有依賴C這個(gè)類,搞到最后發(fā)現(xiàn),它還依賴一個(gè)看似完全不相關(guān)的類,重用的念頭只好打住。如果你覺得夸張了,你可以嘗試從一個(gè)數(shù)據(jù)庫(如sqlite)中,把它的B+樹代碼拿出來用一下。
在『多態(tài)』的幫助下,情況就會大不相同了。A這個(gè)類依賴于B這個(gè)類,我們可以把B定義成一個(gè)接口,讓使用A這個(gè)類的使用者傳入進(jìn)來,也就是所謂的依賴注入。如果你想重用A這個(gè)類,你可以為它定制一個(gè)B接口的實(shí)現(xiàn)。比如,我最近在一個(gè)只有8K內(nèi)存的硬件上,為一塊norflash寫了一個(gè)簡單的文件系統(tǒng)(且看作是A類),如果我直接去調(diào)用norflash的API(且看作是B類),就會讓文件系統(tǒng)(A類)與norflash的API(B類)緊密耦合到一起,這就會讓文件系統(tǒng)的重用性大打折扣。
我的做法是定義了一個(gè)塊設(shè)備的接口(即B接口):
typedef unsigned short block_num_t; struct _block_dev_t; typedef struct _block_dev_t block_dev_t; typedef block_num_t (*block_dev_get_block_nr_t)(block_dev_t* dev); typedef bool_t (*block_dev_read_block_t)(block_dev_t* dev, block_num_t block_num, void* buff); typedef bool_t (*block_dev_write_block_t)(block_dev_t* dev, block_num_t block_num, const void* buff); typedef void (*block_dev_destroy_t)(block_dev_t* dev); struct _block_dev_t { block_dev_get_block_nr_t get_block_nr; block_dev_write_block_t write_block; block_dev_read_block_t read_block; block_dev_destroy_t destroy; };
在初始化文件系統(tǒng)時(shí),把塊設(shè)備注入進(jìn)來:
bool_t sfs_init(sfs_t* fs, block_dev_t* dev);
這樣,文件系統(tǒng)只與塊設(shè)備接口交互,不需要關(guān)心實(shí)現(xiàn)是norflash、nandflash、內(nèi)存還是磁盤。而且?guī)韼讉€(gè)附加好處:
可以在PC上做文件系統(tǒng)的單元測試。在PC上,用內(nèi)存模擬一個(gè)塊設(shè)備,文件系統(tǒng)可以正常工作了。
可以通過裝飾模式為塊設(shè)備添加磨損均衡算法和壞塊管理算法。這些算法和文件系統(tǒng)都可以獨(dú)立重用。
『多態(tài)』讓真正的重用成為可能,沒有『多態(tài)』就沒有各種框架。在C語言中,多態(tài)是通過函數(shù)指針實(shí)現(xiàn)的,而在C++中是通過虛函數(shù),在Java中有專門的接口,在JS這種動態(tài)語言中,每個(gè)函數(shù)是多態(tài)的。『多態(tài)』雖然不是面向?qū)ο蟮摹禾赜械摹粚傩?,但是面向?qū)ο蟮木幊陶Z言讓『多態(tài)』更加簡單和安全。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對億速云的支持。如果你想了解更多相關(guān)內(nèi)容請查看下面相關(guān)鏈接
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。