設(shè)計模式和函數(shù)式編程——從策略模式開始

半年沒有學(xué)習(xí)設(shè)計模式了,這半年以來主要做的框架開發(fā)工作,也算是有一些實踐經(jīng)驗(雖然遠(yuǎn)遠(yuǎn)不夠),同時也是了解了很多函數(shù)式編程的概念,寫的代碼里狀態(tài)越來越少,代碼風(fēng)格越來越聲明式(好久沒寫過原生的for和while了hhh),也開始覺得一些設(shè)計模式變得臃腫起來了?,F(xiàn)在繼續(xù)回來學(xué)習(xí)設(shè)計模式,順便看看它們結(jié)合函數(shù)式編程中的概念會對樣板代碼有如何的簡化。

這里我選擇了策略模式進(jìn)行學(xué)習(xí),之后是命令模式和狀態(tài)模式,選擇其的原因是因為這三個模式經(jīng)由借助Java8所提供的函數(shù)式的工具——函數(shù)式接口和閉包——能很大程度地簡化。
函數(shù)式接口和閉包
函數(shù)式接口結(jié)合Lambda表達(dá)式,使我們能夠書寫作為值/字面量的函數(shù)。這樣,將函數(shù)作為值來看待,作為入?yún)魅?,作為返回值等使用方法都變得明顯和符合直覺了。曾經(jīng)我們只能夠傳遞“名詞”給函數(shù),而現(xiàn)在我們能傳遞“動詞”了。
而閉包使我們能夠捕獲外部作用域的變量,從而構(gòu)造一個“窮人的類”——就如同類是數(shù)據(jù)和行為的一個聚合一樣,閉包函數(shù)也是這樣一個聚合,只不過行為只有一種罷了,比如說下面兩個Counter完全可以認(rèn)為是等價的——
再考慮另一個情形——我們試圖用多線程執(zhí)行某個task,而這個task有一個依賴的對象。如果在上古時代的Java,我們必須創(chuàng)建一個實現(xiàn)Runnable的類來包含相應(yīng)依賴,比如需要這么寫——
這里故意沒有使用匿名實現(xiàn)類,但其實使用匿名實現(xiàn)類的話也需要用到閉包。
需要多加一個類!如果這個類只用一次的話,那也太麻煩了!而在Java 8里,我們可以這么寫——
如果我們有多個task都會利用這個依賴呢?舊時代的Java就只能對每個task都創(chuàng)建一個類了。我們可以用方法引用(本質(zhì)上也是匿名函數(shù))來解決這個問題——建立一個類來包含依賴的對象,把每個task作為一個方法并在方法中使用該依賴,如taskA,taskB。
或者也可以通過函數(shù)參數(shù)來注入依賴,如taskC,使用時使用匿名函數(shù)對該task進(jìn)行調(diào)用,注入依賴。
容易發(fā)現(xiàn),這樣的代碼雖然更加簡短,但是卻是破壞了開閉原則的——各種Task都定義在這個類里,如果要增加新的Task,則必須要修改源代碼。但這在實踐上真的會造成影響嗎?應(yīng)當(dāng)具體問題具體分析。
也需要知道的是,雖然Java 8為函數(shù)式編程做了一些努力,但它的完善程度仍舊是遠(yuǎn)遠(yuǎn)不夠的,它仍舊試圖把函數(shù)都當(dāng)作特定類型的對象來對待,這樣即使兩個函數(shù)的函數(shù)簽名相同,它們兩個也未必是能夠互換使用的。這在異常處理,函數(shù)組合等地方都帶來了很大的麻煩,而Scala或Kotlin在這方面做的很好——首先函數(shù)的簽名就是函數(shù)的類型,這在很多時候甚至比被迫的命名還要清晰——Int -> Int
可比IntUnaryOperator
要好理解的多了;而且函數(shù)仍被當(dāng)作類來看待,因此也具有自己的方法——和其他的函數(shù)進(jìn)行組合等。這是Java做不到的(Java提供的函數(shù)式接口似乎普遍包含了一個組合方法andThen
,Function
接口包含了compose
,但是并不統(tǒng)一),即使能做到也缺乏泛用性。而試圖進(jìn)行柯里化等操作時,得到的函數(shù)簽名更是不忍直視,比如Int -> Int -> Int -> Int
會得到Function<Integer, Function<Integer, Function<Integer, Integer>>>
,并且調(diào)用時也得fn.apply(1).apply(2).apply(3),
策略模式就本質(zhì)上說來,就是將為特定目的/接口的算法封裝在同樣接口的類中,使我們能夠方便添加新的算法,以及更換/切換使用的算法。策略模式無論是從面向?qū)ο蟮慕嵌冗€是從函數(shù)式的角度都是非常容易理解的。一個簡單的例子就是,JDBC如何適配多個數(shù)據(jù)庫?答案是提供同樣的接口,為每個數(shù)據(jù)庫都對該接口進(jìn)行實現(xiàn),在運行時選擇使用的數(shù)據(jù)庫的對應(yīng)的實現(xiàn)類。
We did it!代碼很簡單,可是它真能“不忘初心”嗎?non non噠喲,基礎(chǔ)價變了怎么辦?成年人的定義變了怎么辦?遇上節(jié)日了不打折嗎?會員的折扣變了怎么辦?顯然,這代碼可抵抗不了業(yè)務(wù)的變化,它必須得動。
介紹和示例
能夠發(fā)現(xiàn),當(dāng)我們進(jìn)行對計算規(guī)則進(jìn)行改變的時候,我們實際上只需要改變getPrice方法中的內(nèi)容,而buyTicket方法,以及getPrice的簽名(接口)都是不變的。答案很顯然了,我們把getPrice抽象成接口,而讓具體的算法去實現(xiàn)這樣的接口就行了,而buyTicket則利用該接口對具體算法進(jìn)行使用。這樣的接口就稱為策略(Strategy)。
這樣,當(dāng)業(yè)務(wù)有調(diào)整的時候,只需要編寫新的策略,并通過配置文件等形式修改注入的具體策略即可,甚至能夠在運行時對使用的策略進(jìn)行修改。我們還可以讓策略之間互相依賴,比如對另外一個策略的票價再打個折之類的。
總而言之(真快?。。呗阅J街谐霈F(xiàn)三種角色——上下文(context),使用策略的地方,即客戶端;抽象策略,或者說策略的接口,其將被上下文所使用;具體策略,顧名思義。
FP的角度
從函數(shù)式編程的角度來看,這一個個的具體的策略類實際上是一個個具有同樣的接口/簽名的函數(shù),它們被命名,被保存了。于是,我們可以通過函數(shù)來表示具體策略,讓函數(shù)的簽名來代替抽象策略,從而消滅抽象策略這一角色,在上下文中直接使用符合該函數(shù)簽名的函數(shù)作為具體策略。比如,這里的getPrice函數(shù),它的簽名為(int, int) -> int
,或者從Java的話說,BiFunction<Integer, Integer, Integer>
或Function<Tuple2<Integer, Integer>, Integer>
,抽象策略的類就可直接使用該函數(shù)類來代替,而具體策略只需要實現(xiàn)該接口的示例。比如我們可以這么寫——
適用場景
對同一個接口,需要能夠切換多個實現(xiàn)的情況下使用策略模式非常適合,比如前面說到的票價計算場景,以及選擇使用介質(zhì)的緩存(本地,內(nèi)存,磁盤,網(wǎng)絡(luò)),統(tǒng)一不同的文件系統(tǒng)等。需注意的是如果策略包含多個方法,則還是使用面向?qū)ο蟮氖侄胃奖阈?/p>
go further
我們可以通過函數(shù)組合的方法對不同策略進(jìn)行組合,達(dá)到復(fù)用代碼,或者對結(jié)果進(jìn)行“代理”的目的。比如我們可以讓策略的返回值來乘以一個數(shù)來模擬打折情況——
我們也可以通過流式接口等形式來創(chuàng)建策略的工廠類,通過DSL來描述業(yè)務(wù),最終創(chuàng)建出最后的服務(wù),比如可能可以這樣——
甚至利用動態(tài)語言等的特性,嵌入個lua虛擬機整整活?這里還會有無數(shù)的騷操作,但是我想不出來了,告辭!
關(guān)于函數(shù)組合
什么是函數(shù)組合?我們知道,數(shù)學(xué)上的函數(shù)是兩個集合之間的映射,如f(x) = x + 1 (x ∈ R)為一個實數(shù)集到實數(shù)集的映射。而類型可以認(rèn)為是一個數(shù)據(jù)的集合,如int型代表……-1,0,1,……的集合,char型代表'a','b',……的集合。計算機中的函數(shù)因此也可表示從集合到集合的映射,如上面的getPrice函數(shù),其可以表達(dá)為(int, int) -> int
,即一個(int, int)
——int型和int型的笛卡爾積——的集合到int型的映射。
如果我們有一個函數(shù)f : A -> B
,其中A為輸入值的類型,B為輸出值的類型,又有一個函數(shù)g : B -> C
,這樣我們就可以組合這兩個函數(shù),得到函數(shù)g . f : A -> C
。其中(g . f)(x) = g(f(x))
。
扯這些有啥用呢?我們可以通過函數(shù)簽名來對函數(shù)進(jìn)行組合,從而把簡單的函數(shù)組裝成復(fù)雜的函數(shù),以更聲明式的手段表達(dá)業(yè)務(wù)邏輯。比如在Java中,我們可以寫Stream.of(1, 2, 3).filter(i -> i % 2 == 1).map(i -> i * 3).reduce(0, Integer::sum)
,而在函數(shù)式語言中,我們會寫sum . map (* 3) . filter (\i -> div i 2 == 1)
。一個js的例子如下——
在Java中,我們對數(shù)據(jù)進(jìn)行鏈?zhǔn)降奶幚恚诤瘮?shù)式編程中,我們通過組合小的,易理解、處理、證明的函數(shù)來構(gòu)造最終的復(fù)雜函數(shù)并將其應(yīng)用到數(shù)據(jù)上。顯然后者是形式更加清晰(至少在Haskell里是這樣的……),測試更為容易,更加容易進(jìn)行復(fù)用的。
