軟體設計本質論—白話物件導向

來源:互聯網
上載者:User

不同的人在談物件導向編程(OOP)時所指的含義並不相同。有人認為任何採用圖形介面的應用程式都是物件導向的。有人把它作為術語來描述一種特別的處理序間通訊機制。還有人使用這個詞彙是另有深義的,他們其實是想說:“來啊,買我的產品吧!”我一般不提OOP,但只要提到,我的意思是指使用繼承和動態綁定的編程方式。 --《C++沉思錄》

《C++沉思錄》說的是十幾年前的事了,現在大家對物件導向的回答已經是眾口一詞:封裝、繼承和多態。大家都知道,在物件導向中,一輛汽車是一個對象,汽車這個概念是一個類。汽車有漂亮的外觀,把各種內部原理都隱藏起來了,司機不必知道它的內部工作原理仍然能開車,即使汽車隨技術的進步不斷升級,對司機也沒有什麼影響,這就是封裝的好處。

汽車是交通工具的一種,汽車是一個類,交通工具也是一個類,而交通工具類包括了汽車類,從而具有更廣泛的意義。這種從抽象到具體的關係就是繼承關係,我們可以說汽車類繼承了交通工具類,汽車類是交通工具類的子類,交通工具類是汽車類的父類。

作為交通工具,它肯定可以運動(move),從甲地運動到乙地,就起到了交通的作用。輪船是一種交通工具,所以輪船類也是交通工具類的子類。同樣是運動,輪船的運動和汽車的運動方式肯定有所不同,這樣以不同的方式完成同樣的功能就叫多態。

關於對象:對象就是某一具體的事物,比如一個蘋果, 一台電腦都是一個對象。每個對象都是唯一的,兩個蘋果,無論它們的外觀有多麼相像,內部成分有多麼相似,兩個蘋果畢竟是兩個蘋果,它們是兩個不同的對象。對象可以是一個實物,也可能是一個概念,比如某個蘋果對象是實物,而一項政策可能就是一個概念性的對象了。

關於類:對象可能是一個無窮的集合,用枚舉的方式來表示對象集合不太現實。抽象出對象的特徵和功能,按此標準將對象分類,這就引入類的概念。類就是一類事物的統稱,類實際上就是一個分類的標準,符合這個分類標準的對象都屬於這個類。當然,為了方便起見,通常只需要抽取那些,對當前應用來說是有用的特徵和功能。

關於抽象類別:類是對對象的抽象,比如,蘋果是對所有具體的蘋果的抽象。如果我們對蘋果這個類進行一步抽象,可以得到一個水果類。這種對類本身進行抽象而得到的類,就是抽象類別。抽象類別不像普通類,它是沒有對象與之對應的。像蘋果類,你總是可以拿到一個叫蘋果的東西,而對於水果類,根本沒一個真正叫水果的東西。你可以說一個蘋果是一個水果,從邏輯上講沒有錯,但沒有什麼意義。一般在程式中,抽象類別是不能執行個體化的。

關於物件導向:物件導向就是以對象為中心。為什麼不說是面對類,而說是物件導向呢?類是對象的集合,考慮類實際上也是在考慮對象,有時甚至並不嚴格的區分它們。所以說物件導向一詞比面向類更確切。

既然以對象為中心,物件導向所考慮的內容自然是對象、對象間的協作、對象的分類、類之間的關係等等,由此引申了出幾個重要的概念。

1. 封裝

what:對象也有隱私,對象的隱私就是對象內部的實現細節。要想對象保持良好的形象就要保護好對象隱私,所謂的封裝其實就是保護對象隱私。當然,沒有人能完全隱藏自己的隱私,比如你去轉戶口時,你不得不透露自己的家庭資訊和健康情況。另外,在不同的場合所透露隱私的數量也不一樣,朋友和家人可能會知道你更多隱私,同事次之,其他人則知道得更少。物件導向也考慮了這些實際的情況,所以像C++之類的語言有public/private/protected/friend等關鍵字,以適應於不同的情況。

