[譯] 被遺忘的物件導向程式設計史(軟體編寫)(第十六部分)

LeviDing發表於2019-02-28

注: 這是 “組合軟體” 系列的一部分,旨在從頭開始學習 Javascript ES6+ 中的函數語言程式設計和組合軟體技術。更多內容即將推出,敬請關注!
< 上一篇 | << Start over at Part 1

我們今天使用的函式式和指令式程式設計範例最早出現於 20 世紀 30 年代,當時使用 Lambda 演算和圖靈機進行數學探索,它們是通用計算(可執行通用計算的形式化系統)的替代公式。Church Turing 理論表明,lambda 演算和圖靈機是等價的,任何使用圖靈機可以做的計算都可以使用 lambda 演算來計算,反之亦然。

注:有一個普遍的誤解,認為圖靈機可以計算任何可計算的問題。但是有一些問題(例如,停機問題)在某些情況下是可計算的,但有些情況卻是使用圖靈機無法計算的。當我在本文中使用“可計算”一詞時,我的意思是“可以由圖靈機計算”。

Lambda 演算代表一種自上而下的函式式應用計算方法,而圖靈機的紙帶/暫存器式的機器公式則代表一種自下而上的、命令式(步進式)計算方法。

從 20 世紀 40 年代,開始出現像機器碼和組合語言這樣的低階語言,直到 20 世紀 50 年代末,出現了第一個流行的高階語言。Lisp 語言,當然還包括 Clojure,Scheme,AutoLISP 等語言,直到今天還在被廣泛使用。FORTRAN 和 COBOL 都出現在 20 世紀 50 年代,但直到今天還在被當作命令式高階語言的範例在使用,儘管對於大多數應用程式來說,C 族語言已經取代了 COBOL 和 FORTRAN。

指令式程式設計和函數語言程式設計都起源於計算數學理論,它們比數字計算機還要早。“物件導向程式設計”(OOP)是艾倫·凱(Alan Kay)在 1966 年或 1967 年讀研究生時創造的。

Ivan Sutherland 創新的 Sketchpad 應用程式是 OOP 的早期靈感。它開發於 1961 年至 1962 年,並於 1963 年發表在他的 Sketchpad 論文 中。其中物件是表示在示波器螢幕上顯示的影像的資料結構,並且通過動態表示具有繼承特性,Ivan Sutherland 在他的論文中稱之為“masters”。任何物件都可以成為“master”,而物件的其他例項稱為“occurrences”。Sketchpad 的 masters 和 JavaScript 的原型鏈繼承有很多共同之處。

注意: 麻省理工學院林肯實驗室的 TX-2 是使用鐳射筆直接進行螢幕互動的圖形計算機顯示器的早期用途之一。1948-1958 執行的 EDSAC,可以在螢幕上顯示圖形。1949 年,麻省理工學院的“旋風”專案使用了一個示波器。該專案的動機是建立一個能夠模擬多架飛機儀器反饋的通用飛行模擬器。這引領了 SAGE 計算系統的發展。TX-2 是 SAGE 的測試計算機

提出於 1965 年的 simula 語言,是第一個被廣泛認可的“物件導向”的程式語言。與 Sketchpad 一樣,Simula 使用了物件,甚至還引入了類,類繼承,子類和虛方法。

注意:虛方法是定義在類上的方法,它被設計為可以被子類重寫。虛方法允許程式動態指定在編譯程式碼時可能不存在的方法,以此確定在執行時呼叫哪個具體方法。JavaScript 具有動態型別,並使用委託鏈來確定要呼叫的方法,因此不需要向開發者再提供虛方法的概念。換句話說,JavaScript 中的所有方法都是執行時進行動態排程,因此 JavaScript 中的方法不需要宣告為“虛方法”。

偉大的想法

“我使用‘物件導向’這個術語,我告訴你我並沒有考慮到 C++。” Alan Kay,OOPSLA ‘97

Alan Kay 在 1966 或 1967 年間在研究生院創造了“物件導向程式設計”一詞。這個偉大的想法是想在軟體中使用封裝好的微型計算機,通過訊息傳遞而不是直接的資料共享,以此來防止將程式分裂為單獨的“資料結構“和“程式”。

“遞迴設計的基本原理是使區域性具有與整體相同的能力。”BB Barton,B5000 的主要設計者,這是一個經過優化來執行 Algol-60 的主機。

Smalltalk 由 Alan Kay、Dan Ingalls、Adele Goldberg 和 Xerox PARC 的其他人一起開發。Smalltalk 比 Simula 更加物件導向 — Smalltalk 中的所有東西都是物件,包括類,整數和塊(閉包)。最初的 Smalltalk-72 不支援建立子類。建立子類是由 Dan Ingalls 在 Smalltalk-76 中引入的

