首頁/ 汽車/ 正文

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

程式設計正規化一詞最早來自 Robert Floyd 在 1979 年圖靈獎的頒獎演說,是程式設計師看待程式應該具有的觀點,代表了程式設計者認為程式應該如何被構建和執行的看法,與軟體建模方式和架構風格有緊密關係。

現在主流的程式設計正規化有三種:

sn

程式設計正規化

程式設計正規化(英文)

基本元件

特徵

結構化程式設計

structured programming

程式 = 資料結構 + 演算法

約束指令方向

面向物件程式設計

object-oriented programming

程式 = 實體 + 關係

約束資料作用域

函數語言程式設計

functional programming

程式 = 資料 + 函式

約束資料可變性

這幾種程式設計正規化之間的關係如下:

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

眾所周知,計算機執行在圖靈機模型之上。最初,程式設計師透過紙帶將指令和資料輸入到計算機,計算機執行指令,完成計算。後來,程式設計師編寫程式(包括指令和資料),將程式載入到計算機記憶體,計算機執行指令,完成計算。時至今日,軟體已經非常複雜,規模也很大,人們透過軟體來解決各個領域(Domain)的問題,比如通訊,嵌入式,銀行,保險,交通,社交,購物等。

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

人們把一個個具體的領域問題跑在圖靈機模型上,然後做計算,而領域問題和圖靈機模型之間有一個很大的 gap(What,How,Why),這是程式設計師主要發揮的場所。程式設計正規化是程式設計師的思維底座,決定了設計元素和程式碼結構。程式設計師把領域問題對映到某個程式設計正規化之上,然後透過程式語言來實現。顯然,程式設計正規化到圖靈機模型的轉化都由編譯器來完成,同時這個思維底座越高,程式設計師做的就會越少。

你可能會有一個疑問:為什麼會有多個程式設計正規化?換句話說,就是程式設計師為什麼需要多個思維底座,而不是一個?

思維底座取決於程式設計師看待世界的方式,和哲學、心理學都有關。程式設計師開發軟體是把現實中的世界模擬到計算機中來執行,每個程式設計師在這個時候都相當於一個造物主,在計算機重新創造一個特定領域的世界,那麼如何看待這個世界就有些哲學觀的味道在裡面。這個虛擬世界的最小構築物是什麼?每個構築物之間的關係是什麼?用什麼方式把這個虛擬世界層累起來。隨著科學技術的演進,人們看待世界的方式會發生變化,比如生物學已經演進到細胞,自然科學已經演進到原子,於是程式設計師模擬世界的思維底座也會發生變化。

程式設計師模擬的世界最終要跑在圖靈機模型上,這就有經濟學的要求,成本越小越好。資源在任何時候都是有限的,效能是有約束的,不同的程式設計正規化有不同的優缺點,程式設計師在解決領域問題時需要有多個思維底座來進行權衡取捨,甚至融合。

為了能更深刻的理解程式設計正規化,我們接下來一起回顧一下程式設計正規化的簡史。

1 程式設計正規化簡史

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

機器語言使用 0 和 1 組成的二進位制序列來表達指令,非常晦澀難懂。組合語言使用助記符來表達指令,雖然比機器語言進步了一些,但編寫程式仍然是一件非常痛苦的事情。組合語言可以透過彙編(編譯)得到機器語言,機器語言可以透過反彙編得到組合語言。組合語言和機器語言一一對應,都是直接面向機器的低階語言,最貼近圖靈機模型。

站在結構化程式設計的視角,機器語言和組合語言也是有程式設計正規化的,它們的程式設計正規化就是非結構化程式設計。當時 goto 語句滿天飛,程式及其難以維護。後來,大家對於 goto 語句是有害的達成了共識,程式語言設計上透過三種單入口、單出口的

控制結構

(順序、選擇、迴圈結構)把 goto 語句儘量迴避掉。

