今天在開發一個簡單查詢時,發現我的Lambda操作類的GetValue方法無法正確獲取枚舉類型值,以至查詢結果錯誤。
我增加了幾個單元測試來捕獲錯誤,代碼如下。
/// <summary>
/// 測試值為枚舉 /// </summary>
[TestMethod] public void TestGetValue_Enum() { var test1 = new Test1(); test1.NullableEnumValue = LogType.Error; //屬性為枚舉,值為枚舉
Expression<Func<Test1, bool>> expression = test => test.EnumValue == LogType.Debug; Assert.AreEqual( LogType.Debug.Value(), Lambda.GetValue( expression ) ); //屬性為枚舉,值為可空枚舉
expression = test => test.EnumValue == test1.NullableEnumValue; Assert.AreEqual( LogType.Error, Lambda.GetValue( expression ) ); //屬性為可空枚舉,值為枚舉
expression = test => test.NullableEnumValue == LogType.Debug; Assert.AreEqual( LogType.Debug, Lambda.GetValue( expression ) ); //屬性為可空枚舉,值為可空枚舉
expression = test => test.NullableEnumValue == test1.NullableEnumValue; Assert.AreEqual( LogType.Error, Lambda.GetValue( expression ) ); //屬性為可空枚舉,值為null
test1.NullableEnumValue = null; expression = test => test.NullableEnumValue == test1.NullableEnumValue; Assert.AreEqual( null, Lambda.GetValue( expression ) ); }
單元測試成功捕獲了Bug,我打開Lambda操作類,准備修改GetValue方法,代碼見Util應用程序框架公共操作類(八):Lambda表達式公共操作類(二)。
面對GetValue雜亂無章的代碼,我頓時感覺無法下手,必須徹底重構它。
我之前也看過一些Lambda表達式解析的代碼和文章,基本都是使用NodeType來進行判斷。我一直沒有使用這種方式,是因為NodeType數量龐大,並且多種NodeType可能轉換為同一種Expression類型。我當時認為用switch判斷NodeType工作量太大,所以直接采用As轉換為特定表達式,再判斷是否空值。
我把這種山寨方法稱為瞎貓碰到死耗子,主要依靠單元測試來捕獲需求,通過斷點調試,我可以知道轉換為哪種特定表達式。這種方法在前期看上去貌似很有效,比判斷NodeType的代碼要少,但由於使用表達式的方式千差萬別,負擔越來越重,以至無法維護了。
為了徹底重構GetValue方法,我需要補充一點表達式解析的知識,我打開開源框架linq2db,仔細觀察他是如何解析的。終於看出點眉目,依靠NodeType進行遞歸判斷。
我以前只知道使用NodeType進行判斷,但不知道應該采用遞歸的方式,真是知其然不知其所以然。
我對GetValue進行了重構,代碼如下。
/// <summary>
/// 獲取值,范例:t => t.Name == "A",返回 A /// </summary>
/// <param name="expression">表達式,范例:t => t.Name == "A"</param>
public static object GetValue( Expression expression ) { if ( expression == null ) return null; switch ( expression.NodeType ) { case ExpressionType.Lambda: return GetValue( ( (LambdaExpression)expression ).Body ); case ExpressionType.Convert: return GetValue( ( (UnaryExpression)expression ).Operand ); case ExpressionType.Equal: case ExpressionType.NotEqual: case ExpressionType.GreaterThan: case ExpressionType.LessThan: case ExpressionType.GreaterThanOrEqual: case ExpressionType.LessThanOrEqual: return GetValue( ( (BinaryExpression)expression ).Right ); case ExpressionType.Call: return GetValue( ( (MethodCallExpression)expression ).Arguments.FirstOrDefault() ); case ExpressionType.MemberAccess: return GetMemberValue( (MemberExpression)expression ); case ExpressionType.Constant: return GetConstantExpressionValue( expression ); } return null; } /// <summary>
/// 獲取屬性表達式的值 /// </summary>
private static object GetMemberValue( MemberExpression expression ) { if ( expression == null ) return null; var field = expression.Member as FieldInfo; if ( field != null ) { var constValue = GetConstantExpressionValue( expression.Expression ); return field.GetValue( constValue ); } var property = expression.Member as PropertyInfo; if ( property == null ) return null; var value = GetMemberValue( expression.Expression as MemberExpression ); return property.GetValue( value ); } /// <summary>
/// 獲取常量表達式的值 /// </summary>
private static object GetConstantExpressionValue( Expression expression ) { var constantExpression = (ConstantExpression)expression; return constantExpression.Value; }
運行了全部測試,全部通過,說明沒有影響之前的功能。這正是自動化回歸測試的威力,如果沒有單元測試,我哪里敢重構這些代碼呢。另外,修改Bug采用TDD的方式,能夠一次修復,永絕后患,值得你擁有。
同時,我還重構了其它類似的代碼,就不再貼出,下次我發放源碼時,有興趣可以看看。
.Net應用程序框架交流QQ群: 386092459,歡迎有興趣的朋友加入討論。
謝謝大家的持續關注,我的博客地址:http://www.cnblogs.com/xiadao521/