農林漁牧網

您現在的位置是:首頁 > 農業

工程設計論——如何寫好工程程式碼

2022-03-04由 阿里技術 發表于 農業

組間設計是什麼

工程設計論——如何寫好工程程式碼

一 內容概述

從抽象的工程設計論角度闡述瞭如何寫好一份程式碼。闡述了設計模式和設計原則的底層原理。

解釋了設計模式與設計原則適用的場景及侷限性。工程設計論是在有限設計能力下對被設計物件進行的認知和進行逆運算的過程。在不符合這一條件的領域,不應當死扣設計模式與設計原則。在軟體領域,一個顯而易見的例子就是不要在極度追求效能的程式碼中死扣設計模式與設計原則。

解釋了設計原則中的單一職責原則為何難以掌握和運用。

面向介面設計是軟體系統設計的最終形態,對開發流程中先寫單例再開發的原因做了解釋。

二 理論基礎

哲學基礎:羅素《哲學問題》。

數學基礎:矩陣理論,工程控制論。

工程基礎:一定工程設計經驗,如程式碼開發等。

設計科學基礎:謝友柏老師的《設計科學與設計競爭力》,Nam Suh的《公理設計》。

三 什麼是設計——設計和計算與認知之間的聯絡

一門科學的建立,應當首先明確本學科的侷限性,確定本學科最基本的問題與框架。明確的基本框架應能夠迅速得到一門學科的基礎結論與研究方法;明確的基本問題可以用於檢驗上述的結論與方法。指出自然界中每一杯水中都有金元素並不能對金礦的發現起到什麼促進作用。設計科學的現在的發展應該做減法而不是做加法。對於設計所具備的特徵,有很多描述。這些描述最基本的共同點是設計是需要達到一定的目標的(即需求)。其他特徵並不是設計最基本的特徵。例如最最佳化設計中就沒有需求變更,logo設計中就沒有系統故障。

如果認同需求是設計的共同點,那麼搞清楚需求是什麼則是重要的。 大部分人都認為,在我們的實際工作中,需求是不明確的,不完整的。那我們不妨用辯證的思維來考慮這個問題的反面,什麼是明確的,完整的需求?一份完整的需求,對於所有人而言都是清晰的,不會產生什麼不一樣的理解。那麼對於什麼樣的產品能夠滿足相應的需求,也應該是清晰的。用集合論的話來說,一個集合被其外延所完全確定。換句話說,如果需求能夠被一個確定的驗收方式來定義,比如說單元測試,那麼這份需求可以說是明確和完整的。

我們還需要更進一步地探討什麼是驗收。以單元測試為例,我們用單元測試來輸出一個True或是輸出一個False;如果認為單元測試本身是一個函式,那驗收就是要求被設計物件在該函式下的相必須為True。那麼,如果我們的需求足夠簡單,會發生什麼情況?比如說我們的需求是找到一個x,使其滿足x+1=0,我們一般稱這種問題為求解,或者是逆運算。可以看到,當我們對需求及其實現方式的認識完全清晰的時候,需求將退化成為一個函式,設計將退化成為逆運算的過程。

設計的過程中,我們對於需求及實現方式的認識是不全面的,這是其與逆運算不同的核心點(而不是需求不明確或者是需求會發生變更)。例如化工產品的合成路線設計,例如高效排序演算法的設計,都不存在需求本身不明確的問題。認知的不全面迫使我們需要在設計的過程中,一邊對需求及其實現方式進行認知,獲取更多的知識,一邊進行求逆運算,找到能夠滿足需求的實現方式。

四 工程設計的過程

在進行工程設計的過程中,對於認知和計算的交替流程,我們總結了一套行之有效的經驗,即對需求的拆分和組合。

我們剛剛已經說過,需求本質上是要求一個物件,在某個函式下的像具備某些特徵,這本質上是一種約束。而“被設計物件由A,B兩個元件構成,A具備123特徵,B具備456特徵”,這和需求的描述方式沒有什麼不同,本質上也是一種約束。也就是說,分拆本身也是對被設計物件的一種約束,只不過滿足分拆約束的物件並不一定滿足需求的約束。因此我們不妨把分拆的約束當作一種對被設計物件的弱約束。