隨著計算機技術的不斷髮展,人們開始尋求與機器無關且面向使用者的高階語言。無論何種機型的計算機, 只要配備上相應高階語言的編譯器,則用該高階語言編寫的程式就可以執行。首先被廣泛使用的高階語言是 Fortran,有效的降低了程式設計門檻,極大的提升了程式設計效率。後來 C 語言橫空出世,它提供了對於計算機而言較為恰當的抽象,遮蔽了計算機硬體的諸多細節,是結構化程式語言典型代表。時至今日,C 語言依然被廣泛使用。

當高階語言大行其道以後,人們開發的程式規模逐漸膨脹,這時

如何組織程式

變成了新的挑戰。有一種語言搭著 C 語言的便車將面向物件的設計風格帶入主流視野,這就是 C++,它完全相容 C 語言。在很長一段時間內,C++ 風頭十足,成為行業中最主流的程式語言。後來,計算機硬體的能力得到了大幅提升,Java 語言脫穎而出。Java 語言假設程式的程式碼空間是開放的,在 JVM 虛擬機器上執行,一方面支援面向物件,另一方面支援 GC 功能。

不難看出,程式語言的發展就是一個逐步遠離計算機硬體,向著待解決的領域問題靠近的過程,也是一個

抽象層次

逐步提升的過程。所以,程式語言後續的發展方向就是探索怎麼更好的解決領域問題。

前面說的這些程式語言只是程式語言發展的主流路徑,其實還有一條不那麼主流的路徑也一直在發展,那就是函數語言程式設計語言,這方面的代表是 Lisp。

首先,函數語言程式設計的主要理論基礎是 Lambda 演算,它是圖靈完備的;

其次,函數語言程式設計是抽象代數思維,更加接近現代自然科學,使用一種形式化的方式來解釋世界,透過公式來推導世界,極度抽象(比如 F=ma)。在這條路上,很多人都是偏學術風格的,他們關注解決方案是否優雅,如何一層層構建抽象。他們也探索更多的可能,垃圾回收機制就是從這裡率先出來的。

但函數語言程式設計離圖靈機模型太遠了,在圖靈機上的執行效能得不到直接的支撐,同時受限於當時硬體的效能,在很長一段時間內,這條路上的探索都只是學術圈玩的小眾遊戲,於是函數語言程式設計在當時被認為是一個在工程上不成熟的程式設計正規化。當硬體的效能不再成為阻礙,如何解決問題開始變得越來越重要時,函數語言程式設計終於和程式語言發展的主流路徑匯合了。促進函數語言程式設計引起廣泛重視還有一些其他因素,比如多核 CPU 和分散式計算。

程式設計正規化是抽象的,程式語言是具體的。

程式設計正規化是程式語言背後的思想

,要透過程式語言來體現。程式設計正規化的世界觀體現在程式語言的核心概念中,程式設計正規化的方法論體現在程式語言的表達機制中,一種程式語言的語法和風格與其所支援的程式設計正規化密切相關。

雖然

程式語言和程式設計正規化是多對多的關係,但每一種程式語言都有自己的主流程式設計正規化

。比如,C 語言的主流程式設計正規化是結構化程式設計,而 Java 語言的主流程式設計正規化是面向物件程式設計。程式設計師可以打破“次元壁”,將不同程式設計正規化中的優秀元素吸納過來,比如在 linux 核心程式碼設計中,就將物件元素吸納了過來。無論在以結構化程式設計為主的語言中引入面向物件程式設計,還是在以面向物件程式設計為主的語言中引入函數語言程式設計,在一個程式中應用

多正規化

已經成為一個越來越明顯的趨勢。不僅僅在設計中,越來越多的程式語言逐步將不同程式設計正規化的內容融合起來。C++ 從 C++ 11 開始支援 Lambda 表示式,Java 從 Java 8 開始支援 Lambda 表示式,同時新誕生的語言一開始就支援多正規化,比如 Scala,Go 和 Rust 等。

從結構化程式設計到面向物件程式設計,再到函數語言程式設計,離圖靈機模型越來越遠,但抽象層次越來越高,與領域問題的距離越來越近。

2 結構化程式設計

結構化程式設計,也稱作程序式程式設計,或面向過程程式設計。

2.1 基本設計

