篇首語
在基礎理論篇當中已經向大家介紹了Func類、函數閉包及函數柯裡化等內容,進而介紹了函數式編程在Linq當中的運用。本文將延續這一話題,繼續討論函數式在重構等方面的一些技巧,希望能對大家的工作帶來一些啟發。
本文面向有一定基礎的讀者,如果在閱讀過程中您看不懂某些術語或代碼,請移步《C#函數式程序設計初探——理論基礎篇》。注意,本文提供的一些思路僅供參考,切勿盲目模仿,否則後果自負。
主要內容
利用閉包緩存數據,令方法按需執行,提煉重復參數
第一部分 利用閉包緩存數據
首先來看一段簡單的示例代碼:
class Program
{
static void Main(string[] args)
{
int num = 10;
int result1 = Calculator.Square(10);
int result2 = Calculator.Square(10);
Console.WriteLine(result1);
Console.WriteLine(result2);
Console.ReadKey();
}
}
public class Calculator
{
private static Dictionary<int, int> SquareResult = new Dictionary<int, int>();
public static int Square(int x)
{
if (!SquareResult.ContainsKey(x))
{
Console.WriteLine("緩存計算結果");
SquareResult[x] = x * x;
}
return SquareResult[x];
}
}Square是一個帶有緩存功能的求平方算法,它將計算的結果緩存在了一個詞典當中防止重復計算,這個技巧在進行很復雜的計算(比如求正弦)當中是比較有用的(空間換時間)。
在我們的日常工作當中相信大家都寫過或者遇見過類似的代碼:一個詞典被放置在工廠類當中作為單例容器,也就是所謂的“池模式”。
這裡的求平方只是一個為了說明問題的簡單例子,現實需求往往更加復雜,而使用設計模式是需要結合實際需求場景的。設想這樣的情景:如果這個計算並不像計算平方這樣長期通用,而是希望“緩存詞典”中的內容僅僅是在這個方法(算法)的內部多次使用(即離開了Square的調用函數Main,我就想要釋放這個詞典),那麼我就毫無必要為了解決一個算法的時間性能優化的具體問題點,而引入一個新的靜態類來污染整個面向對象結構,一方面這樣做導致了類數量的膨脹,另一方面調用函數Main與靜態類Calculator發生了強耦合調用關系。如果我們的系統中到處都充滿了Calculator這樣的類,就大大增加了理解、維護和接手的成本。
在這種情況下,很容易我們就能想到把這個函數定義到調用函數的內部,這個思路和《重構》當中的“提煉方法”是完全相反的(恐怕是因為在Java裡沒法這麼搞),代碼如下:
class Program
{
static void Main(string[] args)
{
Dictionary<int, int> SquareResult = new Dictionary<int, int>();
Func<int,int> Square = x => {
if (!SquareResult.ContainsKey(x))
{
Console.WriteLine("緩存計算結果");
SquareResult[x] = x * x;
}
return SquareResult[x];
};
int num = 10;
int result1 = Square(10);
int result2 = Square(10);
Console.WriteLine(result1);
Console.WriteLine(result2);
Console.ReadKey();
}
}這樣一來,我們就從系統當中“干掉”了一個扎眼的靜態類,再者,我們發現在後續的調用代碼中並沒有使用SquareResult這個集合變量,那麼我們可以說這個變量同樣污染了函數空間,於是乎想到通過柯裡化的方式把這個集合移動到Square方法的內部:
class Program
{
static void Main(string[] args)
{
Func<Func<int,int>> GetSquareFunc = () => {
Dictionary<int, int> SquareResult = new Dictionary<int, int>();
return x => {
if (!SquareResult.ContainsKey(x))
{
Console.WriteLine("緩存計算結果");
SquareResult[x] = x * x;
}
return SquareResult[x];
};
};
Func<int,int> Square = GetSquareFunc();
int num = 10;
int result1 = Square(10);
int result2 = Square(10);
Console.WriteLine(result1);
Console.WriteLine(result2);
Console.ReadKey();
}
}首先我們定義了一個返回函數的函數起名叫GetSquareFunc,在其中定義了一個詞典的局部變量,並在這個函數內部返回一個閉包,這個閉包的內部調用了我們的詞典進行緩存和判斷。在調用時,我們首先要通過GetSquareFunc來動態生成一個求平方函數,之後使用這個運行時產生的函數來進行求平方操作。
在這裡我們看到了如何利用閉包與柯裡化的方式緩存數據,使用了函數式的手段進行代碼重構之後我們的世界清靜多了,不過有人可能會說這麼做有點“反OO”,這不是把算法和調用耦合到一個調用方法裡了嗎?是的,重構總會有一些副作用,所以說任何重構與模式的使用都是要結合需求情境的。同時也有人會問,你這不是多此一舉嗎,我干嘛不直接把這個緩存邏輯內聯在算法裡呢?那麼我想問,難道你希望用一堆#region/#endregion讓代碼成為很長的一坨嗎?
嗯,關於重構的話題已經脫離了本文的范圍,而且牽扯到心理學、強迫症、潔癖症等……總之,這是函數式的一個應用,我們還是從需求出發!
第二部分 令方法按需執行
首先來看一段代碼:
static void Main(string[] args)
{bool result = DoSth(2, GetList());
Console.WriteLine("執行結果" + result);
Console.ReadKey();
}
static bool DoSth(int x, List<object> list)
{
if (x < 10) return false;
//...
return true;
}
static List<object> GetList()
{
Console.WriteLine("獲取數據源,耗時5秒");
return new List<object>();
}這段代碼有一個容易被我們平常所忽略的诟病,在C#語言中,如果函數的參數是一個函數調用,那麼C#一定會先調用這個參數當中的函數,也就是說,如果DoSth的第一個參數小於10,那麼獲取數據源的5秒鐘就白白浪費掉了,而此時我們卻又不得不傳入一個list作為DoSth的參數!
顯然,這個DoSth的API設計是有問題的!那麼我們如何來改造這個方法呢?
相信大家一定都能想到在DoSth內部來獲取list之類的方法,在這裡,我們將讀取數據的方法作為一個參數傳進DoSth當中,並在其內部通過判斷後,“惰性”執行讀取數據源的方法:
static void Main(string[] args)
{
bool result = DoSth(2, GetList);
Console.WriteLine("執行結果" + result);
Console.ReadKey();
}
static bool DoSth(int x, Func<List<object>> GetListFunc)
{
if (x < 10) return false;
List<object> list = GetListFunc();
//...
return true;
} 這裡我們將獲取數據源的方法作為一個委托傳給了DoSth函數,並在函數內部通過判斷後執行這個委托來“延遲”獲取數據源,這樣一來就解決了獲取數據的問題。
有的人一定會問了,我完全可以在調用這個方法之前定義一個空的集合傳進去,判斷完成之後再讀取數據呀,何苦寫一堆Func什麼的呢?
答案是,難道你想為了一個方法定義一個私有的類級別成員嗎?如果到處都飄滿了這種零散的變量,那還有什麼面向對象可言呢?還記得你曾經在aspx.cs後台文件開頭寫的一堆一堆的變量初始化聲明嗎?
這個例子僅僅是演示一下Func作為參數實現延遲調用在重構當中的一個例子,其實這個API設計的真正症結在於它把判斷邏輯和業務執行邏輯緊緊耦合在了一個方法裡!也就是說要像讓這個函數更“純”一些,就應該把判斷邏輯移除到方法之外!
第三部分 提煉重復參數
假設在某數據庫訪問層有這樣一個裝填參數的輔助類:
static class SqlParamHelper
{
public static void SetParam(SqlCmd obj, SqlType type, string fieldName, object param);
}以及調用代碼:
SqlCmd sqlCmd = new SqlCmd();ParamHelper.SetParam(sqlCmd, SqlType.Guid, "PK", new Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"));
ParamHelper.SetParam(sqlCmd, SqlType.String, "Name", "小明");
ParamHelper.SetParam(sqlCmd, SqlType.Int, "Age", 20); 有沒有覺得這個代碼很蛋疼呢?我就在工作中見過一些類似這樣的例子,以上只是仿造的一段代碼。先拋開具體的數據庫應用不說,首先這個方法調用有一個共性的參數sqlCmd,從功能實現角度來講,我不得不傳進這個參數才能讓SetParam方法做一些有價值的事情,但是每次我都要傳進它去,顯得實在是太啰嗦了!我們有什麼方法來重構這段代碼呢?
核心問題在於,既然SetParam不得不用這個sqlCmd,那麼把它提出來了,秉承前面例子的思想,我不想搞一個單獨的變量來污染方法空間,那麼把它放在哪好呢?
答案就是使用閉包!
首先我們定義一個返回函數的方法:
static Action<SqlType,string,object> GetSetParamFunc(SqlCmd sqlCmd)
{
SqlCmd cmd = sqlCmd;
return (type, fieldName, param) => SqlParamHelper.SetParam(cmd, type, fieldName, param);
}思路是,既然我們要干掉這個參數,那我們既要把它緩存起來,並且返回一個可以利用這個參數的閉包,於是乎,調用的代碼就改變成了:
Action<SqlType,string,object> SetParam = GetSetParamFunc(sqlCmd);
SetParam(SqlType.Guid, "PK", new Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"));
SetParam(SqlType.String, "Name", "小明");
SetParam(SqlType.String, "Age", 20);首先通過GetSetParamFunc方法的調用,返回一個內部使用sqlCmd值的閉包函數,然後調用這個新獲取的函數,使用三個參數來調用。
另外,你有沒有發現這麼做之後,方法調用環境和SqlParamHelper靜態類的耦合全都被推到了GetSetParamFunc方法之中呢?有沒有體會出某些設計模式的味道呢?