因為一個BUG, 我在一個搖搖欲墜,幾乎碰一下就會散架的項目中某一個角落中發現下面這樣一段代碼
這段程序與那個BUG有密切的關系。 我來回反復的捉摸這段代碼, 發現這段代碼實現了兩個功能
第一個是在一個從數據庫中讀取的列表數組中找出某個值是最大的一條記錄, 並且把這個最大的值和跟這個值相關的時間給取出來。
第二個比較復雜 ,是將這個列表數組中的值映射到另外一個列表數組中, 可以把這個過程看作是SQL中的JOIN操作, 只是JOIN的條件異常復雜 ,在這里我也不詳述了,閱讀的同學也不必去深入探究。
就這段代碼來說, 很難通過大致觀察就理解代碼的意思 , 代碼之中光循環就套了3層, 而且還有多處復雜的條件判斷,代碼格式混亂,連編碼的底線縮進都沒有滿足。 可悲的是這種類型的代碼廣泛存在於全球范圍內無數Web服務器之上, 每天運行着。
在很久以前, 那會我還很年輕, 看到項目中哪個地方代碼有問題,我就難受, 必須改掉它。 后來我發現, 爛代碼就像地溝油, 在我所生活的城市, 到哪里都能碰的到, 除非不吃飯, 否則就只能睜一只眼閉一只眼,只要不是味道有問題, 吃也就吃了。
然而,這次卻不一樣, 這段代碼運行在某個功能項的關鍵部位, 不透徹的理解清晰這段代碼, 以后出現問題還是會被卡在這里。雖然現在我理解了這段代碼的意思 ,但過些天回過頭來, 我又會忘掉這段代碼所表達的意義。這並不是我的記憶力問題的, 而是因為這段代碼所表達的意途不夠清晰。
於是我把代碼重構成了下面這個樣子, 代碼本身的功能並沒有變化
是不是還是看不明白代碼所表達的意思? 沒關系, 因為這段代碼所表示的功能太過於復雜 ,而且還依賴於代碼所有的整個函數的上下文, 因此無法理解也無可厚非。 但是從代碼結構上來看, 重構后的代碼的卻清晰了不少。
我將原本擁擠在一起的兩個功能進行了拆分, 上面部份是求最大值, 下面部份是對兩個數組進行映射。 這里我用到了兩個PHP中數組的函數 array_map和array_reduce, 這篇文章想表達的主線思路就是利用此類函數來提高PHP代碼的可讀性。 這類函數主要包括以下4個函數
array_filter
array_map
array_walk
array_reduce
這4個函數威力巨大, 在處理列表數組方面可以完全替換掉for、foreach、while這些循環控制語句, 這也是函數式編程方式在PHP的一部份體現。
1.array_filter函數
這段代碼比較好理解,將數組中性別字段為女的數據項提取出來。 整段代碼的邏輯大致如下
1.定義result數組, 用來存放結果
2.循環數組, 對每一個數據項進行條件判斷, 查看其中的性別字段是否為女
3.如符合條件則放入result數組中
這是原汁原味的命令式程序代碼。
如果data變量中的數據並非存放於php數組中, 而是存在於關系數庫的表之中, 那何取得性別為女的數據結果呢? 對於程序員來說這貌似是一個更加簡單的問題,一句SQL語句就搞定了
顯然, 利用SQL查詢數據更加方便,意途也更加清晰,畢間一個SQL表達 式就將所有的程序邏輯都給表達了現來。這句SQL只表達了:“我需要性別為女的數據,至於怎么拿, 我不管 ”, 除了結果 , 其它的它一概不知。
我們不妨把這種思路引入到PHP程序設計之中,不也意味着我們的PHP程序的邏輯表達也更加清晰,代碼的可讀性也更高的。所幸, 這種利用表達式編程的方法在PHP中也完全可以實現。
利用array_filter函數,可以輕松的完成這個任務, 仔細觀察一下, 是不是原來的程序邏輯都不見了,包括定義數組、循環、條件判斷這些都不見了,邏輯方面是只剩下了一個性別比較語句,這對於代碼所實現的功能一目了然。 和上面的SQL比較一下, 這里的性別判斷語句就是SQL中where子句后面的條件判斷, 而array_filter函數其實就是SQL中的where子句。 這就是SQL語句面向結果編程的邏輯原封不變的在PHP中的體現,也就是時下最流行的“聲明性編程”或者也稱為“表達式編程”。
此外, 代碼中性別判斷語句所在的位置稱之為lambda表達式, 更通俗一些的叫法是匿名函數。不難看出, 在SQL的where條件中編寫條件判斷遠不如在匿名函數中寫PHP代碼來的靈活,在where條件中只能執行or和and邏輯,而在php匿名函數中可以隨便怎么寫,只要函數的返回值是個布爾值就可以了,這也是php聲明性編程優於SQL聲明性編程的地方。
2.array_map函數
再來看一個例子
數據中的性別字段是中文的,值也是中文的, 現在想把字段名和字段值都改為英文的, 就可以用上面這段代碼實現, 至於實現的邏輯這里不贅述了。
下面是利用SQL的實現方式
SQL中case when語句好像不太好看, 但是不影響整體邏輯的表達。 將這段SQL轉換成PHP的方式實現
相比之前的PHP實現, 是不是簡潔明了了許多。
在這里使用到了 array_map函數 。 在SQL語句中以select語句最為常用, select的字面意思是“選擇”,而select語句也被稱之為選擇查詢, 事實上從關系數據庫的角度來說,select被稱之為“投影”, 並不是查詢什么的。 換言之, select 語句只是將SQL的查詢結果以一定的方式(選字段、計算值等等)提取出來了。 php中的array_map表達的也是這層意思, “映射”與“投影”完全是一種意思的不同表達。
3.array_walk函數
array_walk函數沒有像 array_map和array_filter這樣深刻的意義, 但是它在設計可讀性良好的代碼時也是不可或缺的。
array_walk是for或foreach語句的替代函數
以上代碼分別是 foreach和array_walk對於遍歷數組的實現方式。 看起來, 好像array_walk的實現方式更加復雜, 但是在更深層次的語義方面
foreach表達的是循環遍歷, 但是在這個循環的過程中,要做什么樣的處理,是沒有任何約束的, 刪除被遍歷的數組的某一項 ,或者修改一個十萬八千里以外的變量的值,這便是所謂的“代碼副作用”,俗話說“白蟻雖小, 危害無窮”, 當這些看似微不足道的副作用發展壯大時, 便會給程序員維護程序代碼帶來的障礙是致命的。
而array_walk函數缺省情況下所有執行代碼的作用域都在匿名函數內,如果要依賴或操作函數之外的數據, 必須通過匿名函數的use關鍵字導入。通俗一點的請, array_walk函數的權限不如foreach來的大, 因此,使用array_walk函數后,雖然無法讓你隨心所欲的編程,但是大限度的減少了你代碼的副作用,兩相權衡array_walk所帶來的好處還是有值得使用它的理由的。 首先, 大多數時候寫代碼根本不需要太大的“權限”,其次, 把代碼所影響的范圍控制到最小好處不言而喻。微信張小龍講過,微信做的最好的一點便是“克制”,我們寫代碼又何嘗不是。這一點array_filter和array_map中也有體現, 寬泛的講,所有使用匿名函數的地方都能享受到這個好處。
array_walk所表達的語義就是“假如你需要用到我, 那么你除了遍歷以外,其它的事情最好都別干,否則你還是去用原生的foreach吧”
4.array_reduce函數
array_reduce是上面所講的三個函數的集大成者,這三個函數的底層完全可以由array_reduce實現。
先看一下下面的php代碼
常規的PHP寫法,代碼分別用於計算數組記錄中平均年齡和最大年齡,代碼需要循環數組,並把計算結果存入一個標量(單個值,區分於列表變量)。
假如要以表達式編程的方式完成編寫這兩個功能, 利用array_filter、 array_walk、array_map三個函數是很難一部到位的實現的。
於是, 就到了array_reduce大顯身手的時候了
上面的代碼是求平均年齡和最大年齡的表達式編程的實現,如果對array_reduce函數的工作機制不了解,看上面兩段代碼會覺得在看天書。
這是 array_reduce函數的實現代碼,函數有3個參數, 3個參數的作用分別是
第一個參數$data, 就要是處理的數據源
第二個參數$callback,循環遍歷時會被調用的函數,函數返回的結果在下一次循環調用時會被再次當成參數傳入。
第三個參數$initial,作為$callback函數被初次調用時的參數傳遞
再來一個遞歸版本的array_reduce實現,幫助更好的理解這個函數的使用意義
善用array_reduce函數幾乎可以替換掉絕大多數需要使用foreach、for、while語句的代碼。
在標准的函數式編程語言中, 是沒有循環控制語句的,假如要進循環計算, 都是使用此類函數來實現的, 如果某些極端的情況下這些函數無法滿足需求,那么就以手動寫遞歸來實現循環, 以達到表達式編程的目的。
總結一下, 為什么要在寫php代碼時使用這4個函數
1.通過函數本身的意義就能表達出代碼實現了什么樣的功能,而不用去琢磨代碼具體細節來理解代碼的作用
2.表達式編程相對於命令式編程能極大的簡化功能的實現過程, 提升編碼效率
3.表達式編程對於代碼的可讀性、可維護性具有非凡的意義
4.利用匿名函數控制代碼的副作用
5.由傳統的面向過程式程序設計向現代化的函數式編程靠攏
補充:
通過前面示例的講解, 利用這4個函數實現的代碼相對於傳統的實現方式並沒有不可思議的變化, 然而, 當需要解決的問題復雜到一定程度時, 合理利用這4個函數會使代碼的復雜性大規模下降。