因而工程設計可以被總結成為如下的流程:

根據對需求的相關研究,給出實現方式的弱約束,我們一般採用對系統拆分的方式來進行弱約束。在軟體領域,最常見的弱約束就是對元件劃分的約束,各個部件之間的依賴關係,介面的定義,資料互動方式之間的約束。(認知過程,我們一般稱之為需求拆解與架構設計)

利用第一步的弱約束,來對需求中的強約束的實現方式進行具體的分析和求解。(逆運算過程,我們一般稱之為編碼)

我們剛剛已經說明了,分拆本質上也是一種約束。第二步中的求解結果,仍舊有可能是一種對子系統需求,此時就需要我們繼續進行更加細化的設計。

引入弱約束這個概念,是因為在我們對被設計物件一無所知的情況下,研究如何實現相應的需求是相對困難的。那麼我們不妨假設被設計物件具備某些性質(這種假設往往也強依賴於個人經驗),並將這些假設性質(比如說介面)作為研究如何實現的一種工具和框架。

例如在程式碼設計中,拆分為A,B兩個模組並進行並行設計時,如果在A模組的實現流程完全不知道B模組的資訊,那麼將會對A模組的設計產生巨大的阻礙(比如前端完全不知道後端的資料格式)。但是,B模組的具體實現方式還未確定,此時A模組也不可能對B模組的資訊由完整的瞭解,且並不是每一個B模組的資訊對於其他模組都是有用的(比如後端選用的資料庫格式,後端部署的位置,後端的實現方式)。所以我們需要折中的對B模組進行約束(比如規定介面),使得A模組能夠獲得必要的相關資訊。瞭解過認知論的同學也應該知道,這種介面本身就是一種對B模組的認知(參照羅素的感覺材料或是我在

前序文章中

所述的“關係”)。我認為這是依賴注入的底層邏輯,也是面向介面設計將成為軟體設計的最終形態的底層依據。

公式化地來描述上述流程,對於一個找到滿足

工程設計論——如何寫好工程程式碼

的設計問題,我們將這個問題分為兩步:

將J(X)=0拆分成為

工程設計論——如何寫好工程程式碼

根據

工程設計論——如何寫好工程程式碼

的性質,找到使得

工程設計論——如何寫好工程程式碼

工程設計論——如何寫好工程程式碼

的具體值,例如

工程設計論——如何寫好工程程式碼

;並同時研究

工程設計論——如何寫好工程程式碼

,找到

工程設計論——如何寫好工程程式碼

的具體形式,例如

工程設計論——如何寫好工程程式碼

在這個例子中,工程設計與科學研究後進行計算的最大區別即在於,第二步中的具體實現過程是並行的。各個元件的實現的並行在軟體工程中是常見的(前後端分別編碼,最後進行除錯即是如此)。我們當然可以在完全研究清楚J(X)的性質下,再去進行設計。限制我們不去這麼做的條件,並非是這樣得到的產品效果一定不好,而是設計需要投入的工期與人力有限制。完全設計好前端之後,再去進行後端設計,當然是可以的,但是這種序列化的工作模式,顯而易見的會對工期造成負面影響。

為了使得這種拆分方式可行,獨立職責的原則就需要被引進以保證最後的組裝工作順利完成。在上一步中,我們的工作是並行的,意味著我們並不知道

工程設計論——如何寫好工程程式碼

所需要取得的值是多少。

如果我們最終研究得到

工程設計論——如何寫好工程程式碼

那我們顯然是找不到相應的解的。這就需要我們保證f({X}),g({X}),m({X})之間的相互獨立。我們對拆分地獨立性及其負面影響進行進一步地探討:

強獨立:存在一個定義域為兩個自變數組X構成的二元空間,值域為自變數組X的函式融合函式U;使得對於任意的

工程設計論——如何寫好工程程式碼

