面向對象編程和根本狀態
早在 2015 年,Brian Will 撰寫了一篇有挑釁性的博客:面向對象編程:一個災難故事。他隨後發布了一個名為面向對象編程很糟糕的視頻,該視頻更加詳細。
我建議你花些時間觀看視頻,下面是我的一段總結:
OOP 的柏拉圖式理想是一堆相互解耦的對象,它們彼此之間發送無狀態消息。沒有人真的像這樣製作軟體,Brian 指出這甚至沒有意義:對象需要知道向哪個對象發送消息,這意味著它們需要相互引用。該視頻大部分講述的是這樣一個痛點:人們試圖將對象耦合以實現控制流,同時假裝它們是通過設計解耦的。
總的來說,他的想法與我自己的 OOP 經驗產生了共鳴:對象沒有問題,但是我一直不滿意的是面向對象建模程序控制流,並且試圖使代碼「正確地」面向對象似乎總是在創建不必要的複雜性。
有一件事我認為他無法完全解釋。他直截了當地說「封裝沒有作用」,但在腳註後面加上「在細粒度的代碼級別」,並繼續承認對象有時可以奏效,並且在庫和文件級別封裝是可以的。但是他沒有確切解釋為什麼有時會奏效,有時卻沒有奏效,以及如何和在何處劃清界限。有人可能會說這使他的 「OOP 不好」的說法有缺陷,但是我認為他的觀點是正確的,並且可以在根本狀態和偶髮狀態之間劃清界限。
如果你以前從未聽說過「 根本 」和「 偶發 」這兩個術語的使用,那麼你應該閱讀 Fred Brooks 的經典文章《沒有銀彈》。(順便說一句,他寫了許多很棒的有關構建軟體系統的文章。)我以前曾寫過關於根本和偶發的複雜性的文章,這裡有一個簡短的摘要:軟體是複雜的。部分原因是因為我們希望軟體能夠解決混亂的現實世界問題,因此我們將其稱為「根本複雜性」。「偶發複雜性」是所有其它的複雜性,因為我們正嘗試使用硅和金屬來解決與硅和金屬無關的問題。例如,對於大多數程序而言,用於內存管理或在內存與磁碟之間傳輸數據或解析文本格式的代碼都是「偶發的複雜性」。
假設你正在構建一個支持多個頻道的聊天應用。消息可以隨時到達任何頻道。有些頻道特別有趣,當有新消息傳入時,用戶希望得到通知。而其他頻道靜音:消息被存儲,但用戶不會受到打擾。你需要跟蹤每個頻道的用戶首選設置。
一種實現方法是在頻道和頻道設置之間使用 映射 (也稱為哈希表、字典或關聯數組)。注意,映射是 Brian Will 所說的可以用作對象的抽象數據類型(ADT)。
如果我們有一個調試器並查看內存中的映射對象,我們將看到什麼?我們當然會找到頻道 ID 和頻道設置數據(或至少指向它們的指針)。但是我們還會找到其它數據。如果該映射是使用紅黑樹實現的,我們將看到帶有紅/黑標籤和指向其他節點的指針的樹節點對象。與頻道相關的數據是根本狀態,而樹節點是偶髮狀態。不過,請注意以下幾點:該映射有效地封裝了它的偶髮狀態 —— 你可以用 AVL 樹實現的另一個映射替換該映射,並且你的聊天程序仍然可以使用。另一方面,映射沒有封裝根本狀態(僅使用 get()
和 set()
方法訪問數據並不是封裝)。事實上,映射與根本狀態是儘可能不可知的,你可以使用基本相同的映射數據結構來存儲與頻道或通知無關的其他映射。
這就是映射 ADT 如此成功的原因:它封裝了偶髮狀態,並與根本狀態解耦。如果你思考一下,Brian 用封裝描述的問題就是嘗試封裝根本狀態。其他描述的好處是封裝偶髮狀態的好處。
要使整個軟體系統都達到這一理想狀況相當困難,但擴展開來,我認為它看起來像這樣:
- 沒有全局的可變狀態
- 封裝了偶髮狀態(在對象或模塊或以其他任何形式)
- 無狀態偶發複雜性封裝在單獨函數中,與數據解耦
- 使用諸如依賴注入之類的技巧使輸入和輸出變得明確
- 組件可由易於識別的位置完全擁有和控制
其中有些違反了我很久以來的直覺。例如,如果你有一個資料庫查詢函數,如果資料庫連接處理隱藏在該函數內部,並且唯一的參數是查詢參數,那麼介面會看起來會更簡單。但是,當你使用這樣的函數構建軟體系統時,協調資料庫的使用實際上變得更加複雜。組件不僅以自己的方式做事,而且還試圖將自己所做的事情隱藏為「實現細節」。資料庫查詢需要資料庫連接這一事實從來都不是實現細節。如果無法隱藏某些內容,那麼顯露它是更合理的。
我對將面向對象編程和函數式編程放在對立的兩極非常警惕,但我認為從函數式編程進入面向對象編程的另一極端是很有趣的:OOP 試圖封裝事物,包括無法封裝的根本複雜性,而純函數式編程往往會使事情變得明確,包括一些偶發複雜性。在大多數時候,這沒什麼問題,但有時候(比如在純函數式語言中構建自我指稱的數據結構)設計更多的是為了函數編程,而不是為了簡便(這就是為什麼 Haskell 包含了一些「 逃生出口 」)。我之前寫過一篇所謂「 弱純性 」的中間立場。
Brian 發現封裝對更大規模有效,原因有幾個。一個是,由於大小的原因,較大的組件更可能包含偶髮狀態。另一個是「偶發」與你要解決的問題有關。從聊天程序用戶的角度來看,「偶發的複雜性」是與消息、頻道和用戶等無關的任何事物。但是,當你將問題分解為子問題時,更多的事情就變得「根本」。例如,在解決「構建聊天應用」問題時,可以說頻道名稱和頻道 ID 之間的映射是偶發的複雜性,而在解決「實現 getChannelIdByName()
函數」子問題時,這是根本複雜性。因此,封裝對於子組件的作用比對父組件的作用要小。
順便說一句,在視頻的結尾,Brian Will 想知道是否有任何語言支持無法訪問它們所作用的範圍的匿名函數。D 語言可以。 D 中的匿名 Lambda 通常是閉包,但是如果你想要的話,也可以聲明匿名無狀態函數:
import std.stdio;
void main()
{
int x = 41;
// Value from immediately executed lambda
auto v1 = () {
return x + 1;
}();
writeln(v1);
// Same thing
auto v2 = delegate() {
return x + 1;
}();
writeln(v2);
// Plain functions aren't closures
auto v3 = function() {
// Can't access x
// Can't access any mutable global state either if also marked pure
return 42;
}();
writeln(v3);
}
via: https://theartofmachinery.com/2019/10/13/oop_and_essential_state.html
作者:Simon Arneaud 選題:lujun9972 譯者:geekpi 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive