再談“法自然”的設計思路
在上一篇論述(再談什麼是高層設計)裡,我終於有機會給出“恍惚”的定義了,我們可以從這個角度來重新,也是更加精確地定義什麼是“(道)法自然”的設計思路了。
設計包括很多方面(角度),很多工程師新手覺得設計是“我決定”選擇,有經驗的工程師都知道,設計是“我發現”被設計物件的特徵。基於後面這個理解的設計方法,就是“法自然”的設計方法。
我們拿一個例子來討論。比如我們要做一個簡單的功能:統計一個檔案的詞頻分佈,更簡單起見,我們假定這是英文,沒有中文斷句這樣的需求在裡面。你會怎麼分解這個系統的模組?
基於邏輯鏈好看的,“我決定”的設計方法可能會這樣進行設計:
分四個模組:
A- 檔案輸入模組
B- 分詞模組
C- 詞頻統計
D-顯示模組
這種設計方法在模組比較小的時候,常常是對的,而且,如果我們不能看進細節裡面去,也挑不出毛病。
“法自然”的設計方法不是這樣的,法自然的方法首先不認為分解模組是必要的。分解模組不是需求,是我們個人的“私慾”,分解模組不帶來收益(至少在你找到之前)。所以第一步的分解應該是隻有一個模組,然後我們思考這個模組的內部流程設計:
我們用getopt就可以取得入口引數,拿到檔案並開啟,然後順序讀入一個個字元,把不同單詞的結果寫入一個數據結構,完成統計後,基於這個資料結構把結果打印出來。
這不需要分解什麼模組,但我們真正擔心的問題是未來,後面我們會需要怎麼擴充套件這個系統?我想一種情形:
- 多檔案支援
- 多檔案型別支援
- 多種展示方式支援(比如文字列印,圖示展示等)
基於前面的推演和這裡對未來的期望,我們很容易就發現所有這些部分,哪裡的邏輯是和其他部分的聯絡是很薄弱的:掃描資料的表達形式。所以,我們需要的模組分解可能是這樣的:
A-框架
B-資料維護模組(支援加入資料和讀取資料,支援資料格式轉換)
C-展示程式
如果我們研究出來對未來的預期不是這樣的,比如,我們發現未來我們的擴充套件主要是近義詞合併統計,那麼我們需要的模組分解可能是這樣的:
A-框架
B-命令列語法Parser,獲取近義詞定義
C-可定製Parser程式
D-展示程式
這個例子太簡單了,可能這個意思不太容易看出來,但不知道讀者是否可以看出:模組的分解不是被設計出來的,它是在把所有邏輯都擺上去後,自動因為很重的內聚和薄弱的外部連結而“斷開”的。如果我們總從自己的思路出發來控制架構,而不是抱著一種“未定”的策略去研究細節,只會對效能和工作量造成傷害。一個系統的構架,是生成的,不是人為扭曲出來的。
用圖來表達,一開始我們可以這樣認知這個系統:

然後我們分析它內部的邏輯:

這個關聯關係是被功能驅動的,內部自然會形成一個有關聯強弱的結構,這樣,關聯比較弱的地方,就會斷裂成外在的模組:

我這裡標出一個稱為“代理”的節點,這表示這個節點將成為左邊的子系統和右邊的子系統的橋樑。如果我們要表演“做架構”的樣子,任何分塊都可以用代理來代替,因為我們其實把代理背後的節點全部體現在代理上。這樣的結果是兩個子系統的關聯並沒有發生變化。我們思考這個問題,或者獨立在一個概念空間中修改程式碼時需要面對的概念並沒有減少。
這就是我為什麼常說:希望用不確定的API去取代確定的API來“解耦”其實就是裝樣子。因為你丟開“程式介面”的表相,完全從概念關聯的角度來想這個問題,用myapi(void *data)來取代:
myapi1(struct GetServiceID *id)/myapi2(struct DoServer1 *req)/myapi3....
並沒有讓系統解耦(當然,這個寫法本身可以稱為其他抽象方法的輔助,那是另一個問題)。因為你根本就沒有改變他們之間的關聯度。進了myapi內部,你還是要區分這個data本身的內在屬性。你這個動作,僅僅把關聯延遲到提供myapi的模組內部而已。用上面那個圖來理解,你會製造另一個真正的斷裂點。
只是對“法自然”策略最基本的理解:是需求在驅動我們的設計,而不是我們自己的“定義”驅動了設計的成功。所以,所謂成功的設計,就是“不犯錯”的設計,你自己不作死就很好了,不是你引導設計的成功,而是需求在驅動你不得不做對應的選擇。我們要時刻忘掉個人的想法,讓每個設計解決(至少)一個問題,而不是用“架構理念”來決定你做什麼選擇。
但這個策略不限於此,如果我們理解了恍惚是什麼,我們馬上就面對了另一個問題:你根據需求已經決定了一套邏輯和空間,但你還有其他邏輯需要面對,比如Latency?比如低功耗?比如可靠性,可維護性,可服務性?可延續性?
這才是架構師面對的最難的問題,還是用前面那個詞頻統計的例子,假設你現在功能已經做得相當的產品化了,所有程式碼100%單元測試覆蓋,展現的介面請美工專門調過色,Parser裡面插了對應的進度呈現演算法,可以幾乎線性地在介面上呈現完成的進度。
但客戶不接受你這個反應速度,人家說了:我用競爭對手的產品,1G的資料只要5分鐘就出結果了,你這個,要半個小時,你怎麼處理?
你做過分析,發現問題就在你的架構上,你先把結果存到磁碟上,然後才一次呈現,你的對手是中間結果就開始呈現了,雖然取了巧,但很多使用者看了個開頭,心中已經有結論了,不需要後面繼續統計,這時人家的優勢就很明顯了。
你覺得要解決這個問題的,好了,你原來的分解就成為你的最大障礙,特別是你在一個很大的組織中,那些把Parser過程做到線性呈現的人可能還剛剛拿了個“2018年度全公司最佳創意獎”,結果你來這一出?人家會不跟你玩的好不好?
這還是小問題,這種問題你都解決不了,你就不要當什麼架構師了。但這是一個例子,它反映了你在名稱空間上面對的挑戰。本質上,架構師的很多資訊是來自團隊的,你自己看不完所有的細節。比如你可以要求,每處理10M的本文,必須輸出一次中間結果,但模組維護者告訴你,如果這樣的話,這個模組必須重寫,而且可能需要丟失十分之一的功能。你信還是不信?你不信?自己看程式碼去。一個模組可以這樣,100個呢?其出彌遠,其知彌少。你調查具體模組消耗的時間越多,你離開你的構架邏輯越遠。
更嚴重的問題是,你自己的思考也是建立在這種抽象上的,就算你自己親手寫的程式碼,但寫的時候是一回事,你去用它的時候,用的還是它的抽象概念(比如API)。你的思考受這個名稱空間的限制。
當你面對了這個問題,你就會發現,你只剩下一個選擇,就是直接面對“恍惚”,不要嘗試去細化這些概念。簡單說,你要直接基於“恍惚”去做臨時決策。功能,效能,功耗,可XX性,都是你面對的問題的一部分,讓他們都呈現在你的視野中,誰先變清晰就處理誰。但不要把這個作為你最終結論,時刻重新去找下一個“清晰”點,處理那個新的問題。這個操作策略,就叫“法自然”。
這個說出來好像解決不了任何問題。其實不是的,它給出了另一種具體操作策略:不要認為架構是開始寫下來的那篇文件,也不要在架構設計中僅寫選擇。
這兩個原則第一個很好理解,第二個我用一個生活的例子來解釋吧。
我們常常聽到有人問這樣的問題:語文重要還是數學重要?身體重要還是工作重要?老婆重要還是老媽重要?……
這些問題不解決問題我們都知道,但如果我們有三個假期的時間,這三個假期用來補語文還是補數學呢?補語文還是補數學就有個“選擇”的問題在裡面了。這就是我們架構設計常常面對的問題了:選效能還是選功能?
這個選擇每天都在變,架構設計怎麼寫?
架構要寫的是:這個市場是如何認識效能的,是如何認識相關的功能的,我們現在處於什麼實現水平,我們曾經做過的選擇是什麼,我們當下的選擇是什麼。
這樣帶來的好處是,讓主要矛盾可以被用更快的方式突出來,讓每個子角度受到一定的壓力,把資源投資最重要的邏輯上。比如你做一個記憶體分配演算法,很多人上來就開始考慮“執行緒安全”,但“執行緒安全”不是核心訴求,“分配記憶體”才是,這樣,“執行緒安全”當然好,但在沒有看到足夠收益前,這部分邏輯你就要給我退回去,讓路出來給我處理其他邏輯,比如我先考慮使用這個記憶體分配演算法的那個系統的執行模型,直到那個執行模型不得不需要執行緒安全的時候,你再給我加上這些要求,才是合理的。
這就是架構設計,或者說高層設計,相對程式碼的不同。越高層的設計,寫得越是恍惚。這也就是王小波在《萬壽寺》裡面說的,“一切都在無可挽回地走向庸俗”,當我們從恍惚最終走向具體,所有的浪漫,終究會走向庸俗。但我們需要的是,在我們終究走向庸俗的時候,我們是真得沒有問題要解決了,而不是我們半途而廢了。
todo:文中選的例子我都不太滿意,但正好有個靈感,就先把邏輯建出來,看看以後能否找到更好的例子來替換吧。