在使用低階語言程式設計的年代,程式設計師站在直接使用指令的角度去思考,習慣按照自己的邏輯去寫,指令之間可能共享資料,這其中最方便的寫法就是需要用到哪塊邏輯就 goto 過去執行一段程式碼,然後再 goto 到另外一個地方。當代碼規模比較大時,就難以

維護和擴充套件

了,這種程式設計方式便是

非結構化程式設計

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

迪克斯特拉(E。W。dijkstra)在 1969 年提出

結構化程式設計

,摒棄了 goto 語句,而以模組化設計為中心,將待開發的軟體系統劃分為若干個相互獨立的模組,這樣使完成每一個模組的工作變得單純而明確,為設計一些較大的軟體打下了良好的基礎。按照結構化程式設計的觀點,任何演算法功能都可以透過

三種基本程式控制結構

(順序、選擇和迴圈)的組合來實現。

結構化程式設計主要表現在以下三個方面:

① 自頂向下,逐步求精(分治分層,自頂向下分解,自底向上實現)。將編寫程式看成是一個逐步演化的過程,將分析問題的過程劃分成若干個層次,每一個新的層次都是上一個層次的細化。

② 模組化。將系統分解成若干個模組(函式),每個模組實現特定的功能,最終的系統由這些模組組裝而成,模組之間透過介面傳遞資訊。

③ 語句結構化。在每個模組中只允許出現順序、選擇和迴圈三種流程結構的語句,避免避免使用goto語句。

結構化程式設計是用計算機的思維方式去處理問題,將資料結構和演算法分離(程式 = 資料結構 + 演算法)。資料結構描述待處理資料的組織形式,而演算法描述具體的操作過程。我們用過程函式把這些演算法一步一步的實現,使用的時候一個一個的依次呼叫就可以了。

在三種主流的程式設計正規化中,結構化程式設計離圖靈機模型最近。人們學習程式設計的時候,大多數都是從結構化程式設計開始。按照結構化程式設計在做設計時,也是按照指令和狀態(資料)兩個維度來考慮。在指令方面,先分解過程(Procedure),然後透過 Procedure 之間的一系列

關係(主要是呼叫關係,包括回撥)

來構建整個計算,對應演算法(用流程圖描述)設計。在狀態方面,將例項資料都以全域性變數的形式放在模組的靜態資料區,對應資料結構設計。

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

2.2 架構風格

結構化程式設計一般偏底層,一般適用於追求確定性和效能的

系統軟體

。這類軟體偏靜態規劃,

需求變化也不頻繁

,適合多人並行協作開發。將軟體先分層和模組,然後再確定模組間的 API,接著各組就可以同時啟動開發。各組進行資料結構設計和演算法流程設計,並在規定的時間內進行整合交付。

分層模組化

架構支撐了軟體的大規模並行開發,且偏靜態規劃式開發交付。層與層之間限定了依賴方向,即層只能向下依賴,但同層內模組之間的依賴卻無法約束,經常會出現模組之間互相依賴的情況,導致可裁剪性和可複用性過粗,響應變化(功能修改和擴充套件)能力較弱。

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

2.3 結構化程式設計的優點

貼近圖靈機模型,可以充分調動硬體,控制性強。從硬體到 OS,都是從圖靈機模型層累上來的。結構化程式設計離硬圖靈機模型比較近,可以充分挖掘底下的能力,儘量變得可控。

流程清晰。從 main 函式看程式碼,可以一路看下去,直到結束。

2.4 結構化程式設計的缺點

資料的全域性訪問性帶來較高的耦合複雜度,區域性可複用性及響應變化能力差,模組可測試性差。想單獨複用一個 Procedure 比較困難,需要將該過程函式相關的全域性資料及與全域性資料相關的其他過程函式(生命週期關聯)及其他資料(指標變數關聯)一起拎出來複用,但這個過程是隱式的,必須追著程式碼一點點看才能做到。同理,想要單獨修改一個 Procedure 也比較困難,經常需要將關聯的所有 Procedure 進行同步修改才能做到,即散彈式修改。還有一點,就是模組之間可能有

資料耦合

