天天看點

程式設計範式:程式設計語言的核心思想,代碼組織方式和規則限制

作者:小智雅彙

程式設計範式一詞最早來自 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-

繼續閱讀