面向物件程式設計/OOP 簡介
有關面向物件程式設計 OOP 的概述和歷史,請參考維基百科文章.
我們預計讀者對程式設計有基本瞭解,因為我們會用多種語言給出示例。我們會解釋討論中任何不明顯的語法,儘管這並非重點。重點是提供一些關於語言風格的指示,以及對面向物件思想在現實世界中的應用的洞察。
我們將 OOP 分為兩個階段——經典階段和現代階段。雖然這種區分在一定程度上是任意的,但我們認為將 OOP 視為它在 1980 年代和 1990 年代初期的實踐方式,可以說明當前實踐的動機。
什麼是經典 OOP?
面向物件程式設計可以追溯到一種名為Simula的語言,特別是 Simula 67,它在 1960 年代很流行。正是 Simula 首次引入了“類”和“物件”,從而產生了“面向物件”程式設計這一術語。到了 1990 年代初,人們在大型 OOP 專案方面積累了足夠的經驗,發現了一些侷限性。諸如Self之類的語言、諸如介面程式設計(也稱為元件或面向元件程式設計)之類的想法,以及諸如泛型程式設計之類的 методологии都是為了解決這些問題而開發的。儘管經常遭到 OOP 純粹主義者的嘲笑,但在 1998 年——包括泛型程式設計功能——C++的標準化,真正迎來了 OOP 的現代時代,我們也稱之為多正規化程式設計。這主要是由於 C++ 的流行以及標準模板庫(STL)的巧妙設計,向如此龐大的受眾展示了這種新方法的實用性。
到 1980 年,施樂將Smalltalk提供給了外部人士,並將其恰當地命名為 Smalltalk-80。與其他早期的程式語言不同,Smalltalk 是一種完整的環境,而不僅僅是一種語言,這與當時的 Lisp 有著共同的特點。雖然 Lisp 機器預示著 IDE 的出現,但 Smalltalk 卻開創了 GUI,最終影響了 Macintosh 電腦的開發。與此同時,施樂在 70 年代開發 Smalltalk 的時候,C 語言由於 UNIX 主要用 C 編寫而變得流行。因此,正是 C——本來不可能成為候選者——被 Bjarne Stroustrup 與來自 Simula 的思想融合在一起,創造了“帶類的 C”,它在 1983 年更名為 C++。1985 年,Bertrand Meyer 對 Smalltalk、C++ 以及各種附加在 Lisp 方言上的物件系統感到不滿意,於是建立了 Eiffel。雖然 Eiffel 更近一些,但 Java 本質上是克隆了老式的 C++,因此我們將 Smalltalk、老式的 C++、Eiffel 和 Java 視為經典 OOP。附加在 Lisp 上的物件系統(最終在 1994 年標準化為 CLOS)產生了一種截然不同的方法。雖然我們不認為 CLOS 是經典 OOP,但它確實影響了現代 OOP。
經典 OOP 發展出過度依賴一種稱為“繼承”的技術的趨勢,最終程式設計師意識到,他們在很多概念上截然不同的情況下使用繼承。現代 OOP 基本上包含了這些概念,有時作為語言級別的特性,有時透過程式設計師實踐。目標主要是鬆散耦合、更易於維護和重用。從歷史上看,David Ungar 和 Randall Smith 在 1987 年完成了他們的第一個可工作的 Self 編譯器,到 1990 年,Sun Microsystems 接手了這個專案。雖然 Self 沒有真正發展成為現代 OOP 語言,但它是一種第二代 OOP 語言。然後,在 1990 年代初,Alexander Stepanov 和 Meng Lee 開創了泛型程式設計,並編寫了 C++ STL 的早期草案。這開啟了一種(仍在繼續的)趨勢,即在更傳統的 OOP 環境中融入函數語言程式設計思想,這是一種逆 CLOS。此外,1990 年代初還出現了CORBA 和微軟的 COM,它們是導致最初 Windows API 出現的想法的自然延伸。這種介面或元件程式設計是封裝——OOP 的基本原則,正如我們即將看到的那樣——的自然延伸。所有這些發展都旨在進一步管理或降低複雜性。由於這是 OOP 的最初目標,並且這些技術與經典 OOP 結合使用,因此我們認為將它們納入關於“OOP”的論述中是合適的。
因此,當代面向物件程式設計往往與經典面向物件程式設計截然不同。尤其是,現在有更多抽象可供選擇,因此要習慣哪種抽象最適合哪種問題型別更具挑戰性!在如今經典的書中,Gamma 等人介紹了設計模式,它幫助將各種 OOP 技術綜合起來,並將其應用於非常常見的問題。
未來將包含更多函數語言程式設計技術在 OOP 環境中的標準化,特別是 lambda 表示式和閉包,以及更強大的超程式設計結構。透過泛型或超程式設計技術自動應用設計模式是一個有趣的領域。
鬆散地說,術語物件用於喚起與現實世界中的物體(如椅子或吉他)的聯絡。但對於軟體來說,只使用了一些簡化的抽象,專門用於手頭的任務。雖然真正的椅子是由原子和分子構成的,並根據物理定律及其原子構成對環境做出反應,但“椅子物件”會根據你是編寫遊戲還是為傢俱店編寫 POS 系統而有很大差異。從椅子的角度來解決問題,不如從問題的角度來解決問題更有成效。
在本手冊中使用的大多數語言中,你會發現從技術角度來說(類似於以 9600 波特率吹口哨,我理解),物件是“類的例項”。太好了,那是什麼意思呢?好吧,我們可以將這種想法一直追溯到柏拉圖及其柏拉圖式理想。如果你花更多的時間看《比爾和泰德的奇妙冒險》,而不是讀柏拉圖,那麼這個想法是,椅子的概念是一個獨立於任何特定椅子的實體。換句話說,你可以抽象地想象一把椅子,而不必去想任何特定的椅子。
在大多數 OOP 語言中,這種抽象的椅子概念被稱為類(來自分類),是實際製造椅子的原型或藍圖。用藍圖製造東西的行為通常被稱為例項化,製造出來的東西既是物件,又是充當藍圖的類的例項。作為人類,我們通常傾向於反過來——我們對遇到的物體進行分類。我們可以很容易地識別出我們遇到的類似椅子的東西是椅子;正是這種分類讓我們首先獲得了術語類。
很容易陷入關於物件性的深奧哲學辯論;在一些領域,如知識表示和計算本體論,它們非常重要。然而,對於計算機程式設計師來說,只需要弄清楚你的應用程式需要了解和處理關於椅子的什麼資訊。這本身可能是一個非常困難的問題,通常沒有必要使其更加困難!
如果你對整個概念仍然不太清楚,可以考慮一個更技術性的解釋。結構體(結構)、記錄、表和其他組織相關資訊的 方式早於面向物件程式設計。你可能熟悉以下類似的 Pascal 程式碼
TYPE chair = RECORD
model : integer;
weight : integer;
height : integer;
color : COLOR;
END;
這實際上並沒有建立一個 chair 變數,而是定義了當你建立 chair 變數時它會是什麼樣子。你可以繼續建立椅子陣列等等,正如我們希望你自己發現的那樣,這種東西對於保持程式的可理解性是必不可少的。面向物件程式設計想要利用這種優勢,並儘可能地從可理解性、正確性和簡單性中獲益。
一個基本的解決問題的方法是分而治之——你只需要弄清楚如何將你的問題分成子問題。面向物件程式設計的創新者意識到,我們已經找到了劃分問題的方法,並且這反映在我們組織資料的方式中,就像上面一樣。如果你檢視包含那個 chair RECORD 的應用程式,你肯定會發現很多關於椅子操作的程式碼。為什麼還要費心定義它呢?所以,如果你要將所有這些程式碼從整個應用程式中提取出來,並將其與 chair 定義放在一起,那麼推理是,你應該更容易確保
- 所有關於椅子的程式碼都是正確的
- 所有關於椅子的程式碼彼此一致
- 沒有重複的關於椅子的程式碼
- 總的來說,你的程式碼更整潔,因為關於椅子的程式碼不再與沙發程式碼等混在一起
所以,你將那個 chair 定義和從整個應用程式中提取的程式碼放在一起,並稱之為一個類。將 chair 變數稱為一個物件,你就開始進行面向物件程式設計了。
由於這本應該是實用的書籍,讓我們看看我們的第一個程式碼示例,這次是 Python 程式碼
class Chair:
model = None
height = None
weight = None
color = None
def has_arms(self):
return self.model % 2 # odd-numbered models have armrests
這看起來與 Pascal 示例並沒有太大區別。唯一的區別是 class Chair 現在包含一個 has_arms 方法。方法是在類中定義的函式,通常用於處理某些特定於類的 資料。希望目的很清楚,所以我要指出重要部分:has_arms 是一個計算或推斷屬性——這些資訊不是直接儲存的。
在 Python 中,你會像這樣使用這個類
c = Chair()
c.model = 15
c.height = 40
c.weight = 10
c.color = 7
if c.has_arms():
do_something
else:
do_other_thing
這裡,"c" 變數是 'chair' 類的例項,也稱為 chair 物件。我們像在 Pascal 中一樣初始化屬性(這將在以後改變!),如果椅子有扶手就做一件事,否則就做另一件事。但我們從未初始化 "has_arms" 屬性或類似的東西。
由於目標之一是讓你對語法無關,我們再次用 C++ 語言展示了同一個例子
class Chair
{
public:
int model;
int weight;
int height;
int color;
bool has_arms()
{
return model % 2 ? true : false;
}
};
Chair c;
c.model = 15;
c.height = 40;
c.weight = 10;
c.color = 7;
if (c.has_arms())
do_something();
else
do_other_thing();
現在,我們只想提一下,這僅僅是冰山一角,這個例子並不代表好的風格(在任何一種語言中)。建立功能如此少的物件似乎沒什麼意義,你很容易生成不需要新類的等效程式碼
struct Chair
{
int model;
int weight;
int height;
int color;
} c;
Chair c = {15, 40, 10, 7 };
if (c.model % 2)
do_something();
else
do_other_thing();
本節的目的是幫助你理解這些術語;我們將在關於封裝、多型性、繼承等等部分深入探討其優勢。然而,讓我們說,雖然“底層”方式可能看起來更短更簡單,但面向物件的優勢隨著程式大小和複雜性的增加而增加。這並不奇怪,因為這就是面向物件程式設計的設計目的,但這確實讓簡單的例子難以找到。所以請耐心等待。