,打樁複雜度高,很難單獨測試。

隨著軟體規模的不斷膨脹,結構化程式設計

組織程式的方式

顯得比較僵硬。結構化程式設計貼近圖靈機模型,恰恰說明結構化程式設計抽象能力差,離領域問題的距離比較遠,在程式碼中找不到領域概念的直接對映,難以組織管理大規模軟體。

剛才在優點中提到,結構化程式設計貼近圖靈機模型,可以充分調動硬體,控制性強。為什麼我們需要這個控制性?你可能做過嵌入式系統的效能最佳化,你肯定知道控制性是多麼重要。你可能要最佳化版本的二進位制大小,也可能要最佳化版本的記憶體佔用,還有可能要最佳化版本的執行時效率,這時你如果

站在硬體怎麼執行的最佳狀態來思考最佳化方法

,那麼與圖靈機模型的 gap 就非常小,則很容易找到較好的最佳化方法來實施較強的控制性,

否則中間有很多抽象層,則很難找到較好的最佳化方法。

除了

效能

確定性

對於系統軟體來說也很重要。對於 5G,系統要求端到端時延不超過 1ms,我們不能 80% 的情況其時延是 0。5ms,而 20% 的情況其時延卻是 2ms。賣出一個硬體,給客戶承諾可以支援 2000 使用者,我們不能 80% 的情況可以支援 3000 使用者,而 20% 的情況僅支援 1000 使用者。靜態規劃性在某些系統軟體中是極度追求的,這種確定性需要對底層的圖靈機模型做很好的靜態分解,然後把我們的程式從記憶體到指令和資料一點點對映下去。因為結構化程式設計離圖靈機模型較近,所以對映的 gap 比較小,容易透過靜態規劃達成這種確定性。

3 面向物件程式設計

隨著軟體種類的不斷增多,軟體規模的不斷膨脹,人們希望

可以更小粒度的對軟體進行復用和裁剪

3.1 基本設計

將全域性資料拆開,並將資料與其緊密耦合的方法放在一個邏輯邊界(logical

boundary)內,這個邏輯邊界就是物件。使用者只能訪問物件的 public 方法,而看不到物件內部的資料。

物件將資料和方法天然的封裝在一個邏輯邊界內,可以整體直接複用

而不用做任何裁剪或隱式關聯。

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

人們將領域問題又開始對映成實體及

關係

(程式 = 實體 + 關係),而不再是資料結構和演算法(過程)了,這就是面向物件程式設計,核心特點是封裝、繼承和多型(封裝成實體,並使用和多型關係)。

封裝是面向物件的根基,它將緊密相關的資訊放在一起,形成一個邏輯單元。我們要隱藏資料,基於行為進行封裝,最小化介面,不要暴露實現細節。

繼承分為兩種,即實現繼承和介面繼承。實現繼承是站在子類的視角看問題,而介面繼承是站在父類的視角看問題。很多程式設計師把實現

繼承

當作一種程式碼複用的方式,但這並不是一種好的程式碼複用方式,推薦使用

組合

對於面向物件而言,多型至關重要,介面繼承是常見的一種多型的實現方式。正因為多型的存在,軟體設計才有了更大的彈性(功能修改的封閉性和擴充套件性),能夠更好地適應未來的變化。只使用封裝和繼承的程式設計方式,我們稱之為

基於物件程式設計

,而只有把多型加進來,才能稱之為

面向物件程式設計

。可以這麼說,面向物件設計的核心就是多型的設計。

3.2 面向物件建模

面向物件程式設計誕生後,程式設計師需要從領域問題對映到實體和關係這種模型,後續再對映到圖靈機模型就交給面向物件程式語言的編譯器來完成。於是問題來了,領域千差萬別,如何能將領域問題高效簡潔的對映到實體和關係?這時 UML(Unified Model Language,統一建模語言)應運而生,是由一整套圖表組成的標準化建模語言。可見,面向物件極大的推進了軟體建模的發展。

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

現在有一些新的程式設計師對於 UML 不太熟悉,建議至少要掌握兩個 UML 圖,即類圖和序列圖:

