工程設計論——如何寫好工程代碼
精選一、內容概述
1. 從抽象的工程設計論角度闡述了如何寫好一份代碼。闡述了設計模式和設計原則的底層原理。
2. 解釋了設計模式與設計原則適用的場景及局限性。工程設計論是在有限設計能力下對被設計對象進行的認知和進行逆運算的過程。在不符合這一條件的領域,不應當死扣設計模式與設計原則。在軟件領域,一個顯而易見的例子就是不要在極度追求性能的代碼中死扣設計模式與設計原則。
3. 解釋了設計原則中的單一職責原則為何難以掌握和運用。
4. 面向接口設計是軟件系統(tǒng)設計的最終形態(tài),對開發(fā)流程中先寫單例再開發(fā)的原因做了解釋。
二、理論基礎
1. 哲學基礎:羅素《哲學問題》。
2. 數(shù)學基礎:矩陣理論,工程控制論。
3. 工程基礎:一定工程設計經(jīng)驗,如代碼開發(fā)等。
4. 設計科學基礎:謝友柏老師的《設計科學與設計競爭力》,Nam Suh 的《公理設計》。
三、什么是設計——設計和計算與認知之間的聯(lián)系
一門科學的建立,應當首先明確本學科的局限性,確定本學科最基本的問題與框架。明確的基本框架應能夠迅速得到一門學科的基礎結論與研究方法;明確的基本問題可以用于檢驗上述的結論與方法。指出自然界中每一杯水中都有金元素并不能對金礦的發(fā)現(xiàn)起到什么促進作用。設計科學的現(xiàn)在的發(fā)展應該做減法而不是做加法。對于設計所具備的特征,有很多描述。這些描述最基本的共同點是設計是需要達到一定的目標的(即需求)。其他特征并不是設計最基本的特征。例如最優(yōu)化設計中就沒有需求變更,logo 設計中就沒有系統(tǒng)故障。
如果認同需求是設計的共同點,那么搞清楚需求是什么則是重要的。 大部分人都認為,在我們的實際工作中,需求是不明確的,不完整的。那我們不妨用辯證的思維來考慮這個問題的反面,什么是明確的,完整的需求?一份完整的需求,對于所有人而言都是清晰的,不會產生什么不一樣的理解。那么對于什么樣的產品能夠滿足相應的需求,也應該是清晰的。用集合論的話來說,一個集合被其外延所完全確定。換句話說,如果需求能夠被一個確定的驗收方式來定義,比如說單元測試,那么這份需求可以說是明確和完整的。
我們還需要更進一步地探討什么是驗收。以單元測試為例,我們用單元測試來輸出一個 True 或是輸出一個 False;如果認為單元測試本身是一個函數(shù),那驗收就是要求被設計對象在該函數(shù)下的相必須為 True。那么,如果我們的需求足夠簡單,會發(fā)生什么情況?比如說我們的需求是找到一個 x,使其滿足 x+1=0,我們一般稱這種問題為求解,或者是逆運算??梢钥吹?,當我們對需求及其實現(xiàn)方式的認識完全清晰的時候,需求將退化成為一個函數(shù),設計將退化成為逆運算的過程。
設計的過程中,我們對于需求及實現(xiàn)方式的認識是不全面的,這是其與逆運算不同的核心點(而不是需求不明確或者是需求會發(fā)生變更)。例如化工產品的合成路線設計,例如高效排序算法的設計,都不存在需求本身不明確的問題。認知的不全面迫使我們需要在設計的過程中,一邊對需求及其實現(xiàn)方式進行認知,獲取更多的知識,一邊進行求逆運算,找到能夠滿足需求的實現(xiàn)方式。
四、工程設計的過程
在進行工程設計的過程中,對于認知和計算的交替流程,我們總結了一套行之有效的經(jīng)驗,即對需求的拆分和組合。
我們剛剛已經(jīng)說過,需求本質上是要求一個對象,在某個函數(shù)下的像具備某些特征,這本質上是一種約束。而“被設計對象由 A,B 兩個組件構成,A 具備 123 特征,B 具備 456 特征”,這和需求的描述方式?jīng)]有什么不同,本質上也是一種約束。也就是說,分拆本身也是對被設計對象的一種約束,只不過滿足分拆約束的對象并不一定滿足需求的約束。因此我們不妨把分拆的約束當作一種對被設計對象的弱約束。
因而工程設計可以被總結成為如下的流程:
1. 根據(jù)對需求的相關研究,給出實現(xiàn)方式的弱約束,我們一般采用對系統(tǒng)拆分的方式來進行弱約束。在軟件領域,最常見的弱約束就是對組件劃分的約束,各個部件之間的依賴關系,接口的定義,數(shù)據(jù)交互方式之間的約束。(認知過程,我們一般稱之為需求拆解與架構設計)。
2. 利用第一步的弱約束,來對需求中的強約束的實現(xiàn)方式進行具體的分析和求解。(逆運算過程,我們一般稱之為編碼)。
我們剛剛已經(jīng)說明了,分拆本質上也是一種約束。第二步中的求解結果,仍舊有可能是一種對子系統(tǒng)需求,此時就需要我們繼續(xù)進行更加細化的設計。
引入弱約束這個概念,是因為在我們對被設計對象一無所知的情況下,研究如何實現(xiàn)相應的需求是相對困難的。那么我們不妨假設被設計對象具備某些性質(這種假設往往也強依賴于個人經(jīng)驗),并將這些假設性質(比如說接口)作為研究如何實現(xiàn)的一種工具和框架。
例如在代碼設計中,拆分為 A,B 兩個模塊并進行并行設計時,如果在 A 模塊的實現(xiàn)流程完全不知道B模塊的信息,那么將會對 A 模塊的設計產生巨大的阻礙(比如前端完全不知道后端的數(shù)據(jù)格式)。但是,B 模塊的具體實現(xiàn)方式還未確定,此時 A 模塊也不可能對 B 模塊的信息由完整的了解,且并不是每一個 B 模塊的信息對于其他模塊都是有用的(比如后端選用的數(shù)據(jù)庫格式,后端部署的位置,后端的實現(xiàn)方式)。所以我們需要折中的對 B 模塊進行約束(比如規(guī)定接口),使得 A 模塊能夠獲得必要的相關信息。了解過認知論的同學也應該知道,這種接口本身就是一種對 B 模塊的認知(參照羅素的感覺材料或是我在前序文章中所述的“關系”)。我認為這是依賴注入的底層邏輯,也是面向接口設計將成為軟件設計的最終形態(tài)的底層依據(jù)。
公式化地來描述上述流程,對于一個找到滿足的設計問題,我們將這個問題分為兩步:
1. 將 J(X)=0 拆分成為。
2. 根據(jù)的性質,找到使得
的
的具體值,例如
;并同時研究
,找到
的具體形式,例如
。
在這個例子中,工程設計與科學研究后進行計算的最大區(qū)別即在于,第二步中的具體實現(xiàn)過程是并行的。各個組件的實現(xiàn)的并行在軟件工程中是常見的(前后端分別編碼,最后進行調試即是如此)。我們當然可以在完全研究清楚 J(X) 的性質下,再去進行設計。限制我們不去這么做的條件,并非是這樣得到的產品效果一定不好,而是設計需要投入的工期與人力有限制。完全設計好前端之后,再去進行后端設計,當然是可以的,但是這種串行化的工作模式,顯而易見的會對工期造成負面影響。
為了使得這種拆分方式可行,獨立職責的原則就需要被引進以保證最后的組裝工作順利完成。在上一步中,我們的工作是并行的,意味著我們并不知道所需要取得的值是多少。如果我們最終研究得到:
。
那我們顯然是找不到相應的解的。這就需要我們保證 f({X}),g({X}),m({X}) 之間的相互獨立。我們對拆分地獨立性及其負面影響進行進一步地探討:
1. 強獨立:存在一個定義域為兩個自變量組X構成的二元空間,值域為自變量組X的函數(shù)融合函數(shù)U;使得對于任意的。
2. 弱獨立:對于任意的。
3. 不獨立:存在。
對于強獨立而言,只要組合函數(shù) J,及部分函數(shù) f,g 的研究和求解是成功的,設計即是成功的。強獨立的意思是,如果我們分別找到兩個取值,使得部分函數(shù) f,g 的值取到了我們想要的結果 m,n;那我們可以根據(jù)
找到一個綜合的解
使得部分函數(shù) f,g 同時取到我們想要的值。比如說對于:
。那么對于任意一個我們要求的 f,g 的取值,我們都可以將其用
來保證
。
對于弱獨立而言,同時對組合函數(shù) J,及部分函數(shù) f,g 進行研究,可能會帶來組合上的困難,但是不至于使得設計徹底失敗。比如說對于。對于任意的 m,n,我們都是能找到
來滿足我們的需求的(注意這里一般是由研究組合函數(shù)J的同學,來提出對部分函數(shù) f,g 詳細取值要求 m,n)。由于對函數(shù) g
進行研究和設計的人,事先可能不知道
,他們完全可能設計出來
的方式。因此這種情況,需要后期的合作與調試,才能完成整個設計。
對于不獨立而言,同時對組合函數(shù) J,及部分函數(shù) f,g 進行研究,可能會使得設計徹底失敗。比如說。研究組合函數(shù)J的同學最終得到的答案可能是
,這顯然是無解的。因此這個拆分可以認為是失敗的。
這一規(guī)則對應于軟件領域中的單一職責原則,有人評論這一原則是較為難以運用和掌握的(“單一職責原則是最簡單但又最難運用的原則”)。事實確實如此,接下來我們將對這一點進行探討。
我們換一種看起來正確的模棱兩可的表述更方便我們發(fā)現(xiàn)問題在哪。這個陳述是:獨立的功能應當由獨立的類來實現(xiàn)。那么,問題出現(xiàn)了。我們怎么去判斷兩個功能之間相互獨立?熟悉哲學,并對哲學中對“Free”的討論有接觸的人會很快反應過來,“Free”這個詞必然是建立于某種映射之上,單獨說 A 與 B“Free”沒有任何意義。家庭教育和學校教育是否獨立?道德教育和智力教育是否獨立?從不同的角度會有不一樣的答案。從時間上,家庭教育和學校教育相互獨立;從評分標準上,道德教育和智力教育也相互獨立。如果把教育也作為一種設計,我們是應該把教育劃分成為家庭教育和學校教育,還是劃分成為道德教育和智力教育?劃分的依據(jù)究竟應該是什么?
顯而易見的事情是,我們所能夠接受的判斷功能之間的相互獨立的依據(jù),應該是從其實現(xiàn)方式上相互獨立。那么上面那句話,就可以改寫成為:實現(xiàn)上獨立的功能應當被獨立地實現(xiàn)。這有點像一句政治正確的廢話,其具體的運用強依賴于設計人員對于相關領域的事前經(jīng)驗與判斷。不具備相關領域的經(jīng)驗,進行功能劃分必然會出現(xiàn)一些搞笑的結果。這就是單一職責原則是最簡單,也最困難的原則的原因。
五、總結與局限
設計是在對需求的認知不完整的情況下,對被設計對象進行求解的一個過程。這就迫使我們需要一邊認識被設計對象,一邊進行求解。為了并行化地進行這一過程,也為了使得對被設計對象地認識有初步的研究工具和基礎,我們總結出了一套利用分拆提供弱約束,并基于這種分拆,來并行進行不同組件之間的設計的流程。由于分拆只能提供關于被設計對象的較弱認識,因此依賴倒置和面向接口設計是必須的。為了使得并行化的設計最終可以被組裝,單一職責原則(獨立原則)是必須的。
可以看到,整個設計理論是必須基于對需求的認知不完整,且需要低成本(首要的是時間成本)地完成設計這一條件。對于設計周期比較長,認知較為充分的領域,設計理論并不適用。完全只用設計模式來衡量設計的好壞,也是不可取的。這方面的反例有很多,LeetCode 上面的題目,恐怕沒有哪一個符合了設計模式,比如說找鏈表倒數(shù)第k個節(jié)點中的雙指針就是一個典型。對于人體而言,也并不遵循什么單一職責原則,甚至可以說耦合地不像,人在饑餓的時候,可以分解蛋白質來供能;我們在飛機設計過程中,有考慮過在液壓油泄露時,拿燃油來充當液壓油么?一些經(jīng)典設計也并不遵循設計理論與原則,例如活塞環(huán)既能夠防止漏氣,又能夠降低摩擦磨損,這顯然也不是符合獨立公理的。
只有對設計科學的底層邏輯有著深入的研究,才能使得這門科學發(fā)揮其真正的作用。雖然本文盡可能地對這個領域進行了一些減法地操作,略去了一些不核心的要素,但是無論在理論上,還是例子上,都沒有能夠提供一個真正能夠被驗證成為正確或是錯誤的想法或是命題。本文甚至連錯誤都算不上,這無論如何都是讓人不滿意的。
六附—利用分拆來設計系統(tǒng)的一個例子
很多設計領域的文章提出的例子,都是一些已有的設計;或是拿著根本沒有市場的需求來設計一款產品。這種先射箭后畫靶的行為并不能促進科學的發(fā)展。因此找一個大家都熟知的領域,提出解決起來較為有難度,但是需求明確的問題來作為探討的例子。很幸運的是,我的確解決了我自己提出的問題。
在機械領域,平面桿件機構的設計是最基本的問題。例如對下圖中這種四桿機構,我們經(jīng)常會進行擺角的設計等工作。那么,我們能不用勻速的電機和平面桿件,使得平面桿件上的某一點有著指定的軌跡?例如用平面桿件畫一只兔子?
對于這個問題,我們梳理我們已經(jīng)知道的知識,來給出一些弱約束:
1. 一個確定的平面桿組機構,其上任意一點的位置都是一個隨時間變化的周期函數(shù)。我們可以用復數(shù)域上的函數(shù)來進行表示,即:。
2. 由勻速電機帶動的桿件(主動件),其終點的軌跡是一個圓,且這個圓的運動規(guī)律與其他桿件無關。
3. 不由勻速電機帶動的桿件(從動件)的軌跡,由主動件的運動軌跡和其與主動件的鏈接所決定。
那么,我們再由拆分給出另外的弱約束,以解決這一問題:
A.最終設計的平面桿組,由主動件和一些連接組構成。這些連接組應當具備兩個自由端點,且連接組上一點在運動中始終是這兩個自由端點的中點,即。
在這樣一個弱約束下,我們的問題就變?yōu)榱耍?/p>
1. 如何通過一些圓周運動,及建立在其上的加法體系,擬合任意一個周期運動。
2. 如何找到一個連接組,使得其具備上述條件。
問題一的答案由傅里葉變換給出:。
問題二可以由如下桿組完成,轉動副 2 始終為轉動副 1,3 的中點:
最終的設計,我用了 16 個主動件,及 16 個連接組,共計 80 個桿件,得到的結果已經(jīng)在上圖中展示了。
誠實而言,我認為這個例子在說明弱約束和強約束,以及拆分對于工程設計的必要性方面,仍舊難以擺脫先射箭后畫靶的嫌疑。但是至少,我不認為我設計的機構,就是本問題的最優(yōu)解;我想本問題用以說明工程設計并不能得到最好的設計這一點,還是足夠的。