函數式編程是最近被熱炒的一個概念。國內外衆多大牛紛紛發表文章,認爲函數編程可能會再度興起。搞得一貫喜歡跟風的小弟我如坐鍼氈。所以,也抽空研究了一下函數式編程這個時髦的概念。
上個世紀,我曾經在圖書館借了一本介紹全部主要計算機語言的書,那本書簡單得介紹過Lisp和其餘語言的語法。其中提到,Lisp是一門函數語言。固然,那時對這句話沒什麼概念。
命令式編程是一種用程序狀態描述計算的方法。使用這種範型的編程人員用語句改變程序狀態。這就是爲何,像 Java 這樣的程序是由一系列讓計算機執行的命令 (或者語句) 所組成的。
另外一方面,函數式編程是一種強調錶達式的計算而非命令的執行的一種編程風格。表達式是用函數結合基本值構成的,它相似於用參數調用函數。
也就是說,函數式編程主要是函數調用,而不是其它的程序語句。
而命令式編程,是經過程序語句的執行運行的。程序語句的執行,會改變程序中保存的狀態。
實際上,咱們通常使用的命令式語言,如C++,Java,C#等的代碼中,也能夠看到大量的函數調用。
一個優秀的軟件工程師使用面向對象編程語言編寫出來的代碼,除了少數的建立對象實例的代碼外,大量的代碼都是函數調用。
所以,儘管傳統上認爲C++,Java,C#等面向對象編程語言是命令式編程語言。但咱們同樣能夠在面向對象編程語言中實現函數式編程風格!
實際上,可能你寫的很多代碼也是採用了函數式編程風格,只是你不知道罷了!
什麼是函數編程?
在常常被引用的論文 「Why Functional Programming Matters」(請參閱 參考資料) 中,做者 John Hughes 說明了模塊化是成功編程的關鍵,而函數編程能夠極大地改進模塊化。在函數編程中,編程人員有一個自然框架用來開發更小的、更簡單的和更通常化的模塊, 而後將它們組合在一塊兒。函數編程的一些基本特色包括:
支持閉包和高階函數。
支持懶惰計算(lazy evaluation)。
使用遞歸做爲控制流程的機制。
增強了引用透明性。
沒有反作用。
閉包和高階函數和命令模式
閉包和高階函數
函數編程支持函數做爲第一類對象,有時稱爲 閉包或者 仿函數(functor)對象。實質上,閉包是起函數的做用並能夠像對象同樣操做的對象。與此相似,FP 語言支持 高階函數。高階函數能夠用另外一個函數(間接地,用一個表達式) 做爲其輸入參數,在某些狀況下,它甚至返回一個函數做爲其輸出參數。這兩種結構結合在一塊兒使得能夠用優雅的方式進行模塊化編程,這是使用 FP 的最大好處。
看到這裏,我想有設計模式經驗的朋友必定會聯想到「命令模式」。
是的!面向對象編程中的命令模式,不就是函數式編程中的閉包和告誡函數嗎?!
命令模式
別名:Action動做模式,Transaction事務模式。我也叫它「參數回調模式」,由於本質上,命令模式和C的參數回調是同樣的。
意圖
|
將一個請求封裝爲一個對象,從而使你可用不一樣的請求對客戶進行參數化;對請求排隊或記錄請求日誌,以及支持可撤消的操做。
|
適用性
|
- 抽象出待執行的動做以參數化某對象,你可用過程語言中的回調(c a l l b a c k )函數表達這種參數化機制。所謂回調函數是指函數先在某處註冊,而它將在稍後某個須要的時候被調用。C o m m a n d 模式是回調機制的一個面向對象的替代品。
- 在不一樣的時刻指定、排列和執行請求。一個C o m m a n d 對象能夠有一個與初始請求無關的生存期。若是一個請求的接收者可用一種與地址空間無關的方式表達,那麼就可將負責該請求的命令對象傳送給另外一個不一樣的進程並在那兒實現該請求。
- 支持取消操做。C o m m a n d 的E x c u t e 操做可在實施操做前將狀態存儲起來,在取消操做時這個狀態用來消除該操做的影響。C o m m a n d 接口必須添加一個U n e x e c u t e 操做,該操做取消上一次E x e c u t e 調用的效果。執行的命令被存儲在一個歷史列表中。可經過向後和向前遍歷這一列表並分別調用U n e x e c u t e 和E x e c u t e 來實現重數不限的「取消」和「重作」。
(這是 指,備忘錄模式中,撤銷管理器經過方法的參數,把狀態加到管理器中呢?!同時,容器中的全部狀態都是一個接口的實現類,而後,均可以執行相同的方法:
undo/redo。
)
- 支持修改日誌,這樣當系統崩潰時,這些修改能夠被重作一遍。在C o m m a n d 接口中添加裝載操做和存儲操做,能夠用來保持變更的一個一致的修改日誌。從崩潰中恢復的過程包括從磁盤中從新讀入記錄下來的命令並用E x e c u t e 操做從新執行它們。
- 用構建在原語操做上的高層操做構造一個系統。這樣一種結構在支持事務( t r a n s a c t i o n )的信息系統中很常見。一個事務封裝了對數據的一組變更。C o m m a n d 模式提供了對事務進行建模的方法。C o m m a n d 有一個公共的接口,使得你能夠用同一種方式調用全部的事務。同時使用該模式也易於添加新事務以擴展系統。
|
C++中STL和Boost等類庫中普遍使用的仿函數類,也是命令模式的一種實現。
在面向過程編程語言,或者函數編程語言中,經過把函數指針做爲函數的參數,能夠實現參數回調。
在面向對象編程語言中,經過把某個仿函數接口的指針做爲函數的參數,也能夠實現相似於面向過程語言的函數參數的回調。
面向對象編程語言實現命令模式有幾種變體。
如,能夠把某個仿函數接口的指針做爲類的一個實例變量保存在類中。
閉包和高階函數和命令模式
函數式編程中的閉包,對應於命令模式中,用於回調的接口。這個接口封裝了一個或者多個函數。
如:public interface Comparable<T>
有一個方法
Comparable接口就是一個閉包。它的具體實現類就是閉包的具體實現。
Comparable接口僅僅封裝了一個方法。它經常用做方法的參數,方法體內進行調用參數的
compareTo
方法。
使用了回調參數的那個函數,就是函數式編程中的「高階函數」。
爲命令模式正名
一直以來,在面向對象編程語言的世界中,對GOF提出的命令模式的非議一直不斷。那些純粹的面向對象編程專家看到命令模式中那些個只是封裝了一個函數的接口感到恐懼。
那是函數,仍是類?這仍是OOP嗎?
面向對象編程,使用接口描述世界。純粹的OOP語言,如Java,Ruby和C#中,只有類是第一類的語言元素。函數都是封裝在一個個類中的。
可是,類只是咱們對於世界的一種描述,一種觀點。對於世界中那些純粹的功能,怎麼辦?難道非要給它們加上它們並不須要的數據嗎?
仍是還函數以原本面目吧!使用命令模式,用一個接口把函數封裝起來!實話實說:咱們就是須要一個函數!怎麼樣?不行嗎!
據支持函數式編程的大牛們說,函數式編程比命令式編程更增強大。並且這是有數學依據的。究竟是不是真的,我不知道。我對理論沒什麼興趣。
至少,一向支持命令模式的我,爲命令模式找到了強援。若是面向對象社區不要命令模式,那麼咱們就索性聲稱命令模式就是函數式編程得了!
命令模式是面向對象編程語言中模仿函數式編程的一種模式!
下面再說說命令模式其餘的實現方式
除了使用一個仿函數接口(或者說函數的包裝接口)外,對於某些語言,命令模式還有其餘的實現方式。
Java和C#都有很強大的動態能力。它們的反射機制能夠動態獲得類、函數、屬性。
在Java中,
Method類就是對全部函數的封裝類。
public final class Method
extends AccessibleObject
implements GenericDeclaration, Member
Method
提供關於類或接口上單獨某個方法(以及如何訪問該方法)的信息。所反映的方法多是類方法或實例方法(包括抽象方法)。
Method
容許在匹配要調用的實參與底層方法的形參時進行擴輾轉換;但若是要進行收縮轉換,則會拋出
IllegalArgumentException
。
能夠經過調用
這個方法,調用所須要的函數。
這樣,在java中,咱們實際上可使用Method類做爲方法的參數,進行回調!
.NET中也有相似的機制。
另外,C#有一個關鍵字delegate,委派,這實際上也就是方法的面向對象的對等物。委派的聲明,實際上就是方法的聲明。是
C中函數指針/參數回調機制的直接對應物。
Delegate和Java的Method其實是同一個東西。咱們能夠用Method來模擬C#的委派。
爲訪問者模式正名
意圖
|
表示一個做用於某對象結構中的各元素的操做。它使你能夠在不改變各元素的類的前提下定義做用於這些元素的新操做。
|
適用性
|
- 一個對象結構包含不少類對象,它們有不一樣的接口,而你想對這些對象實施一些依賴於其具體類的操做。
- 須要對一個對象結構中的對象進行不少不一樣的而且不相關的操做,而你想避免讓這些操做「污染」這些對象的類。Vi s i t o r 使得你能夠將相關的操做集中起來定義在一個類中。當該對象結構被不少應用共享時,用Vi s i t o r 模式讓每一個應用僅包含須要用到的操做。
- 定義對象結構的類不多改變,但常常須要在此結構上定義新的操做。改變對象結構類須要重定義對全部訪問者的接口,這可能須要很大的代價。若是對象結構類常常改變,那麼可能仍是在這些類中定義這些操做較好。
|
訪問者模式,就是把一個類分紅兩個部分。一個部分是數據。把它們封裝到一個類中。這種類常被叫作「數據容器類」。
另外一部分是對數據的操做。根據不一樣的關注點,把函數分紅一個或者幾個接口。
在使用時,把接口和數據容器類組合起來使用。
最典型的訪問者模式,就是著名的DAO數據訪問對象模式。
把數據庫表的字段用一個數據容器類封裝起來。對類對象的操做,用一個DAO接口封裝起來。
如對用戶表數據的操做。建立2個類:
User類:
Integer id
String name
IUserDao接口
List<User> queryAll();
User query(Integer id);
Void delete(Integer id);
Void delete(User user);
Void reLoad(User user);
調用者:
UserService類:
IUserDao dao;
List<User> query (條件){
dao的各個函數調用。
}
這些DAO類,封裝了一些函數,自己不保存數據。操做時全部的數據都在各個函數中經過參數傳入。
訪問者模式中的訪問者接口,僅僅封裝了多個函數,而沒有數據,也能夠看做是函數式編程的閉包。
調用訪問者的函數的函數,就是函數式編程的高階函數。
訪問者模式,和命令模式同樣,在OOP社區也飽受爭議。一些程序員認爲,訪問者模式是罪大惡極,惡貫滿盈!
原本一個好好的既包含數據,又包含操做數據的函數的類,被該死的訪問者模式硬生生掰成2個甚至多個類。
數據容器類,只有數據,沒有操做,那還能算是真正的類嗎?!
訪問者類,只有函數,沒有數據,和命令模式一個樣,能算是真正的類嗎?!
實際上,在程序中,每每數據的結構是最穩定的。而操做數據的函數,因爲業務上的緣由,是很是不穩定的。所以,訪問者模式把數據和操做數據的函數分開。而且讓訪問者來訪問數據。而數據並不知道訪問者對象的存在和它們有哪些函數。(GOF提出的訪問者模式中,訪問者和被訪問者<數據容器類>是互相關聯的關係,我認爲這樣作很差。應該是訪問者知道被訪問者這樣的單向關係。數據沒有必要知道本身會被怎樣操做。它只管保存數據和公開數據便可!)
並且,類不一樣的用戶,須要對數據進行的操做是不同的。不一樣的用戶,須要不一樣的訪問者函數。若是不使用訪問者模式,那麼全部用戶將不得不獲得做用於數據上的全部方法。而其中絕大部分是該用戶並不須要的。這顯然是浪費,邏輯上也說不過去。
如今,訪問者模式和函數式編程也攀上了關係。若是OOP社羣不要訪問者模式,那麼,咱們能夠很差意思的說:其實…我用的是函數式編程:)
結論
兄弟,你用過命令模式,訪問者模式,DAO模式嗎?那麼你已經在OOP語言中使用了函數式編程。
你寫過只有函數,沒有數據的類嗎?那麼你已經寫過函數式編程了。
你用過Method類的invoke方法嗎?當時你在用函數式編程。
你用過delegate嗎?當時你也在用函數式編程。
你用過函數指針嗎?當時你在用函數式編程。
類,必需要有數據嗎?沒必要!
類,必需要有函數嗎?也沒必要!