why:封裝可以隔離變化。據以往的經驗,我們知道內部實現是容易變化的,比如電腦在不斷的升級,機箱還是方的,但裡面裝的CPU和記憶體已是今非昔比了。變化是不可避免的,但變化所影響的範圍是可以控制的,不管CPU怎麼變,它不應該影響使用者使用的方式。封裝是隔離變化的好辦法,用機箱把CPU和記憶體等等封裝起來,對外只提供一些標準的介面,如USB插口、網線插口和顯示器插口等等,只要這些介面不變,內部怎麼變,也不會影響使用者的使用方式。

封裝可以提高易用性。封裝後只暴露最少的資訊給使用者,對外介面清晰,使用更方便,更具方便使用性。試想,如果普通使用者都要知道機箱內部各種晶片和跳線,那是多麼恐怖的事情,到現在為止我甚至還搞不清楚硬碟的跳線設定,幸好我沒有必要知道。

how:在C語言中,可以用結構+函數來類比類的實現,而用這種結構定義的變數就是對象。封裝有兩層含義,其一是隱藏內部行為,即隱藏內建函式,調用者只能看到對外提供的公用函數。其二是隱藏內部資訊,即隱藏內部資料成員。現在都建議不要對外公開任何資料成員,即使外部需要知道的資料成員,也只能通過函數擷取。

在C語言中要隱藏內建函式很簡單:不要它把放在標頭檔中,在C檔案中定義時,前面加static關鍵字,每個類放在獨立的檔案中。這樣可以把函數的作用範圍限於當前檔案內,當前檔案只有類本身的實現,即只有當前的類自己才能看到這些函數,這就達到了隱藏的目的。

在C語言中要隱藏資料成員較為麻煩,它沒有提供像C++中所擁有的public/protected/friend/private類似的關鍵字。只能通過一些特殊方法類比部分效果,我常用的方法有兩種。

其一是利用C的特殊文法,在標頭檔中提前聲明結構,在C檔案才真正定義它。這樣可以把結構的全部資料資訊都隱藏起來。因為外部不知道對象所佔記憶體的大小,所以不能靜態建立該類的對象,只能調用類提供的建立函數才能建立。這種方法的缺陷是不支援繼承,因為子類中得不到任何關於父類的資訊。如:

標頭檔:

struct _LrcPool;
typeef struct _LrcPool LrcPool;
LrcPool* lrc_pool_new(size_t unit_size, size_t n_prealloc_units);
void* lrc_pool_alloc(LrcPool* thiz);
void lrc_pool_free(LrcPool* thiz, void* p);
void lrc_pool_destroy(LrcPool* thiz);

  C檔案:

struct _LrcPool
{
 size_t unit_size;
 size_t n_prealloc_units;
};

LrcPool* lrc_pool_new(size_t unit_size, size_t n_prealloc_units)
{
 LrcPool* thiz = LRC_ALLOC(LrcPool, 1);

 if(thiz != NULL)
 {
  thiz->unit_size = unit_size;
  thiz->n_prealloc_units = n_prealloc_units;
 }
 return thiz;
}

其二是把私人資料資訊放在一個不透明的priv變數中。只有類的實現代碼才知道priv的真正定義。如:

標頭檔:

struct _LrcBuilder
{
 LrcBuilderBegin on_begin;
 LrcBuilderOnIDTag on_id_tag;
 LrcBuilderOnTimeTag on_time_tag;
 LrcBuilderOnLrc on_lrc;
 LrcBuilderEnd on_end;
 LrcBuilderDestroy destroy;
 char priv[1];
};

  C檔案:

struct _LrcDumpBuilder
{
 FILE* fp;
};