① 類圖是靜態檢視,體現類和結構;

② 序列圖是動態檢視,體現物件和互動;

軟體設計一般從動態圖開始,在動態互動中會把相對比較固定的模式下沉到靜態視圖裡,然後形成類和結構。在看程式碼的時候,透過類和結構就知道一部分物件和互動的資訊了,可以約束及校驗物件和互動的關係。

面向物件建模一般分為四個步驟:

① 需求分析建模

② 面向物件分析(OOA)

③ 面向物件設計(OOD)

④ 面向物件編碼(OOP)

在 OOA 階段,分析師產出分析模型。同理,在 OOD 階段,設計師產出設計模型。

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

分析模型和設計模型

的分離,會導致分析師頭腦中的業務模型和設計師頭腦中的業務模型不一致,通常要對映一下。伴隨著重構和 fix bug 的進行,設計模型不斷演進,和分析模型的差異越來越大。

有些時候,分析師站在分析模型的角度認為某個需求較容易實現,而設計師站在設計模型的角度認為該需求較難實現,那麼雙方都很難理解對方的模型。長此以往,在分析模型和設計模型之間就會存在致命的隔閡,從任何活動中獲得的知識都無法提供給另一方。

Eric Evans 在 2004 年出版了 DDD(領域驅動設計, Domain-Driven Design)的開山之作《領域驅動設計——軟體核心複雜性應對之道》,拋棄將分析模型與設計模型分離的做法,尋找單個模型來滿足兩方面的要求,這就是

領域模型

。許多系統的真正複雜之處不在於技術,而在於領域本身,在於業務使用者及其執行的業務活動。如果在設計時沒有獲得對領域的深刻理解,沒有

將複雜的領域邏輯以模型的形式清晰地表達出來

,那麼無論我們使用多麼先進多麼流行的平臺和基礎設施,都難以保證專案的真正成功。

DDD 是對面向物件建模的演進,核心是建立正確的領域模型:

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

DDD 的精髓是對

邊界的劃分和控制

,共有四重邊界:

第一重邊界,是在問題空間分離子域,包括核心域,支撐域和通用域;

第二重邊界,是在解決方案空間拆分 BC(限界上下文,Bounded Context),BC 之間的協作關係透過 Context Mapping(上下文對映) 來表達;

第三重邊界,是在 BC 內部分離業務複雜度和技術複雜度,形成分層架構,包括使用者介面層,應用層,領域層和基礎設施層;

第四重邊界,是在領域層引入聚合這一最小的設計單元,它從完整性與一致性對領域模型進行了有效的隔離,聚合內部包括實體、值物件、領域服務、工廠和倉儲等設計元素;

3。3 設計原則與模式

設計原則很多,程式設計師最常使用的是 SOLID 原則,它是一套比較成體系的設計原則。它不僅可以指導我們設計模組(類),還可以被當作一把尺子,來衡量我們的設計應對變化(功能修改與擴充套件)的有效性。

SOLID 原則是五個設計原則首字母的縮寫,它們分別是:

① 單一職責原則(Single responsibility principle,SRP):一個類應該有且僅有一個變化的原因;

② 開放封閉原則(Open–closed principle,OCP):軟體實體(類、模組、函式)應該對擴充套件開放,對修改封閉;

③ 里氏替換原則(Liskov substitution principle,LSP):子型別(subtype)必須能夠替換其父型別(base type);

④ 介面隔離原則(Interface segregation principle,ISP):不應強迫使用者依賴於它們不用的方法;

⑤ 依賴倒置原則(Dependency inversion principle,DIP):高層模組不應依賴於低層模組,二者應依賴於抽象;抽象不應依賴於細節,細節應依賴於抽象;

前面我們提到,對於面向物件來說,核心是多型的設計,我們看看 SOLID 原則如何指導多型設計:

① 單一職責原則:透過介面分離變與不變,隔離變化;

② 開放封閉原則:多型的目標是系統對於變化的擴充套件而非修改;

③ 里氏替換原則:介面設計要達到細節隱藏的圓滿效果;

