-
前言
這篇其實是上兩篇的兩個主題思想的承接和發散:
-
- 我也想少寫注釋,想用2-4個很清晰的單詞去描述函數,但是這個函數好復雜啊,我恨不得寫近百字去描述它,要我用幾個單詞去描述?臣妾實在是做不到啊~ <如何做到少寫注釋>
- 我也不想寫這么多if else,然后看着那一堆一堆{}{{}{}{{}}}}}}}{{{}{{}頭暈眼花,但邏輯就是有這么復雜,我能怎么辦呢? <如何簡化代碼邏輯>
這篇博文,應該就是我對於以上問題結合設計原理的一些思考,不算多高深,但都是自己的總結,我也不會去談xx設計模式,因為我覺得設計模式的本質就是讓你寫更好的代碼,而不是反之,所以理解它背后的思想,才是真正有價值的東西.
- 盡可能讓你的函數符合"純函數"標准
先介紹下什么是"純函數" 純函數其實並沒有一個很統一的定義,像Haskell的定義,就太苛刻,幾乎是數學領域了,我比較認同下面這個定義:
純函數應該具有以下兩個特性:
-
-
它沒有任何副作用。 函數不會更改函數以外的任何變量或任何類型的數據。
-
它具有一致性。 在提供同一組輸入數據的情況下,它將始終返回相同的輸出值。
-
我自己總結下,意思是一個設計良好的函數,應該就像一個黑盒子一樣,你完全不需要關注函數內部的實現,你只需要關注三點, 1.函數名 2.函數接受的參數類型 3.函數返回值的類型,只要我們確定了這三 點,我們即可完全"掌控"這個函數, 我們給定一個輸出,必然會返回預設的結果,這個結果不受其他任何因素的干擾. 當然,這其實是最理想的情況,"純函數"也並非就是非黑即白的定性修飾,它更多的是一個程度上的修飾,有些函數是無論如何也不可能寫成成純函數的,比如訪問非托管資源的函數. 但我們可以這樣說:FunA和FunB都不是純函數,但FunA比FunB更"純函數"(可以類比"聲明式"這個概念).
, 更具體的介紹,可以看msdn里面的一個小專題 純函數轉換簡介 .
那么,我們為什么要寫純函數呢?因為省事省心, 直接來兩段代碼,
public void DoSthWithTwoVariable1() { var p1 = Session["P1_key"]; var p2 = _p2; //......DosthWith p1 and p2 } public void DoSthWithTwoVariable2(Type1 p1 , Type2 p2) { //......DosthWith p1 and p2 }
第一個函數要考慮的東西很多,比如session里面是否有值,-p2這個全局變量會不會受到其他地方的干擾,而這些其實不該是doSth應該關心的,它的職責范圍被擴大了.
這兩個函數,其他人或者過段時間我們自己調用的時候,誰更讓人放心?
所以我們要使函數顯得純.第一步就是盡可能避免全局變量,我們分析一個函數,就只分析這個函數的全部代碼(有效范圍)就好,如果引入了全局變量,我們分析的時候,關注范圍也難免會被強制擴大到全局,同理,能聲明為靜態函數的,就應該避免聲明為成員函數,因為成員函數可以訪問對象的實例,而該對象在調用成員函數的時候,是個什么狀態,有無初始化,函數是否會修改實例(引用類型)的參數,如果我們要對這個函數做重構,就難免會束手束腳.
寧願多花一點功夫,將需要的變量在封裝的純函數中不斷傳遞,也不要輕易將它設置為全局變量,因為在函數中傳遞,按照你調用的順序,它的流程仍然是穩定的,而一旦使用全局變量,那么它就失去的約束,在哪里被人初始化了?怎么初始化的,順序是不是按我要求的,有沒有哪個地方在我做第二次初始化之前,就調用了第二次處理的功能邏輯?
再看一個例子:
public void SetType3() { var p1 = this._p1; var p2 = this._p2; //......Deal p1 and p2 this._p3 = xxx; } public static void SetType3(MyClass obj) //靜態函數,但修改了實例的成員 不是純函數 { var p1 = obj._p1; var p2 = obj._p2; //......Deal p1 and p2 obj._p3 = xxx; } public static void SetType3(Type1 p1, Type2 p2, MyClass obj) //靜態函數,但修改了實例的成員 不是純函數 { //......Deal p1 and p2 obj._p3 = xxx; } public static Type3 GetType3(Type1 p1, Type2 p2) { //......Deal p1 and p2 Type3 p3 = xxx; return p3; }
以上四個函數的純函數程度,是依次遞增的,都是大家很常用的寫法,那么這四個函數的區別是什么呢?
是我們調用者對函數內部實現邏輯的關注程度,依次遞減,他們的功能也越來越純粹(意味着更容易提煉和復用),調用起來也更省心,
當然,也難免會更瑣碎,比如GetType3,還需要做一些具體的取值,傳值,賦值操作.
其實他們也沒有什么優劣之分,這之間的度,自己把握就好.
Ps: 2015-01-29 11:06:30補充:
今天看Qunit官網介紹,發現里面一個例子,也是我這種思想的一個印證: Make Things Testable
function prettyDate(time) VS function prettyDate(now, time) //前者內部聲明了now(當前時間),后者作為參數傳遞進去。
很明顯,qunit官方也是推薦這種純函數式的風格的。
Ps: 2016-2-15 11:27:59補充:
今天學習React的時候,看見他們對Component里面方法的分類,同樣有這種思想的體現,
Methods defined within this block are static, meaning that you can run them before any component instances are created, and the methods do not have access to the props or state of your components. If you want to check the value of props in a static method, have the caller pass in the props as an argument to the static method.
通過對函數的合理分工,讓整個組件的邏輯更加清晰工整.
我現在越來越覺得,我們最需要提高的,不是編程語言,功能庫的熟練程度,而是我們對"美"的感知能力,當一個事物被精心雕琢,融入了作者的智慧與心血時,它自然而然就會散發一種美的光芒.
- 函數內部的變量,盡可能少,聲明盡可能晚,絕對禁止一值多用
變量盡可能少: 函數內部的變量,有效范圍是整個函數,如果我們在函數前面聲明了10個變量,那么我們都必須時刻關注這些變量的使用情況,有些變量其實就在前面用了一次,但后來閱讀的時候,你也不記得后面是不是還用到了它,所以減少變量數量,就意味着減少代碼復雜度.舉例:
//取得操作實例,根據id取得對象,取出最終我們要的state, // appointmentManager,thisAppointment這兩個變量我們都只用了一次,但以后看的時候,我們也不確定后面還用不用 var appointmentManager = ManagerFactory.Create<AppointmentManager>(); var thisAppointment = appointmentManager.GetById(appId); var state = thisAppointment.State; //其實可以這樣,那么我們只需要關注一個state就好,閱讀壓力大大減少 var state = ManagerFactory.Create<AppointmentManager>().GetById(appId).State;
聲明盡可能晚:可能我們寫類的時候養成了習慣,將變量放在最上面,統一聲明,易於整理和查閱. 其實類的聲明和函數的聲明是不一樣的,類的所有成員(變量和函數)都是無所謂先后的,而函數里面的局部變量,則是有先后順序的,我們在不必要的地方引入了不必要的約束,也就意味着不必要的麻煩.
比如我們有一個200行代碼的函數,我們在最前面聲明了10個變量,這些變量是依次在函數不同部位使用的,但因為在最前面已經聲明了,所以我們閱讀這個函數的時候,也需要時刻注意這10個變量在函數中的使用情況, 這里我們簡單的引入一個"關注度"的概念: G = 變量個數*變量的有效代碼范圍 ,那么這時候的總G數 = 10*200 = 2000.
而如果開始只聲明2個變量,剩下的變量在使用的時候才聲明,比如p3,p4是在101行代碼里面聲明的,那么你閱讀1-100行代碼的時候,就不需要關注p3,p4了(也沒法關注,都還沒聲明呢),然后剩下6個變量在151行聲明,那么現在的關注度,就只有G=2*200 +2*100 +6*50 = 900.
禁止一值多用:前面不是說要盡可能少的聲明變量么,有些人就這樣做:比如我聲明一個state,表示Appointment的狀態,用完之后,后面需要用訂單狀態的時候,我仍然用state字段去接值,參與新的,屬於Order的業務邏輯,這個我還真見過.不過相信這種大神應該還是極少數吧.
- 重構還是不重構,這不是個問題
幾乎所有提到程序設計的書籍,都是推薦將函數中比較獨立的業務抽取出來,放在一個新的函數中,好處很多:結構清晰,代碼復用,業務解耦合.
但有時候我們的情況很尷尬,說功能獨立吧,也不是特別獨立,說要提公吧,其實在其他地方用的可能性也不大,但要就這樣和主體業務放在一起,代碼也確實顯得比較亂,提公之后,又將業務邏輯分散了,這種情況應該怎么辦呢?
其實我們可以選一個折中的方案:委托.
比如一個流程,需要在保存之前篩選初始數據,這個篩選的方法很大可能只在這里用(但也不排除以后再其他地方也會用,雖然可能性不大),和主體業務耦合也比較強,其實我們可以在函數中聲明一個
Func<IList<Product>, AttrItemDTO, bool> FilterProduct1= (lambda Express) 或Func<IList<Product>, AttrItemDTO,int, bool> FilterProduct2= (lambda Express)
我們可以通過傳遞參數的形式,寫成純函數形式的FilterProduct2(第三個參數就是state),也可以寫成FilterProduct1,在lambda里面直接使用前面函數中聲明的"全局變量"state,
這兩者都是將篩選這一流程進行了一次折中的"重構",而且花銷很小, 首先它的業務邏輯還是線性順序進行的,一條線下來,再次即使以后需要重構或者提公,也非常容易.
Ps:其實委托和lambda等函數式思維的引入,真的可以給我們帶來很多新的思維啟發, 不過可能是我們以前都太習慣於過程式的編碼, 還需要鍛煉鍛煉這種新的開發理念吧.
Ps2: 關於這種函數式寫法的一個非常炫酷的示例,可以參考下csdn .NET斑竹caozhy寫的一個數獨游戲