typedef struct _LrcDumpBuilder LrcDumpBuilder;
LrcBuilder* lrc_dump_builder_new(FILE* fp)
{
 LrcDumpBuilder* data = NULL;
 LrcBuilder* thiz = (LrcBuilder*)calloc(sizeof(LrcBuilder) + sizeof(LrcDumpBuilder), 1);

 if(thiz != NULL)
 {
  thiz->on_begin = lrc_dump_builder_on_begin;
  thiz->on_id_tag = lrc_dump_builder_on_id_tag;
  thiz->on_time_tag = lrc_dump_builder_on_time_tag;
  thiz->on_lrc = lrc_dump_builder_on_lrc;
  thiz->on_end = lrc_dump_builder_on_end;
  thiz->destroy = lrc_dump_builder_destroy;
  data = (LrcDumpBuilder*)thiz->priv;
  data->fp = fp != NULL ? fp : stdout;
 }
 return thiz;
}

2. 繼承

what: 繼承描述的是一種抽象到具體的關係。具體的東西繼承了抽象的東西的特性,比如說,水果這個概念比蘋果這個概念更抽象,其意義更具有一般性,而蘋果這個概念則更具體,其意義更狹窄一些,在物件導向裡,我們可以說蘋果類繼承了水果類。繼承是指繼承了父類的特性,繼承本質是源於分類學,細的分類繼承大分類的特性。

why: 繼承描述了抽象到具體的關係,所以能夠有效利用抽象這件武器來戰勝軟體的複雜性。抽象在實現中無處不在,類就是對事物的抽象,提到蘋果你就想到蘋果這一類事物,無需要關心其大小、顏色和成分,蘋果這兩個字就足夠了。名不正則言不順,言不順則事不成,看來老夫子已經領悟到了抽象的威力。

繼承不但利用了抽象的力量來降低系統的複雜性,它還提供了一種重用的方式。假設我們承認下列面這個繼承關係,蘋果繼承了水果,水果繼承了食物,如果我們已經知道什麼是食物,什麼是水果,在描述蘋果時,沒有必要去重複講解食物和水果的概念了,這就是重用,重用了對水果和食物兩個概念的理解。

how: 在C語言中實現繼承很簡單,可以用結構來類比。這種實現基於一個明顯的事實,結構在記憶體中的布局與結構的聲明具有一致的順序。我們知道在程式描述事物的特徵時,主要通過資料變數描述事物的屬性特徵,如顏色、重量和體積等,用函數來描述事物的行為特徵,和運動、成長和搏鬥等。在C語言中實現繼承的方式如:

struct _GObject
{
 GTypeInstance g_type_instance;
 volatile guint ref_count;
 GData *qdata;
};

struct _GtkObject
{
 GObject parent_instance;
 guint32 flags;
};

struct _GtkWidget
{
 GtkObject parent_instance;
 guint16 private_flags;
 guint8 state;
 guint8 saved_state;
 gchar *name;
 GtkStyle *style;
 GtkRequisition requisition;
 GtkAllocation allocation;
 dkWindow *window;
 GtkWidget *parent;
};

繼承

繼承在現實世界中應用很廣,在程式裡也是一樣,甚至可以說是過度使用了。多年以前一些大師已經提出,優先使用組合而不是繼承。主要原因有三點,首先是多級繼承和多重繼承太複雜了,失去了抽象帶來的簡潔性。其次是父類與子類之間共用太多資訊,它們的耦合太緊密。三是父類與子類之間的關係在編譯時間就靜態繫結了,很難做到在運行時多態。

現在一般都提倡,只繼承介面不繼承實現,通過組合達到代碼重用的目的。在《設計模式》中是這樣強調的,在MS的COM裡也是這樣做的。所以我基本上只使用介面繼承,很少遇到什麼麻煩,建議大家也遵循這一準則。

 3. 多態

what: 儘管多態這個詞本身就表明了它所代表的意義,但還是讓初學者感到多少有些神秘。多態就是完成相同功能的多種方式,比如拿動物的運動來說吧,鳥的運動通常是飛,魚的運動通常是遊,陸上動物的運動通常是跑,同是運動,但方式不一樣,這就是多態。不少人對多態的回答是,允許同名函數存在。這種回答顯然沒有抓住多態的本質。

