面向對象編程和根本狀態

早在 2015 年,Brian Will 撰寫了一篇有挑釁性的博客:面向對象編程:一個災難故事。他隨後發佈了一個名爲面向對象編程很糟糕的視頻,該視頻更加詳細。我建議你花些時間觀看視頻,下面是個人一段總結:html

OOP 的柏拉圖式理想是一堆相互解耦的對象,它們彼此之間發送無狀態消息。沒有人真的像這樣製做軟件,Brian 指出這甚至沒有意義:對象須要知道向哪一個對象發送消息,這意味着它們須要相互引用。該視頻大部分講述的是這樣一個痛點:人們試圖將對象耦合以實現控制流,同時僞裝它們是經過設計解耦的。linux

總的來講,他的想法與我本身的 OOP 經驗產生了共鳴:對象沒有問題,可是我一直不滿意的是面向對象建模程序控制流,而且試圖使代碼「正確地」面向對象彷佛老是在建立沒必要要的複雜性。git

有一件事我認爲他沒法徹底解釋。他直截了當地說「封裝沒有做用」,但在腳註後面加上「在細粒度的代碼級別」,並繼續認可對象有時能夠奏效,而且在庫和文件級別封裝是能夠的。可是他沒有確切解釋爲何有時會奏效,有時卻沒有奏效,以及如何和在何處劃清界限。有人可能會說這使他的 「OOP 很差」的說法有缺陷,可是我認爲他的觀點是正確的,而且能夠在根本狀態和偶發狀態之間劃清界限。github

若是你之前從未據說過「根本essential」和「偶發accidental」這兩個術語的使用,那麼你應該閱讀 Fred Brooks 的經典文章《沒有銀彈》。(順便說一句,他寫了許多很棒的有關構建軟件系統的文章。)我之前曾寫過關於根本和偶發的複雜性的文章,這裏有一個簡短的摘要:軟件是複雜的。部分緣由是由於咱們但願軟件可以解決混亂的現實世界問題,所以咱們將其稱爲「根本複雜性」。「偶發複雜性」是全部其它的複雜性,由於咱們正嘗試使用硅和金屬來解決與硅和金屬無關的問題。例如,對於大多數程序而言,用於內存管理或在內存與磁盤之間傳輸數據或解析文本格式的代碼都是「偶發的複雜性」。數據庫

假設你正在構建一個支持多個頻道的聊天應用。消息能夠隨時到達任何頻道。有些頻道特別有趣,當有新消息傳入時,用戶但願獲得通知。而其餘頻道靜音:消息被存儲,但用戶不會受到打擾。你須要跟蹤每一個頻道的用戶首選設置。編程

一種實現方法是在頻道和頻道設置之間使用映射map(也稱爲哈希表、字典或關聯數組)。注意,映射是 Brian Will 所說的能夠用做對象的抽象數據類型(ADT)。數組

若是咱們有一個調試器並查看內存中的映射對象,咱們將看到什麼?咱們固然會找到頻道 ID 和頻道設置數據(或至少指向它們的指針)。可是咱們還會找到其它數據。若是該映射是使用紅黑樹實現的,咱們將看到帶有紅/黑標籤和指向其餘節點的指針的樹節點對象。與頻道相關的數據是根本狀態,而樹節點是偶發狀態。不過,請注意如下幾點:該映射有效地封裝了它的偶發狀態 —— 你能夠用 AVL 樹實現的另外一個映射替換該映射,而且你的聊天程序仍然可使用。另外一方面,映射沒有封裝根本狀態(僅使用 get()set() 方法訪問數據並非封裝)。事實上,映射與根本狀態是儘量不可知的,你可使用基本相同的映射數據結構來存儲與頻道或通知無關的其餘映射。ruby

這就是映射 ADT 如此成功的緣由:它封裝了偶發狀態,並與根本狀態解耦。若是你思考一下,Brian 用封裝描述的問題就是嘗試封裝根本狀態。其餘描述的好處是封裝偶發狀態的好處。bash

要使整個軟件系統都達到這一理想情況至關困難,但擴展開來,我認爲它看起來像這樣:數據結構

  • 沒有全局的可變狀態
  • 封裝了偶發狀態(在對象或模塊或以其餘任何形式)
  • 無狀態偶發複雜性封裝在單獨函數中,與數據解耦
  • 使用諸如依賴注入之類的技巧使輸入和輸出變得明確
  • 組件可由易於識別的位置徹底擁有和控制

其中有些違反了我好久以來的直覺。例如,若是你有一個數據庫查詢函數,若是數據庫鏈接處理隱藏在該函數內部,而且惟一的參數是查詢參數,那麼接口會看起來會更簡單。可是,當你使用這樣的函數構建軟件系統時,協調數據庫的使用實際上變得更加複雜。組件不只以本身的方式作事,並且還試圖將本身所作的事情隱藏爲「實現細節」。數據庫查詢須要數據庫鏈接這一事實歷來都不是實現細節。若是沒法隱藏某些內容,那麼顯露它是更合理的。

我對將面向對象編程和函數式編程放在對立的兩極很是警戒,但我認爲從函數式編程進入面向對象編程的另外一極端是頗有趣的:OOP 試圖封裝事物,包括沒法封裝的根本複雜性,而純函數式編程每每會使事情變得明確,包括一些偶發複雜性。在大多數時候,這沒什麼問題,但有時候(好比在純函數式語言中構建自我指稱的數據結構)設計更多的是爲了函數編程,而不是爲了簡便(這就是爲何 Haskell 包含了一些「逃生出口escape hatches)。我以前寫過一篇所謂「弱純性weak purity」的中間立場

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: theartofmachinery.com/2019/10/13/…

做者:Simon Arneaud 選題:lujun9972 譯者:geekpi 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

相關文章
相關標籤/搜索