④ 介面隔離原則:面向不同客戶的介面要分離開;

⑤ 依賴倒置原則:介面的設計和規定者應該是介面的使用方;

除過設計原則,我們還要掌握常用的設計模式。設計模式是針對一些普遍存在的問題給出的特定解決方案,使面向物件的設計更加靈活和優雅,從而複用性更好(為應對變化,需要程式碼模組之間關係的高內聚、低耦合,包括is-a, has-a, part-of, member-of,uses-a, depend-on等類模組關係)。學習設計模式不僅僅要學習程式碼怎麼寫,更重要的是要了解模式的應用場景。不論那種設計模式,其背後都隱藏著一些“永恆的真理”,這個真理就是設計原則。的確,還有什麼比原則更重要呢?就像人的世界觀和人生觀一樣,那才是支配你一切行為的根本。可以說,設計原則是設計模式的靈魂。

“守破離”是武術中一種漸進的學習方法:

第一步,守,遵守規則直到充分理解規則並將其視為習慣性的事;

第二步,破,對規則進行反思,尋找規則的例外並“打破”規則;

第三步,離,在精通規則之後就會基本脫離規則,抓住其精髓和深層能量;

設計模式的學習也是一個“守破離”的過程:

第一步,守,在設計和應用中模仿既有設計模式,在模仿中要學會思考;

第二步,破,熟練使用基本設計模式後,創造新的設計模式;

第三步,離,忘記所有設計模式,在設計中潛移默化的使用;

3.4 架構風格

面向物件設計大行其道以後,

元件化或服務化架構風格

開始流行起來。元件化或服務化架構風格參考了物件設計:物件有生命週期,是一個邏輯邊界,對外提供 API;元件或服務也有生命週期,也是一個邏輯邊界,也對外提供 API。在這種架構中,應用依賴導致原則,不論高層還是低層都依賴於抽象,好像整個分層架構被推平了,沒有了上下層的關係。不同的客戶透過“平等”的方式與系統互動,需要新的客戶嗎?不是問題,只需要新增一個新的介面卡將客戶輸入轉化成能被系統 API 所理解的引數就行。同時,對於每種特定的輸出,都有一個新建的介面卡負責完成相應的轉化功能。

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

3.5 面向物件程式設計的優點

物件自封裝資料和行為,利於理解和複用。

物件作為“穩定的設計質料”,適合廣域使用。

多型提高了響應變化的能力,進一步提升了軟體規模。

對設計的理解和演進優先是對模型和結構的理解和調整。不要一上來就看程式碼,面向物件的程式碼看著看著很容易斷,比如遇到虛介面,就跟不下去了。通常是

先掌握模型和結構

,然後在結構中開啟某個點的程式碼進行檢視和修改。請記住,

先模型,再介面,後實現

3.6 面向物件程式設計的缺點

業務邏輯碎片化,散落在離散的物件內

。類的設計遵循單一職責原則,為了完成一個業務流程,需要在多個類中跳來跳去。

行為和資料的不匹配協調,即所謂的貧血模型和充血模型之爭。後來發現可透過 DCI(Data、Context 和 Interactive)架構來解決該問題。

面向物件建模依賴工程經驗,缺乏嚴格的理論支撐。面向物件建模回答了從領域問題如何對映到物件模型,但一般只是講 OOA 和 OOD 的典型案例或最佳實踐,屬於歸納法範疇,並沒有嚴格的數學推導和證明。

4 函數語言程式設計

與結構化程式設計與面向物件程式設計不同,函數語言程式設計對很多人來說要陌生一些。你可能知道,C++ 和 Java 已經引入了 Lambda 表示式,目的就是為了支援函數語言程式設計。函數語言程式設計中的函式不是結構化程式設計中的函式,而是

數學中的函式

,結構化程式設計中的函式是一個過程(Procedure)。

4.1基本設計

函數語言程式設計的起源是數學家 Alonzo Church 發明的 Lambda 演算(Lambda calculus,也寫作 λ-calculus)。所以,Lambda 這個詞在函數語言程式設計中經常出現,你可以把它簡單地理解成匿名函式。

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