儘管 Smalltalk 支援類並最終支援建立子類,但 Smalltalk 不只是關於類或建立子類的。它是受到 Lisp 和 Simula 啟發的功能語言。Alan Kay 認為業界對子類過度關注而由此忽略了物件導向程式設計帶來的真正好處。

“我很抱歉,我很久以前為這個話題創造了“物件”一詞,而因為它讓很多人專注於細枝末節。實際上真正重要的是訊息傳遞。”
~ Alan Kay

2003 年的電子郵件 中,Alan Kay 闡明瞭他為什麼稱 Smalltalk 為“物件導向”:

“OOP 對我來說意味著訊息傳遞,對狀態程式的本地保留保護和隱藏,以及對所有事物的動態繫結。”
~ Alan Kay

換句話說,根據 Alan Kay 的說法,OOP 的基本要素是:

  • 訊息傳遞
  • 封裝
  • 動態繫結

顯然,創造了這個術語,併為我們帶來 OOP 的 Alan Kay 並不認為繼承和子類多型性是 OOP 的必要組成部分。

OOP 的本質

訊息傳遞和封裝的結合有一些重要的目的:

  • 避免共享可變狀態,通過封裝狀態並將其他物件與本地狀態更改隔離開來。影響另一個物件狀態的唯一方法是通過傳送訊息來請求(而不是命令)該物件來更改它。狀態變化由區域性控制,蜂窩級別,而不是暴露於共享訪問。
  • 解耦 — 通過訊息傳遞 API,訊息傳送者與訊息接收者鬆散耦合。
  • 適應變化,通過動態繫結在執行時適應變化。Alan Kay 認為執行的時適應性給 OOP 帶來很多至關重要的好處。

這些想法的靈感來自於生物細胞和/或網路上的個人計算機,受到 Alan Kay 的生物學背景和 Arpanet(早期版本的網際網路)的設計影響。在想法提出之前,Alan Kay 就想象軟體執行在巨大的分散式計算機(網際網路)上,個人計算機就像生物細胞一樣,在自己的孤立狀態下獨立執行,並通過訊息傳遞進行通訊。

“我意識到細胞與整個計算機類比會脫離資料 […]”
~ Alan Kay

通過“脫離資料”,Alan Kay 意識到共享可變狀態的問題和由共享資料引起的耦合問題 — 這些都是今天的普遍問題。

但是在 20 世紀 60 年代後期,ARPA 程式設計師對在程式編寫之前選擇資料模型的需求感到沮喪。程式與特定資料結構過度耦合使得程式無法適應變化。他們希望對資料進行更加統一的處理。

“[…] OOP 的核心是不需要再關心物件內部的內容。使用不同機器和不同語言開發的物件應該可以互相交通訊 […]” ~ Alan Kay

物件可以被抽象出來並隱藏資料結構的實現方法。物件的內部實現可以在不破壞軟體系統其他部分的情況下進行更改。實際上,通過遲約束,完全不同的計算機系統可以接管物件的功能,並且軟體可以繼續工作。與此同時,物件可以公開一個標準介面,該介面可以處理物件在內部使用的任何資料結構。相同的介面可以使用連結串列、樹和流等。

Alan Kay 還將物件視為代數結構,這些結構可以通過數學來證明它們的行為:

“我的數學背景使我意識到每個物件可能有幾個與之相關的代數式,可能有代數式族,而這些將會非常有用。”
~ Alan Kay

這已經被證明是對的,並且構成了 promises 和 lenses 等物件的基礎,這兩個物件都受到類別理論的啟發。

Alan Kay 對於物件具有代數性質的觀點將允許物件提供公式化驗證,確定性行為和改進的可測試性,因為代數本質上是遵循方程式規則的操作。

在程式設計師術語中,代數就像是由函式(操作)組成的抽象,這些函式必須通過單元測試強制執行的特定規則(公理/方程式)。

這樣的想法在大多數 C 族面嚮物件語言中被遺忘了幾十年,包括 C++、Java 和 C# 等,但現在在它們的最新版本中正在逐漸找回這樣的特性。

你可能會說程式設計世界正在重新發現面嚮物件語言環境中的函數語言程式設計和理性思維帶來的優勢。

就像之前的 JavaScript 和 Smalltalk 一樣,大多數的現代面嚮物件語言正變得越來越“多正規化”。已經沒有理由在函數語言程式設計和麵向物件程式設計之間進行選擇。當我們追溯它們各自的歷史本源時,它們是既相互相容,又相互互補的。

因為它們有如此多共通的特性,我想說 JavaScript 是針對世界對物件導向程式設計的誤解的一種反擊。Smalltalk 和 JavaScript 都支援:

  • 物件
  • 主類函式和閉包
  • 動態型別
  • 遲繫結(函式/方法在執行時可更改)
  • 不支援類繼承的面嚮物件語言

