再談面向對象的三大特性

面向對象的三大特性:封裝、繼承和多態。這是任何一本面向對象設計的書裏都會介紹的,但鮮有講清楚的,新手看了以後除了記住幾個概念外,並沒真正瞭解他們的意義。前幾天在youtune上看了Bob大叔講解的SOLID原則,其中有一段提到面向對象的三大特性,收穫不少,可是我並不徹底贊同他的觀點,這裏談談個人想法:css

封裝

『封裝』第一層含義是信息隱藏。這是教科書裏都會講解的,把類或模塊的實現細節隱藏起來,對外只提供最小的接口,也就是所謂的『最小知識原則』。有個共識,正常的程序員能理解的代碼在一萬行左右。這是指在理解代碼的實現細節的狀況下,正常的程序員能理解的代碼的規模。好比一個文件系統,FAT、NTFS、EXT4和YAFFS2等,它們的實現是比較複雜的,少則幾千行代碼,多則幾萬行,要理解它們的內部實現是很困難的,可是若是屏蔽它們的內部實現細節,只是要了解它們對外的接口,那就很是容易了。程序員

關於『封裝』的這一層含義,Bob大叔提出了驚人的看法:『封裝』不是面向對象的特性,面向過程的C語言比面向對象的C++/Java在『封裝』方面作得更好!證據也是很充分:C語言把函數的分爲內部函數和外部函數兩類。內部函數用static修飾,放在C文件中,外部函數放在頭文件中。你徹底不知道內部函數的存在,即便知道也無法調用。而像在C++/Java中,經過public/protected/private/friend等關鍵字,把函數或屬性分紅不一樣的等級,這把內部的細節暴露給使用者了,使用者甚至能夠繞過編譯器的限制去調用私有函數。因此在信息隱藏方面,『封裝』不但不是面向對象的特性,並且面向對象減弱了『封裝』。算法

『封裝』的第二層含義是把數據和行爲封裝在一塊兒。我以爲這纔是面向對象中的『封裝』的意義所在,而通常的教科書裏並沒說起或強調。面向過程的編程中,數據和行爲是分離的,面向對象的編程則是把它們當作一個有機的總體。因此,從這一層含義來看,『封裝』確實是面向對象的『特性』。sql

面向對象是一種思惟方式,而不是表現形式。在C語言中,能夠實現面向對象的編程,事實上,幾乎全部C語言開發的大型項目,都是採用了面向對象的思想開發的。把C語言說成面向過程的語言是不公平的,是否是面向對象的編程主要是看指導思想,而不是編程語言。你用C++/Java能夠寫面向過程的代碼,也能夠用C語言寫面向對象的代碼。數據庫

繼承

類就是分類的標準,也就是一類事物,一類具備相同屬性和行爲對象的抽象。好比動物就是一個類,它描述了全部具備動物這個屬性的事物的集合。狗也是一個類,它具備動物全部的特性,咱們說狗這個類繼承了動物這個類,動物是狗的父類,狗是動物的子類。在C語言中也能夠模擬繼承的效果,好比:編程

struct Animal {
...
};
struct Dog {
    struct Animal animal;
    ...
}
struct Cat {
    struct Animal animal;
    ...
}

由於C語言也能夠實現『繼承』,因此Bob大叔認爲『繼承』也不算不上是面向對象的『特性』。可是我以爲,C語言中實現『繼承』的方式,須要用面向對象的思惟來思考才能理解,不然純粹從數據結構的方式來看上面的例子,理解起來就會截然不同:animal是Dog的一個成員,因此Animal能夠當作是Dog的一部分!Is a 變成了has a。只有在面向對象的思想中,說『繼承』纔有意義,因此說『繼承』是面向對象的『特性』並不牽強。設計模式

在C語言裏實現多重繼承更是很是麻煩了,記得glib裏實現了接口的多重繼承,可是用起來仍是挺彆扭的,對新手來講更是難以理解。多重繼承在某些狀況下,會對編譯器形成歧義,比菱形繼承結構:A是基類,B和C是它的兩個子類,D從B和C中繼承過來,若是B和C都重載了A的一個函數,編譯器此時就無法區分用B的仍是C的了(固然這是能夠解決的)。安全

像Bob大叔說的,Java沒有實現多重繼承,並非多重繼承沒有用。而是爲了簡化編譯器的實現,C#沒有實現多重繼承,則是由於Java沒有實現多重繼承:) markdown

除了接口多重繼承是必不可少的,類的多重繼承在現實中也是很常見的。好比:狼和狗都是狗科動物的子類,貓和老虎都是貓科動物的子類。狗科動物和貓科動物都是動物的子類。可是貓和狗都是家畜,老虎和狼都是野生動物。貓不但要繼承貓科動物的特性,還繼承家畜的特性。類就是分類的標準,而混用不一樣的分類標準是多重繼承的主要來源。多重繼承能夠用其餘方式實現,好比traits和mixin。網絡

