利用函式組合提升程式碼可維護性
前言
函式組合,在函數語言程式設計裡面也是挺重要的概念,能夠將函式進行操作合併等,在有些場景下可以大幅度提升程式碼的可讀及可維護性。
下面就演示一些利用函式組合重構程式碼以達到更好可維護性的例子
簡單場景
假設有如下程式碼:
process1(_ param: String) -> String process2(_ param: String) -> String process3(_ param: String) -> String process4(_ param: String) -> String
這些函式來處理字串,如果要組合呼叫的話,可能會寫出來如下的程式碼:
var str = ... str = process1(str) str = process2(str) str = process3(str) str = process4(str) // use str
或者更灑脫一些,寫出如下的程式碼:
let ret = process4(process3(process2(process1(str))))
第二種方式可讀性不算太好,第一種方式程式碼寫起來又會非常繁瑣。那應該如何來優化呢?
優化
Swift中是支援自定義運算子 的,而且swift中函式是一等公民 。這兩個特性是很強大的,利用他們,可以更好的實現函式的組合,可以讓程式碼看起來更簡潔、更易讀。
大概的思路是把process1、process2等進行組合,組合成一個新的函式,呼叫這個新函式的效果,跟分開挨個呼叫是一樣的。
優化後的程式碼如下:
infix operator ++ : AdditionPrecedence func ++ (lhs: @escaping (String) -> String, rhs: @escaping (String) -> String) -> (String) -> String { return { rhs(lhs($0)) } } let ret = (process1 ++ process2 ++ process3 ++ process4)(str)
這樣寫出來的程式碼,易讀且易維護,要增刪操作、調整呼叫順序等都是很容易的。
更多場景
上面這種場景,是比較特殊的場景,函式簽名一致並且是同步函式。在真正工作中更普遍的場景是:
-
函式簽名不一致,如process1(String),process2(Int, String)
-
函式是非同步操作,而且回撥的閉包型別也不一樣等。
函式簽名不一致
要能組合函式型別不一致的問題,可以參考:利用柯里化去除重複程式碼,利用柯里化 (嚴格來說叫partial function application) 可以很容易解決。
程式碼示例如下:
process1(_ param: String) -> String process2(_ i: Int, _ param: String) -> String process3(_ i: Int, _ param: String) -> String process4(_ i: Int, _ param: String) -> String process1 ++ curry(process2) ++ curry(process3) ++ curry(process4)
不過這兒補充下,有柯里化,就有反柯里化 。反柯里化就是給函式增加引數,讓該函式跟其它函式型別對齊。
反柯里化的一種簡單實現如下:
func uncurry(function: @escaping (String) -> String) -> (String, Int) -> String { return { s, _ in function(s) } }
利用該反柯里化方式,新的組合程式碼可以適度簡化為這樣:
uncuryy(process1) ++ process2 ++ process3 ++ process4
uncurry完善的實現,可以參考Github上的一些實現,如 swift-overture
非同步操作
再來看非同步操作的問題。
說到非同步處理,如果熟悉一些非同步處理框架,如PromiseKit或RxSwift,那麼可能知道PromiseKit裡的Promise或RxSwift裡的Observable這兩個物件。
仔細想想,Promise和Observable本身就是很有意思的物件,這些物件可以封裝非同步操作,當然,也可以表示同步操作,表示純資料等。這些物件本身也提供了很多操作,操作之後,返回的結果仍然是該物件型別。(在函數語言程式設計裡面,這兩個物件都可以理解為Monad物件)
理解上面這一點是關鍵,如果Observable本身可以封裝非同步操作,那麼,一個非同步操作就可以表達為一個同步函式,只是返回物件是一個代表同步或非同步的物件。這樣非同步的問題就轉變為同步處理的問題了。
下面繼續舉個簡單的例子
假設有如下4個非同步操作:
asyncProcess1 asyncProcess2_1 asyncProcess2_2 asyncProcess3
1、2、3這幾個是併發,2_1和2_2是序列
用RxSwift寫的傳統程式碼大概如下:
asyncProcess1(param: [String]) -> Observable{}
asyncProcess2_1(param1: Int, param2: [ String]) -> Observable{}
asyncProcess2_2(param: [ String]) -> Observable{}
asyncProcess3(param: [ String]) -> Observable{}
let process2 = Observable.concat(asyncProcess2_1(value, strs), asyncProcess2_2(strs))
let process = Observable.merge([asyncProcess1(strs), process2, asyncProcess3(strs)])
// some code
下面我們就嘗試重構下該程式碼。
先定義下通用的concat和merge的操作符:
// 併發兩個函式,合併成一個函式
infix operator ||| : RxPrecedence
// 序列兩個函式,合併成一個函式
infix operator >>> : RxPrecedence
typealias RxOper= (T) -> Observable
func |||(lfun: @escaping RxOper , rfun: @escaping RxOper ) -> RxOper {
return { value in
Observable.merge([lfun(value), rfun(value)])
}
}
func >>>(lfun: @escaping RxOper , rfun: @escaping RxOper ) -> RxOper {
return { value in
lfun(value).concat(rfun(value))
}
}
然後寫相應的業務程式碼:
// 新的處理程式碼 let process = asyncProcess1 ||| (curry(asyncProcess2_1)(value) >>> asyncProcess2_2) ||| asyncProcess3 // process(strs)...
新程式碼的優勢一目瞭然。並且這些例子都是拿的非常簡單的示例來講解的,真正的使用場景上,當運算元量逐漸增加,操作邏輯逐漸複雜時,傳統的程式碼寫法的冗餘就越能顯現。
參考資料
-
Function Composition in Swift
-
RxSwift中文文件
作者:微雲iOS團隊
連結:https://iweiyun.github.io/2018/10/03/func-compose/