弱獨立:對於任意的

工程設計論——如何寫好工程程式碼

不獨立:存在

工程設計論——如何寫好工程程式碼

對於強獨立而言,只要組合函式J,及部分函式f,g的研究和求解是成功的,設計即是成功的。強獨立的意思是,如果我們分別找到兩個取值

工程設計論——如何寫好工程程式碼

,使得部分函式f,g的值取到了我們想要的結果m,n;那我們可以根據

工程設計論——如何寫好工程程式碼

找到一個綜合的解

工程設計論——如何寫好工程程式碼

使得部分函式f,g同時取到我們想要的值。比如說對於:

工程設計論——如何寫好工程程式碼

。那麼對於任意一個我們要求的f,g的取值,我們都可以將其用

工程設計論——如何寫好工程程式碼

工程設計論——如何寫好工程程式碼

來保證

工程設計論——如何寫好工程程式碼

對於弱獨立而言,同時對組合函式J,及部分函式f,g進行研究,可能會帶來組合上的困難,但是不至於使得設計徹底失敗。比如說對於:

工程設計論——如何寫好工程程式碼

。對於任意的m,n,我們都是能找到

工程設計論——如何寫好工程程式碼

來滿足我們的需求的(注意這裡一般是由研究組合函式J的同學,來提出對部分函式f,g詳細取值要求m,n)。由於對函式g進行研究和設計的人,事先可能不知道

工程設計論——如何寫好工程程式碼

,他們完全可能設計出來

工程設計論——如何寫好工程程式碼

的方式。因此這種情況,需要後期的合作與除錯,才能完成整個設計。

對於不獨立而言,同時對組合函式J,及部分函式f,g進行研究,可能會使得設計徹底失敗。比如說,

工程設計論——如何寫好工程程式碼

工程設計論——如何寫好工程程式碼

。研究組合函式J的同學最終得到的答案可能是

工程設計論——如何寫好工程程式碼

,這顯然是無解的。因此這個拆分可以認為是失敗的。

這一規則對應於軟體領域中的單一職責原則,有人評論這一原則是較為難以運用和掌握的(

“單一職責原則是最簡單但又最難運用的原則”

)。事實確實如此,接下來我們將對這一點進行探討。

我們換一種看起來正確的模稜兩可的表述更方便我們發現問題在哪。這個陳述是:獨立的功能應當由獨立的類來實現。那麼,問題出現了。我們怎麼去判斷兩個功能之間相互獨立?熟悉哲學,並對哲學中對“Free”的討論有接觸的人會很快反應過來,“Free”這個詞必然是建立於某種對映之上,單獨說A與B“Free”沒有任何意義。家庭教育和學校教育是否獨立?道德教育和智力教育是否獨立?從不同的角度會有不一樣的答案。從時間上,家庭教育和學校教育相互獨立;從評分標準上,道德教育和智力教育也相互獨立。如果把教育也作為一種設計,我們是應該把教育劃分成為家庭教育和學校教育,還是劃分成為道德教育和智力教育?劃分的依據究竟應該是什麼?

顯而易見的事情是,我們所能夠接受的判斷功能之間的相互獨立的依據,應該是從其實現方式上相互獨立。那麼上面那句話,就可以改寫成為:實現上獨立的功能應當被獨立地實現。這有點像一句政治正確的廢話,其具體的運用強依賴於設計人員對於相關領域的事前經驗與判斷。不具備相關領域的經驗,進行功能劃分必然會出現一些搞笑的結果。這就是單一職責原則是最簡單,也最困難的原則的原因。

五 總結與侷限

設計是在對需求的認知不完整的情況下,對被設計物件進行求解的一個過程。這就迫使我們需要一邊認識被設計物件,一邊進行求解。為了並行化地進行這一過程,也為了使得對被設計物件地認識有初步的研究工具和基礎,我們總結出了一套利用分拆提供弱約束,並基於這種分拆,來並行進行不同元件之間的設計的流程。由於分拆只能提供關於被設計物件的較弱認識,因此依賴倒置和麵向介面設計是必須的。為了使得並行化的設計最終可以被組裝,單一職責原則(獨立原則)是必須的。

