面向對象設計與面向對象編程

我發現,面向對象設計,被忽視的太嚴重了。使用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做爲這個對象的行爲,並沒什麼問題。


因此咱們創建類型系統,是以『如今』爲參照的,對於『如今』是不變的。但到了之後,或許人類再也不被歸爲高級動物了,不是動物了,那麼全部將human做爲animal的子類型的設計,又要從新設計了。因此咱們用面向對象來設計軟件時,不要沉迷於類型,而是要沉迷於對象。我就是由於在上面的例子中沉迷於類型,纔會有clone不能做爲對象行爲的見解。什麼行爲和屬性,一個對象能克隆本身,那麼給他加個clone方法,不能,就不須要有,根本不須要用類型來定義。爲何JavaScript比C++/Java更面向對象,由於JavaScript不沉迷於類型,而C++/Java沉迷於類型。

面向對象,面向的,是對象。



以上是對類型的介紹,下面,纔是真正的對面向對象的介紹



四 面向對象設計

那麼什麼是面向對象設計?在面向對象的領域中,有幾個方面: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的。爲何,由於在這種狀況下,類型都不是必須的,不少語言都着歎爲觀止的類型設計手段,而沒有一個設計單對象的方式。只有一個對象,意味着什麼封裝、細化、多態都不須要了,必定要創建一個類型,而後用各類詭異、莫名其妙的手段來限制只能創建一個實體對象,是自尋煩惱。面向對象,核心是對象,而不是類型。

相關文章
相關標籤/搜索