4.2 特點

4。2。1 函式是一等公民。

一等公民的含義:

(1)它可以按需建立;

(2)它可以儲存在資料結構中;

(3)它可以當作引數傳給另一個函式;

(4)它可以當作另一個函式的返回值。

4。2。2 純函式。

所謂純函式,是符合下面兩點的函式:

(1)對於相同的輸入,返回相同的輸出;

(2)沒有副作用。

4。2。3 惰性求值。

惰性求值是一種求值策略,它將求值的過程延遲到真正需要這個值的時候。

4。2。4 不可變資料。

函數語言程式設計的不變性

主要體現在值和純函式上

。值類似於 DDD 中的值物件,一旦建立,就不能修改,除非重新建立。

值保證不會顯式修改一個數據,純函式保證不會隱式修改一個數據。

當你深入學習函數語言程式設計時,會遇到無副作用、無狀態和引用透明等說法,其實都是在討論不變性。

4。2。5 遞迴。

函數語言程式設計用遞迴作為流程控制的機制

,一般為尾遞迴。

函數語言程式設計還有兩個重要概念:高階函式和閉包。

4。2。6 高階函式

高階函式是指一種比較特殊的函式,它們可以接收函式作為輸入,或者返回一個函式作為輸出。

4。2。7 閉包

閉包是由函式及其相關的引用環境組合而成的實體,即閉包 = 函式 + 引用環境。

閉包有獨立生命週期,能捕獲上下文(環境)。站在面向物件程式設計的角度,閉包就是隻有一個介面(方法)的物件,即將單一職責原則做到了極致。可見,閉包的設計粒度更小,建立成本更低,很容易做組合式設計。在面向物件程式設計中,設計粒度是一個 Object,它可能還需要拆,但你可能已經沒有意識再去拆,那麼上帝類大物件就會存在了,建立成本高。在函數語言程式設計中,閉包給你一個更精細化設計的能力,一次就可以設計出單一介面的有獨立生命週期的可以捕獲上下文的原子物件,天然就是

易於組合易於重用

的,並且是

易於應對變化

的。

有一句話說得很好:閉包是窮人的物件,物件是窮人的閉包。有的語言沒有閉包,你沒有辦法,只能拿物件去模擬閉包。又有一些語言沒有物件,但單一介面不能完整表達一個業務概念,你沒有辦法,只能將多個閉包組合在一起當作物件用。

對於函數語言程式設計,資料是不可變的,所以一般只能

透過模式匹配和遞迴來完成圖靈計算

。當程式設計師選擇將函數語言程式設計作為思維底座時,就需要解決如何將領域問題對映到資料和函式(程式 = 資料 + 函式)。

函式式設計的思路就是高階函式與組合,背後是抽象代數那一套邏輯。下面這張圖是關於高階函式的,左邊是將函式作為輸入,右邊是將函式作為輸出:

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

對於將函式作為輸入的高階函式,就是面向物件的策略模式。對於將函式作為輸出的高階函式,就是面向物件的工廠模式。

每個高階函式都是職責單一的,所以

函式式設計是以原子的方式透過策略模式和工廠模式來組合類似面向物件的一切

。在這個過程中,到底哪些函式作為入參,哪些函式作為返回值,然後這些返回值函式再傳給哪些函式,接著再返回哪些函式……,你發現你在套公式,

透過公式的層層巢狀完成一個演算法的描述

,所以核心就是設計有哪些高階函式以及它們的組合規則,這是函式式設計中最難的,就是抽象代數的部分。

可見,函式式設計的基本方法為:

藉助閉包的單一介面的標準化和高階函式的可組合性,透過規則串聯設計,完成資料從源到結果的對映描述。

這裡的對映是透過多個高階函式的形式化組合完成,描述就像寫數學公式一樣放在那,等

源資料從一頭傳入,然後經過層層函式公式的處理,最後變成你想要的結果。

資料在形式化轉移的過程中,不僅僅包括資料本身,還包括規則的建立、返回和傳遞。

4.3 架構風格