什麼是物件導向程式設計的必要特性(根據 Alan Kay 的說法)?

  • 封裝
  • 訊息傳遞
  • 動態繫結(程式在執行時進化/適應的能力)

什麼是非必要的特性?

  • 類繼承
  • 物件/函式/資料的特殊處理
  • new關鍵字
  • 多型性
  • 靜態型別
  • 將類識別為“型別”

如果您是 Java 或 C# 的開發者,您可能會認為靜態型別和多型性是必不可少的成分,但Alan Kay 傾向於以代數形式處理共性行為。例如,來自 Haskell:

fmap :: (a -> b) -> f a -> f b
複製程式碼

這是仿函式對映簽名,它通常用於未指定型別 ab,在 a 的仿函式的上下文中應用從 ab 的函式來生成 b 的仿函式。仿函式是數學術語,含義是“支援對映操作”。如果您熟悉 JavaScript 中的 [].map(),那麼您已經知道它的含義是什麼。

這是 JavaScript 中的兩個例子:

// isEven = Number => Boolean
const isEven = n => n % 2 === 0;

const nums = [1, 2, 3, 4, 5, 6];

// map takes a function `a => b` and an array of `a`s (via `this`)
// and returns an array of `b`s.
// in this case, `a` is `Number` and `b` is `Boolean`
const results = nums.map(isEven);

console.log(results);
// [false, true, false, true, false, true]
複製程式碼

.map() 方法是通用的,因為 ab 可以是任何型別,而 .map() 處理它剛剛好,因為陣列是代數上實現 functor 規則的資料結構。型別與 .map() 無關,因為它不會嘗試直接操作它們,而是通過一個函式來返回正確的期望型別。

// matches = a => Boolean
// here, `a` can be any comparable type
const matches = control => input => input === control;

const strings = [`foo`, `bar`, `baz`];

const results = strings.map(matches(`bar`));

console.log(results);
// [false, true, false]
複製程式碼

泛型型別關係很難在 TypeScript 這樣的語言中完全正確地表達,但在 Haskell 的 Hindley Milner 型別中很容易表達,因為它支援更高階的型別(型別的型別)。

大多數型別系統有太多的限制,不允許自由表達動態和函式的想法,例如函式組合,自由物件組合,執行時物件擴充套件,組合器,鏡頭等。換句話說,靜態型別經常會使得組合軟體的編寫更加困難。

如果型別系統限制太多(例如 TypeScript,Java),您將不得不為了實現相同的目標編寫更加複雜的程式碼。這並不是說靜態型別是一個糟糕的主意,或者說所有靜態型別的實現都具有同樣的限制性。我使用 Haskell 型別系統遇到的問題就很少。

如果你熱衷靜態型別,你不會介意這些限制,這會是你更加有掌控力,但是如果你發現一些本文中建議中提到的那些困難,比如很難輸入組合函式和複合代數結構,因此就歸咎於型別系統,這樣並不是可取的想法。人們喜歡 SUV 能給你帶來舒適性,但不會有人抱怨 SUV 不能讓你飛起來。如果為了追求那樣的體驗,您需要一輛擁有更多自由度的車輛。

如果這樣的限制能使您的程式碼更簡單,那就太棒了!但是如果這些限制迫使你編寫更復雜的程式碼,那麼這樣的限制可能就是錯誤的。

什麼是物件?

多年來,物件已經被賦予大量的含義。我們在 JavaScript 中稱之為“物件”的只是複合資料型別,沒有任何基於類的程式設計或 Alan Kay 所說的訊息傳遞的含義。

在 JavaScript 中,物件可以並且通常必須支援封裝,訊息傳遞,通過方法進行行為共享,甚至子類多型(儘管使用委託鏈而不是基於型別的排程)。您可以將任何函式賦給任何屬性。您可以動態建立物件行為,並在執行時更改物件的含義。JavaScript 還支援使用閉包進行封裝以實現隱私。但所有這些都是選擇加入的行為。

我們現在認為物件只是一個複合資料結構,並不需要其他更多的含義。但是使用這樣的物件進行程式設計並不會使您的程式碼比使用函式程式設計更加的“物件導向”。

物件導向不再是真正的物件導向

因為現代程式語言中的“物件”意味的比 Alan Kay 設想的要少得多,所以我使用“元件”而不是“物件”來描述真正的物件導向的規則。在 JavaScript 中,許多 objects 可以由其他程式碼直接擁有和操作,但是元件應該封裝和控制它們自己的狀態。