可以看到,整個設計理論是必須基於對需求的認知不完整,且需要低成本(首要的是時間成本)地完成設計這一條件。對於設計週期比較長,認知較為充分的領域,設計理論並不適用。完全只用設計模式來衡量設計的好壞,也是不可取的。這方面的反例有很多,LeetCode上面的題目,恐怕沒有哪一個符合了設計模式,比如說找連結串列倒數第k個節點中的雙指標就是一個典型。對於人體而言,也並不遵循什麼單一職責原則,甚至可以說耦合地不像,人在飢餓的時候,可以分解蛋白質來供能;我們在飛機設計過程中,有考慮過在液壓油洩露時,拿燃油來充當液壓油麼?一些經典設計也並不遵循設計理論與原則,例如活塞環既能夠防止漏氣,又能夠降低摩擦磨損,這顯然也不是符合獨立公理的。

只有對設計科學的底層邏輯有著深入的研究,才能使得這門科學發揮其真正的作用。雖然本文儘可能地對這個領域進行了一些減法地操作,略去了一些不核心的要素,但是無論在理論上,還是例子上,都沒有能夠提供一個真正能夠被驗證成為正確或是錯誤的想法或是命題。本文甚至連錯誤都算不上,這無論如何都是讓人不滿意的。

六 附——利用分拆來設計系統的一個例子

很多設計領域的文章提出的例子,都是一些已有的設計;或是拿著根本沒有市場的需求來設計一款產品。這種先射箭後畫靶的行為並不能促進科學的發展。因此找一個大家都熟知的領域,提出解決起來較為有難度,但是需求明確的問題來作為探討的例子。很幸運的是,我的確解決了我自己提出的問題。

在機械領域,平面杆件機構的設計是最基本的問題。例如對下圖中這種四杆機構,我們經常會進行擺角的設計等工作。那麼,我們能不用勻速的電機和平面杆件,使得平面杆件上的某一點有著指定的軌跡?例如用平面杆件畫一隻兔子?

工程設計論——如何寫好工程程式碼

工程設計論——如何寫好工程程式碼

對於這個問題,我們梳理我們已經知道的知識,來給出一些弱約束:

一個確定的平面杆組機構,其上任意一點的位置都是一個隨時間變化的週期函式。我們可以用複數域上的函式來進行表示,即:

工程設計論——如何寫好工程程式碼

由勻速電機帶動的杆件(主動件),其終點的軌跡是一個圓,且這個圓的運動規律與其他杆件無關。

不由勻速電機帶動的杆件(從動件)的軌跡,由主動件的運動軌跡和其與主動件的連結所決定。

那麼,我們再由拆分給出另外的弱約束,以解決這一問題:

最終設計的平面杆組,由主動件和一些連線組構成。這些連線組應當具備兩個自由端點,且連線組上一點在運動中始終是這兩個自由端點的中點,即

工程設計論——如何寫好工程程式碼

在這樣一個弱約束下,我們的問題就變為了:

如何透過一些圓周運動,及建立在其上的加法體系,擬合任意一個週期運動。

如何找到一個連線組,使得其具備上述條件。

問題一的答案由傅立葉變換給出:

工程設計論——如何寫好工程程式碼

問題二可以由如下杆組完成,轉動副2始終為轉動副1,3的中點:

工程設計論——如何寫好工程程式碼

最終的設計,我用了16個主動件,及16個連線組,共計80個杆件,得到的結果已經在上圖中展示了。

誠實而言,我認為這個例子在說明弱約束和強約束,以及拆分對於工程設計的必要性方面,仍舊難以擺脫先射箭後畫靶的嫌疑。但是至少,我不認為我設計的機構,就是本問題的最優解;我想本問題用以說明工程設計並不能得到最好的設計這一點,還是足夠的。

搜尋與推薦技術實戰訓練營

點選閱讀原文檢視詳情