前面我們講過,函數語言程式設計引起人們重視的因素包括硬體效能提升,多核 CPU 和分散式計算等。函數語言程式設計的一些特點,使得併發程式更容易寫了。一些架構風格,尤其是分散式系統的架構風格,借鑑了函式式的特點,使得系統的擴充套件性和彈性變得更容易。

函數語言程式設計的建模方式是抽象代數,在上面層累出兩類架構風格:

(1)Event Sourcing,Reative Achitecture

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

(2)Lambda Achitecture,FaaS,Serverless

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

借鑑函數語言程式設計的理念,分散式系統的架構風格,在架構層面完成更高抽象力度的表達,在併發層面完成更好的彈性和可靠性。

4.4 函數語言程式設計的優點

高度的抽象,易於擴充套件。函數語言程式設計是資料化表達,非常抽象,在表達範圍內是易於擴充套件的。

宣告式表達,易於理解。

形式化驗證,易於自證。

不可變狀態,易於併發

。資料不可變不是併發的必要條件,不共享資料才是,但不可變使得併發更加容易。

4.5 函數語言程式設計的缺點

對問題域的代數化建模門檻高,適用域受限。現實是複雜的,不是在每個方面都是自洽的,要找到一套完整的規則對映是非常困難的。在一些狹窄的領域,可能找得到,而一旦擴充套件一下,就會破壞該狹窄領域,你發現以前找到的抽象代數建模方式就不再適用了。

在圖靈機上效能較差。

函數語言程式設計增加了很多中間層,它的規則描述和惰性求值等使得最佳化變得困難。

不可變的約束造成了資料泥團耦合。領域物件是有狀態的,這些狀態只能透過函式來傳遞,導致很多函式有相同的入參和返回值。

閉包介面粒度過細,往往需要再組合才能構成業務概念。

5 小結

作為一個程式設計師,我們應該清楚每種程式設計正規化的適用場景,在特定的場景下選擇合適的正規化來恰當的解決問題。

多正規化融合的設計建議:

每種程式設計正規化都有優缺點,不做某單一正規化的擁躉,分場景靈活選擇合適的正規化恰當的解決問題;

從 DDD 的角度,按照模型一致性,將不同正規化的設計劃分到不同的子域、BC 或層內;

最後,我們重新看看開始的那張程式設計正規化之間的關係圖:

程式設計正規化:程式語言的核心思想,程式碼組織方式和規則約束

最早是非結構化程式設計,指令可以隨便跳,資料可以隨便引用。

後來有了結構化程式設計,人們把 goto 語句去掉了,約束了指令的方向性,過程之間是單向的,但資料卻是可以全域性訪問的。

再到面向物件程式設計的時候,人們乾脆將資料與其緊密耦合的方法放在一個邏輯邊界內,約束了資料的作用域,靠關係來查詢。

最後到函數語言程式設計的時候,人們約束了資料的可變性,

透過一系列函式的組合來描述資料從源到目標的對映規則的編排,在中間它是無狀態的

。可見,從左邊到右邊,是一路約束的過程。

越往左邊限制越少,越貼近圖靈機模型,可以充分調動硬體,“直接”帶來了可控性及廣域適用性。對於

可控性

,因為離圖靈機模型很近,可以按自己的想法來“直接”控制。對於

廣域適用性

,因為約束越多,說明門檻越高,一旦右邊搞不定,可以往回退一步,當你找到合理的物件模型或抽象代數模型時,可以再往前走一步。

越往右邊限制越多,

透過約束建立規則

,透過規則描述系統,“抽象”帶來的定域擴充套件性。對於定域,因為這種“抽象”一定是面向某一個狹窄的切面,找到的物件模型或抽象代數模型會有很強的擴充套件性和可理解性,但一旦超過這個範圍,模型可能就無效了,所以 DDD 一直在強調分離子域、劃分 BC 和分層架構。

ref

C++及系統軟體技術大會 2020,《多正規化融合的 Modern C++軟體設計》,王博

極客時間專欄,《軟體設計之美》,鄭曄

https://zhuanlan。zhihu。com/p/354528902

-End-

相關文章

頂部