我發現,面向對象設計,被忽視的太嚴重了。使用OOP語言,卻不是面向對象來設計。無非就是用類寫C程序而已,並且這種狀況還很常見。另外一種狀況是過分重視類,而忽視了對象,可是對象纔是根本。不過本文會以類型爲主,來介紹面向對象設計。
編程
面向對象是一個徹底不一樣於傳統軟件設計方式的理念,傳統軟件設計是以功能爲主體,以功能爲模塊,以功能爲目標。而面向對象不一樣,是以軟件運行中的組件爲主體,實現某個功能,只是剛好這些組件的交互能夠間接作到。這些組件,就是對象。用面向對象來設計軟件,實際上就是設計一系列的對象,使這些對象在運行中剛好能間接完成軟件要作的事。
設計模式
爲何,面向對象出現的時候,同時也會有類這種東西。由於類就是設計對象的一個方式,將對象劃分爲不一樣的類型,使用類來描述各類類型的對象。就像一個建築藍圖,有我的設計了一個大樓,畫成藍圖,因而,他能夠隨時根據這個藍圖去建大樓。數組
這是反向運用。編程語言
由於咱們是先認識到一個個的對象,纔開始有類型的概念。四條腿,一個長長的頭,和尾巴,很是能跑,咱們發現不少個這樣的對象,因而創建了一個類型:馬。後來又發現了狗、豬、牛、羊等等,咱們又發現了它們都有一個共同的特色:哺乳產子。因而咱們創建一個了類型:會哺乳的,這個類型包含全部是哺乳產子的對象。而後咱們又發現,還有另外一些東西:雞、鴨、鳥、魚等等,和會哺乳的不一樣,它們是下蛋產子、下卵產子。它們和哺乳產子的都有一個共同的特色:會動。因而咱們創建了一個類型:動物,這個類型包含了全部會本身動的對象。通過慢慢的發現,最終就有了如今的體系。函數
咱們還發現了,任何對象,都有兩個東西:行爲、屬性。鳥經常唱歌,這是鳥的行爲,鳥有各類大小、顏色、樣子,這是鳥的屬性。因此咱們的OOP語言,創建類型描述對象時,都會提供方法和成員變量,來對應行爲和屬性。this
你如今用的語言,可能就有一個root類型:object,這個類型其實就是『類型自己』,因此任何類型的對象,均可以視爲一個object類型的對象。咱們在認識了不少不少個對象,創建了不少不少個類型後,同時也開始了反向認識,就是類。從對象創建類型,是從下往上,重建類型劃分對象,是從上到下。spa
咱們用的語言,對於創建類型,有三大要素:封裝、細化、多態。好比在C++,咱們用class定義類型,就是封裝,此類型對象的行爲和屬性,都在class定義中。你創建一個類型:書,而後再創建幾個類型:簡書、帛書、線裝書、電子書等等,你必定會將這些類型歸爲書的子類型,這就是細化。你須要畫一個形狀,這個形狀能夠是矩形圓形亂七八糟型,那麼你只須要針對形狀類型來操做,好比在形狀類型中加上行爲:畫。其細化的類型:矩形圓形亂七八糟型都各自定義本身的畫行爲。你只須要得到一個由形狀細化的類型的對象就好了,調用它的行爲:畫,這就是多態。設計
在軟件設計中運用面向對象,其實就是創建一個類型系統,每個軟件都有一個本身的類型體系。咱們用類型來設計軟件,軟件在運行時,經過各類類型的對象來交互,從而剛好間接的完成要作的事。code
咱們在創建類型時,須要仔細的考慮細化問題。好比一個類型:human,而後咱們再細化出兩個類型:man、woman。對象
class human {}; class man : public human {}; class woman : public human {};
可是,這樣的細化,是有很大問題的。由於一個man對象,是會變成woman對象的,反之亦然。好比泰國的poy,你會把她當成男嗎?爲何?由於她如今就是女的。這個是會變的。而會變的,是屬性,而不是類型。
什麼叫屬性,一個物體的座標、一我的的名字、你錢包的充實度。這些就是屬性,是可變的。而類型不會變,馬屬於哺乳動物,鋼鐵屬於金屬,這些是不變的。
而性別,實際上是屬性,好比在動物界,不少類型的動物,會改變性別,有些還沒性別。咱們更應該,把性別做爲屬性,而不是類型。
class human { public: const string& getsex() {return this.sex;} void setsex(const string& newsex) {return this.sex;} private: string sex; };
這樣,你的類型系統就正常了。爲何呢,好比你設計一個通信錄啊、人際管理啊之類的軟件,你細化出man、woman的話,類型體系臃腫,不說,你光是新建一我的員時都得分析是創建man對象仍是woman對象,做爲屬性就不須要了,你只須要human a("man");就能夠了,他變性了,那麼a.setsex("woman");就能夠了。試想若是你不用屬性,而是用類型來作,又會是怎樣的場面?
因此,咱們在創建類型系統時,在細化時,須要分清楚,究竟是屬性,仍是子類型。
可變的,就是屬性。
不可變的,就是類型。
這裏,咱們分析的是,對象的行爲。咱們在設計一個類型時,如何合理的設計方法。
我經常看到一些,不正確的方法設計。方法沒有放到正確的位置,或者是多餘的方法,或者是不該該用方法而應該用函數。
好比,有人要設計一個看書軟件,因而創建這書這個類型:
class book { protected: string name; string author; };
而後,由於書是要翻的,因而他有了這樣的設計:
class book { public: void before_page(){if (this.cur_page > 0) {this.cur_page -= 1;} } void after_page(){if (this.cur_page < this.page_num) {this.cur_page += 1;} } protected: ... size_t cur_page; size_t page_num; };
看上去,彷佛沒什麼。再一看,好像確實沒問題。
可是。
這是一個錯誤的設計。
面向對象,每個對象都有着本身的行爲、屬性。咱們好好的分析,往上翻頁,往下翻頁,是誰的行爲。是書本身的行爲?就好比現實中咱們跟一本書講:翻到下一頁,而後這本書就特麼的本身動了?本身翻了?這已是個神話故事了。
翻書是誰的行爲,是看書者的行爲,看書者用手翻。而不是這本書本身翻。固然若是你真的看到了一本書本身翻,請必定要告訴我,我要膜拜神蹟。
那麼怎麼樣設計翻頁這個行爲呢?簡單,很是簡單。做爲看書者的方法,就能夠了。
class book { public: ... const string& page(size_t n); private: ... vector<string> pages; }; class reader { public: size_t page() {return this.cur_page;} void before_page(){if (this.cur_page > 0) {this.cur_page -= 1;} } void after_page() {if (this.cur_page < this.page_num) {this.cur_page += 1;} } private: size_t cur_page; }; class con { public: void render () {cout << cur_book.page(cur_reader.page()); << endl;} private: reader cur_reader; book cur_book; }; // 上面這樣的設計還帶來了一個額外的好處:下降耦合。
書是什麼對象,就是一個信息對象,只須要有屬性,書名、做者、每一個頁的文本等等,就這些。固然咱們須要用方法,來包裝屬性,這屬於軟件工程中很重要的一個手段。在這樣的狀況下,book.page(),並非書的行爲,而只是對屬性的包裝。
翻書,是看書者是行爲,因此,放在看書者的類型裏,最合適。
可是我爲何不我直接把cur_book放到reader裏,做爲屬性呢?由於看書者和書,是兩個不一樣的東西,咱們拿起一本書,只是和這本書創建起一個暫時的聯繫。就像朋友問你,絕對不會問:「你的當前書是什麼啊」,這是一種屬性的問法,而只是會問:「你如今在看什麼書啊」,這是聯繫的問法。看書者的屬性,都是聯繫性的,好比書名啊,當前的頁碼啊等等。咱們不須要放cur_book到裏面,可是咱們能夠放一個string book_name到裏面,做爲reader的屬性。
再看另外一個例子,這個不一樣了,這是一個普遍被採用的錯誤例子!
class a : public object { public: a* clone() {return new a();} }; class b : public object { public: b* clone() {return new b();} }; ... object* src = ... // new a or b object* k = src->clone(); ...
對,克隆。一個很是普遍的類型方法設計。這是運用多態來克隆對象,咱們不須要管被克隆的是什麼對象,只要有clone方法,我就讓你克隆,剋死你。
咱們仔細分析一下,問題在哪裏,在哪裏。
那就是不可能。
這個和上面的不一樣了,上面的book的page,只是屬性的包裝。而這個是真正的行爲了,是純動詞了,連名詞都沒了。這就是對象的行爲了,是一個類型的方法了。
可是,這不可能,這個對象不可以這樣行動。
好比科學家克隆一隻羊,若是用這個被普遍採用的方式,那就是這樣的場面:」誒,那隻羊,快點,克隆一個新的你,別墨跡啊。「
滑稽嗎,好笑嗎?
實際上,科學家是怎麼克隆的呢?是科學家用各類手段,來複制這隻羊。也就是說,克隆,是科學家的行爲,而不是那隻羊,被克隆者的行爲。哪怕這隻羊精通天文地理物理數學,啊,這樣的話或許它還真能本身克隆本身,固然前提是它有各類設備能夠用,若是你發現了這樣的一隻羊,請必定告訴我,我將膜拜。
固然不要被上面這段話誤導,這實際上一個執行者和目標對象的關係,克隆由科學家執行,執行者是科學家,目標對象是羊。跟這隻羊精不精通天文地理物理數學不要緊,若是他能本身克隆本身,那麼執行者就是羊,目標對象是它自己,行爲是克隆。
那麼咱們應該如何設計,克隆呢?
那就是分析出誰是執行者,放到執行者裏面。
class scientist { public: sheep* clone(sheep* target); dog* clone(dog* target); pig* clone(pig* target); money* clone(money* target); // 其實money是一種動物 :) }; ... secientist sei; sheep s; animal* k = sei.clone(&s);
有些時候,咱們分析不出執行者,或者實在懶得分析,那麼做爲函數就好了
class string : public object {}; class vector : public object {}; // 你可能須要使用friend來鏈接這些函數 string* clone(string* target); vector* clone(vector* target); ... string s(); object* k = clone(&s);
咱們有沒有在現實中看到能本身克隆本身的東西呢?本身能克隆本身,簡直是神蹟啊。
還真的有能本身克隆本身的東西:病毒。
這是題外話。
因此,雖然我說被普遍採用的clone方式是個錯誤的設計,但指的是語義上的錯誤,在軟件設計中,其實那是正確的設計。病毒能本身克隆本身,那麼計算機裏的字符串、數組能本身克隆本身,有什麼好稀奇的。難以想象,計算機原本就難以想象。既然能本身克隆本身,那麼clone做爲這個對象的行爲,並沒什麼問題。
面向對象,面向的,是對象。
那麼什麼是面向對象設計?在面向對象的領域中,有幾個方面:OOA(面向對象分析)、OOD(面向對象設計)、OOP(面向對象編程)。
首先是面向對象分析,咱們須要弄清楚,軟件中須要有哪些對象,這些對象是什麼關係,要作的是什麼。注意,是對象,而不是類型。就好比一個看書軟件,有哪些對象?好比咱們能夠發現,有書、看書者,還有界面、用戶設置等等對象。
而後是面向對象設計,咱們就是分析出的對象爲主體,圍繞這些對象,來設計軟件。而不是圍繞功能,圍繞功能去設計軟件的,就不是面向對象。面向對象只能是以對象爲主體,什麼亂七八糟功能,都只是這些對象在交互中,剛好間接的完成了而已。模塊的劃分,大概就體現出了,你是否是面向對象。是面向對象的話,基本上每一個模塊都對應着一個對象。固然不是Java那種,Java是以類型劃分模塊,而不是以對象劃分模塊。
簡單的說,只要你的設計是以對象主的,那麼就是面向對象設計。
FILE* f = fopen("a.txt", "rb"); const char* str = "hello world!\n"; fwrite(str, strlen(str), 1, f); fclose(f);
C語言不是OOP語言,但C語言是OOD語言,C語言的不少地方,都體現出了面向對象,好比fopen系列,數學運算系列,字符串系列等等。
在這裏,f就是一個文件對象,fopen創建這個對象,fwrite將數據寫到一個文件對象裏,fclose關閉一個文件對象。fwrite不須要管你打開是哪一個文件,只要能寫,它就給你寫。fclose無論你打開的是什麼文件,反正你傳入一個文件對象,就給你關了。fopen系列,圍繞着文件對象,也就是FILE*類型的對象,這就是面向對象設計。在這個,你打開a.txt文件,而後寫入hello world!,都是經過文件對象間接完成的。fopen/fread/fwrite/fclose/···就是文件對象的方法,FILE*結構裏的東西,就是文件對象的屬性。
有一個更大更好的例子,就是Windows API的HANDLE,幾乎全部的函數,都是圍繞HANDLE作事的。一個HANDLE對象,要麼是最上層的HANDLE類型,要麼是HMENU、HMODULE等更細化的類型。你不用知道這些HANDLE都是什麼鬼樣子,反正你只要用HANDLE對象來作事就好了,Windows API,就是圍繞着HANDLE對象作事的,這就是面向對象設計,並且是最純粹的。
面向對象設計,和什麼語言是無關的。由於這是設計,是體現出現的,而不是具體的樣子。咱們能夠在任何語言中運用面向對象設計。
但面向對象編程和語言是有關的。
區別到底在哪裏?區別在於,面向過程以功能爲主,以目標爲主。好比上面的a.txt例子,用面向過程去作,就是這樣。
OpenFile("a.txt"); const char* str = "hello world!\n"; Write(str, strlen(str), 1); CloseFile();
在這裏,就是純粹的實現功能,打開a.txt文件,而後寫那行字,而後關了。沒有什麼文件對象。在劃分模塊時也是以功能爲主:OpenFile(打開文件)/Write(寫東西到文件)/CloseFile(關閉文件)
用面向對象方式,就是以對象爲主,這裏有文件對象,因此咱們創建一個文件對象,圍繞這個對象,來間接完成要作的事。看起來是否是區別小?實際上,天壤之別。
光有面向對象設計是不夠的,咱們還須要有在骨子裏就支持面向對象設計的語言,就是咱們如今常見的,OOP語言。這些語言,能使你更天然的設計對象,提供封裝、細化、多態來讓你能更好的運用對象。
面向對象不包括封裝、細化、多態。
面向對象編程纔有封裝、細化、多態。
爲何?由於面向對象,主體是對象,實際上是無論類型的。然而咱們須要管類型,因此面向對象編程,纔會提供封裝、細化、多態給咱們,就是讓咱們能有一套手段來管理不一樣的對象。沒有這些,咱們就難以實現面向對象。
爲何str.size()對比strlen(str)是一個巨大的進步,由於咱們有了強力的手段,來實現面向對象。面向對象早就有了,可是沒有提高軟件設計領域的水平,而面向對象編程語言的出現,才提高了軟件設計領域的水平。
可是,雖然咱們有了OOP語言,卻依然會不知不覺的偏離面向對象,致使用類寫C程序,用對象來直接完成功能的出現。
這是由於沒有弄清楚對象,沒有設計好對象。
另外一個偏離是,過分重視類型,重視class,而忽略了對象。好比有些編程者,差很少把面向對象變成了面向設計模式,代碼中全是設計模式,而最重要的,最核心的對象,都被淹沒在各類模式中。
因此,整個面向對象領域中,有三大元素:
面向對象分析
面向對象設計
面向對象編程
缺一不可
在C語言,咱們都會運用面向對象設計,到了OOP語言,就更應該面向對象設計。不要想着功能,不要想着目標,而是對象的交互剛好間接的實現用戶的需求,剛好、間接,這是兩個很重要很重要很重要很重要的概念。主體是對象,用類型來設計對象。用這些對象的交互來剛好間接實現功能,不能直接實現功能,不能。
在結尾的最後,我給你們說一個目前大部分OOP語言的一個缺陷,就是過分重視類型。好比在某個對象只須要有一個實體時就出問題了,設計模式中介紹的單例模式是有極其嚴重的BUG的。爲何,由於在這種狀況下,類型都不是必須的,不少語言都着歎爲觀止的類型設計手段,而沒有一個設計單對象的方式。只有一個對象,意味着什麼封裝、細化、多態都不須要了,必定要創建一個類型,而後用各類詭異、莫名其妙的手段來限制只能創建一個實體對象,是自尋煩惱。面向對象,核心是對象,而不是類型。