why: 關於動物運動這個例子,可能無法展示多態的好處。我們來考慮另外一個多態的例子,隨身碟。隨身碟的技術含量可能不是很高,有很多廠家都在設計和生產,就是說隨身碟有多種不同的實現,這就是隨身碟的多態。隨身碟的多態性對消費者來說是有好處的,選擇多了,你可以在價格、品質和外觀等方式做出平衡,選擇你中意的隨身碟。多態的前提是介面的一致性,否則多態造成的麻煩遠勝於它帶來的好處。不管隨身碟的體積、顏色和品質如何,它都必需遵循相應的USB標準,這些隨身碟在任何帶USB介面的電腦上都可以使用。

how: 多態在C語言中通常用函數指標來實現,函數指標定義了函數的原型,即它的參數和傳回值的描述,以及函數的意義,不同的函數可以有相同的函數原型,比如排序函數,無論是快速排序還是歸併排序,它們的實現不一樣,但函數原型可以一樣。在不同的情況下,讓函數指標到不同的函數實現上,這就實現了多態。下面的C語言例子:

介面:

struct _LrcBuilder;

typedef struct _LrcBuilder LrcBuilder;
typedef LRC_RESULT (*LrcBuilderBegin)(LrcBuilder* thiz, const char* buffer);
typedef LRC_RESULT (*LrcBuilderOnIDTag)(LrcBuilder* thiz, const char* key, size_t key_length,
const char* value, size_t value_length);
typedef LRC_RESULT (*LrcBuilderOnTimeTag)(LrcBuilder* thiz, size_t start_time);
typedef LRC_RESULT (*LrcBuilderOnLrc)(LrcBuilder* thiz, const char* lrc, size_t lrc_length);
typedef LRC_RESULT (*LrcBuilderEnd)(LrcBuilder* thiz);
typedef LRC_RESULT (*LrcBuilderDestroy)(LrcBuilder* thiz);

struct _LrcBuilder
{
 LrcBuilderBegin on_begin;
 LrcBuilderOnIDTag on_id_tag;
 LrcBuilderOnTimeTag on_time_tag;
 LrcBuilderOnLrc on_lrc;
 LrcBuilderEnd on_end;
 LrcBuilderDestroy destroy;
 char priv[1];
};

實現一:

LrcBuilder* lrc_dump_builder_new(FILE* fp)
{
 LrcDumpBuilder* data = NULL;
 LrcBuilder* thiz = (LrcBuilder*)calloc(sizeof(LrcBuilder) + sizeof(LrcDumpBuilder), 1);

 if(thiz != NULL)
 {
  thiz->on_begin = lrc_dump_builder_on_begin;
  thiz->on_id_tag = lrc_dump_builder_on_id_tag;
  thiz->on_time_tag = lrc_dump_builder_on_time_tag;
  thiz->on_lrc = lrc_dump_builder_on_lrc;
  thiz->on_end = lrc_dump_builder_on_end;
  thiz->destroy = lrc_dump_builder_destroy;
  data = (LrcDumpBuilder*)thiz->priv;
  data->fp = fp != NULL ? fp : stdout;
 }
 return thiz;
}

實現二:

LrcBuilder* lrc_default_builder_new(void)
{
 LrcDefaultBuilder* data = NULL;
 LrcBuilder* thiz = (LrcBuilder*)calloc(sizeof(LrcBuilder) + sizeof(LrcDefaultBuilder), 1);

 if(thiz != NULL)
 {
  thiz->on_begin = lrc_default_builder_on_begin;
  thiz->on_id_tag = lrc_default_builder_on_id_tag;
  thiz->on_time_tag = lrc_default_builder_on_time_tag;
  thiz->on_lrc = lrc_default_builder_on_lrc;
  thiz->on_end = lrc_default_builder_on_end;
  thiz->destroy = lrc_default_builder_destroy;
  data = (LrcDefaultBuilder*)thiz->priv;
 }
 return thiz;
}