真正的物件導向的含義:

  • 使用元件程式設計(就是 Alan Kay 所說的“物件”)
  • 必須封裝元件狀態
  • 使用訊息傳遞進行物件間通訊
  • 元件可以在執行時被建立/更改/替換

大多陣列件行為可以使用代數型資料結構進行定義。不需要繼承。元件可以通過共用函式和模組化匯入來進行重用,而無需共享資料。

在 JavaScript 中操作 objects 或使用 class inheritance 並不意味著你在“物件導向”。但是這樣使用元件就意味著“物件導向”。但流行的用法是如何定義單詞,所以也許我們應該放棄物件導向並將其稱為“面向訊息的程式設計(MOP)”而不是“物件導向的程式設計(OOP)”?

拖把被用來清理垃圾是一種巧合嗎?

什麼是好的面向訊息的程式設計(MOP)

在大多數現代軟體中,有一些負責管理使用者互動的介面,還有管理應用程式狀態(使用者資料),程式碼管理系統或者網路 I/O 的程式碼。

每個系統可能都需要長期駐留的程式,例如事件監聽器,跟蹤網路連線狀態,介面元素狀態和應用程式狀態等。

好的 MOP 意味著系統是通過訊息排程與其他元件通訊,不是各個系統去直接操縱彼此的狀態。當使用者單擊儲存按鈕時,可能會排程“儲存”訊息,應用程式狀態元件可能會將其解析並轉發到狀態更新處理程式(例如純縮減器函式)。也許在狀態更新之後,狀態元件可能會向介面元件傳送一個“狀態更新”的訊息,而介面元件又將解析狀態,協調介面的哪些部分需要更新,並將更新後的狀態轉發給處理介面的子元件。

同時,網路連線元件可能正在監視使用者與網路上另一臺計算機的連線,偵聽訊息以及排程更新狀態以便在遠端計算機上儲存資料。它在內部保持網路心跳計時器,無論當前連線是線上還是離線。

這些系統不需要知道系統其他部分的細節。只需要關注自身的模組化問題。系統元件是可分解和可重新組合的。它們實現標準化介面,以便它們能夠相互操作。只要介面滿足,您就可以用使用其他實現方式來替換當前的方式,或在不同的事物之間使用相同的訊息通訊。你甚至可以在執行時進行切換,一切都會照常工作。

同一個軟體系統的元件甚至都不需要位於同一臺機器上。系統可以是分散式的。網路儲存可能會在分散式儲存系統中共享資料,比如 IPFS,因此使用者不會依賴任何特定計算機的執行狀況來確保其資料的安全,以免遭受來自黑客的安全威脅。

物件導向部分受到 Arpanet 的啟發,Arpanet 的目標之一是構建一個分散式網路,可以抵禦像原子彈那樣的攻擊。根據 DARPA 總監 Stephen J. Lukasik 在 Arpanet 開發期間所說(“為什麼建立 Arpanet”):

“我們的目標是開發新的計算機技術,以滿足軍事上抵抗核威脅的需要,實現對美國核力量的控制,並改善軍事戰術和管理決策。”

Arpanet 的主要推動力是方便而不是核威脅,其明顯的防禦優勢是後來提出的。ARPA 使用三個獨立的計算機終端與三個獨立的計算機研究專案進行通訊。Bob Taylor 想要一個單獨的計算機網路將每個專案與其他專案連線起來。

一個好的 MOP 系統可以在應用程式執行時使用可熱插拔的元件來共享網際網路的健壯性。如果使用者在使用手機時因為進入一條隧道而發生離線,它可以繼續工作。如果颶風將其中一個資料中心的電源摧毀,它仍然可以繼續執行。

現在是時候讓軟體世界放棄失敗的類繼承實驗了,轉而接受最初定義物件導向時所秉承的數學和科學精髓。

現在是時候開始構建更靈活,更具彈性,更容易組合的軟體了,讓 MOP 和函式程式設計協調工作。

注意:MOP 的首字母縮略詞已被用於描述“面向監視的程式設計”,OOP 不太可能的會悄無聲息的消失。

如果 MOP 沒有成為程式設計術語,請不要沮喪。
在你的 OOP 基礎上開始 MOP。

通過 EricElliottJS.com 瞭解更多資訊

EricElliottJS.com 的會員可以觀看函式程式設計的相關視訊。如果您還不是會員,請今天註冊


Eric Elliott“程式設計 JavaScript 應用程式”(O`Reilly)的作者,軟體導師平臺 DevAnywhere.io 的聯合創始人。他是很多軟體的貢獻者, Adobe Systems,Zumba Fitness,華爾街日報,ESPN,BBC ,還和頂級的唱片藝人合作,包括 Usher、Frank OceanMet

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到掘金翻譯計劃對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的本文永久連結即為本文在 GitHub 上 的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章