不論是普通繼承,接口繼承,仍是多重繼承,在面向對象的編程語言中,實現起來要更加容易和直觀,在面向過程的語言中,雖然能夠實現,可是比較醜陋,並且本質是面向對象的思考方式。因此『繼承』應該稱得上是面向對象的『特性』了。介於繼承帶來的複雜性,現代面向對象的設計中,都推薦用組合來代替繼承實現重用。

多態

『多態』原本是面向對象思想中最重要的性質(固然也算不上是特有的性質),可是教科書裏都只是介紹了『多態』的表現形式,而沒有介紹它用途和價值。『多態』通常表現爲兩種形式:

  • 容許不一樣輸入參數的同名函數存在。這個性質會帶來必定的便利,特別是對於構造函數和操做符的重載。但這種『多態』是在編譯時就肯定了的,因此只能算成一種語法糖,並無什麼特別的意義。

  • 子類能夠重載父類中函數原型徹底相同的同名函數。若是隻看它的表現形式,在父類中存在的函數,在不一樣的子類中能夠被從新實現,這看起來是吃飽了撐着。可是這種『多態』倒是軟件架構的基礎,幾乎全部的設計模式和方法都依賴這種特性。

隔離變化是軟件架構設計的基本目標之一,接口正是隔離變化最重要的手段。咱們常常說分離接口與實現,針對接口編程,主要是由於接口能夠隔離變化。若是沒有第二種『多態』,就沒有真正意義上的接口。面向對象中的接口,不只是指模塊對外提供的一組函數,並且特指在運行時才綁定具體實現的一組函數,在編譯時根本不知道這組函數是誰提供的。咱們先把接口簡單的理解爲,在基類中定義一組函數,可是基類並無實現它們,而在具體的子類中去實現。這不就是『多態』的第二種表現形式麼。

接口怎麼可以隔離變化呢?Bob大叔舉了一個很是好的例子:

#include <stdio.h>

int main() {
    int c;

    while((c = getchar()) != EOF) {
        putchar(c);
    }

    return 0;
}

這個程序和Hello world是一個級別的,你從鍵盤輸入一個字符,它就顯示一個字符。可是它卻蘊含了『多態』最精妙的招式。好比說輸入吧,getchar是從標準輸入(STDIN)讀入一個字符,鍵盤輸入是缺省的標準輸入,可是鍵盤輸入只是衆多標準輸入(STDIN)中的一種。你能夠從任何一個IO設備讀取數據:從網絡、文件、內存和串口等等,換成任何一種輸入,這個程序都不須要任何改變。

具體實現變了,調用者不須要修改代碼,並且它根本不用從新編譯,甚至不用重啓應用程序。這就是接口的威力,也是『多態』的功勞。

上面的程序是如何作到的呢?IO設備的驅動是一套接口,它定義了打開、關閉、讀和寫等操做。對實現者來講,無論數據從哪裏來,要到哪裏去,只要實現接口中定義的函數便可。對使用者來講,徹底不一樣關心它具體的實現方式。

『多態』不可是隔離變化的基礎,也是代碼重用的基礎。公共函數的重用是有價值的,在面向過程的開發中也很容易作到這種重用。但現實中的重用沒那麼簡單,就連一些大師也感嘆重用太難。好比,你可能須要A這個類,你把它拿過來時,發現它有依賴B這個類,B這個類有依賴C這個類,搞到最後發現,它還依賴一個看似徹底不相關的類,重用的念頭只好打住。若是你以爲誇張了,你能夠嘗試從一個數據庫(如sqlite)中,把它的B+樹代碼拿出來用一下。

在『多態』的幫助下,狀況就會大不相同了。A這個類依賴於B這個類,咱們能夠把B定義成一個接口,讓使用A這個類的使用者傳入進來,也就是所謂的依賴注入。若是你想重用A這個類,你能夠爲它定製一個B接口的實現。好比,我最近在一個只有8K內存的硬件上,爲一塊norflash寫了一個簡單的文件系統(且看做是A類),若是我直接去調用norflash的API(且看做是B類),就會讓文件系統(A類)與norflash的API(B類)緊密耦合到一塊兒,這就會讓文件系統的重用性大打折扣。

個人作法是定義了一個塊設備的接口(即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;
};

在初始化文件系統時,把塊設備註入進來:

bool_t sfs_init(sfs_t* fs, block_dev_t* dev);

這樣,文件系統只與塊設備接口交互,不須要關心實現是norflash、nandflash、內存仍是磁盤。並且帶來幾個附加好處:

  • 能夠在PC上作文件系統的單元測試。在PC上,用內存模擬一個塊設備,文件系統能夠正常工做了。

  • 能夠經過裝飾模式爲塊設備添加磨損均衡算法和壞塊管理算法。這些算法和文件系統均可以獨立重用。

『多態』讓真正的重用成爲可能,沒有『多態』就沒有各類框架。在C語言中,多態是經過函數指針實現的,而在C++中是經過虛函數,在Java中有專門的接口,在JS這種動態語言中,每一個函數是多態的。『多態』雖然不是面向對象的『特有的』屬性,可是面向對象的編程語言讓『多態』更加簡單和安全。

相關文章
相關標籤/搜索