類的三個層次:

類這個概念比較微妙,即使在軟體開發領域,不同的人提到這個概念所指的內容也不一樣。一些大師早就注意到了這一點,為了讓這個概念在不同情況下,具有較準確的意義,他們建議從三個層次看待類這個概念:

1. 概念層(Conceptual)

這是一個較高的層次,通常在進行領域分析時,為了建立概念性模型時使用。這時使用的術語是現實世界中的術語,而不是軟體開發中的術語。在這個層次,類只是一個概念,加上一些不太嚴謹的特徵說明,甚至只有一個名稱。儘管它往往與軟體開發中的類一一對應,便這種映射並不一定是直接的。
  
 2. 規格層(Specification)

在這個層次,類已經是屬於軟體開發範疇了,但主要關注的是類的介面,而不是類的實現。此時你可能想到它的一組介面函數,而不關心這些函數是如何?的。

3. 實現層(Implementation)

在這個層次,我們才真正關注類的實現,此時你可能會想到一些用某種語言寫成的函數體,定義的成員變數等等。

物件導向的好處:

物件導向已經征服了軟體開發的絕大部分領域,近幾年來出現的面向方面的編程(AOP)、產生式編程(GP)和面向組件的開發等等,都提出了一些新的思維,在某些方面大提高了開發效率,但它們並非是取代了物件導向,相反是對物件導向的補充和完善,物件導向始終穩坐第一把交椅。

物件導向到底有何德何能,它憑藉什麼取代面向對程呢?封裝、繼承和多態到底有何種魔力,吸引眾多的高手去完善它,讓佈道者們不厭其煩的頌揚它呢?歸根結底,物件導向會帶來兩個好處,這兩個好處正是設計者們一直在追求的:

1. 降低系統的複雜度。

眾所周知,隨著硬體的飛速發展,電腦的計算能力越來越強大,人們對軟體期望也越來越高,而軟體複雜度又與它的規模成指數倍數增長。軟體複雜度可以說是軟體開發的第一大難題,我們可以輕而易舉寫出5000行代碼,而面對100萬行代碼規模的軟體,會有點讓人覺得人的智力是多麼有限。

而物件導向正是降低系統複雜度的好方法。首先它按類來組織系統,把系統分成幾個大的部分,每個部分又由更小的子類組成,如此細分下去直到我們能輕易實現它為此,這種分而治之的方法符合人類解決複雜問題的習慣。

其次是它採用從抽象到具體的順序來把握事物,抽象讓我們用少量精力先掌握事物的共性,然後再去研究事物更具體的特性,這種逐漸細化的方法也是符合人類解決複雜問題的習慣的。

2. 隔離變化。

需求變化和技術變化也是軟體開發所面臨的兩大難題。使用者似乎從來不知道什麼是他們真正的需求,或許他們真正的需求也是在變化的。技術可謂日新月異,我們不斷的發明新技術,這些技術幫我們提高了生產力。但有時新技術也會砸到自己的腳,為了運用這些新技術,我們要花更多時間學習和工作。或者說創新只是滿足了少數人的樂趣,讓多數人吃了苦頭。

只有變化才是永恒的,詛咒變化無濟於事,面對它並搞掂它才是正途。大師也建議我們去擁抱變化,而不是逃避變化。物件導向正是擁抱變化的銀彈之一,它不是嘗試阻止變化的發生(對此誰也無能為力),而是努力去隔離變化。與整體比較,變化的部分畢竟只佔很小的分量,如果這些變化被隔離開了,它不影響不變的部分的,變的部分獨立變化,不會牽一髮而動全身,這可以大大減少變化引起的麻煩。針對介面編程而不是針對實現編程,這是面對象的精髓,也是擁抱變化的良方。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.