程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> 趣味編程:C#中Specification模式的實現(參考答案 - 下)

趣味編程:C#中Specification模式的實現(參考答案 - 下)

編輯:關於C#

上一篇文章中我們利用C#語言的特性實現了一種輕量級的Specification模式,它的關鍵在於拋棄了具體的Specification類型,而是使用一個委托對象代替唯一關鍵的IsSatisfiedBy方法邏輯。據我們分析,其優勢之一在於使用簡單,其劣勢之一在於無法靜態表示。但是它們還都是在處理“業務邏輯”,如果涉及到一個用於LINQ查詢或其他地方的表達式樹,則問題就不那麼簡單了——但也沒有我們想象的那麼復雜。

好,那麼我們就把場景假想至LINQ上。LINQ與普通業務邏輯不同的地方在於,它不是用一個IsSatisfiedBy方法或一個委托對象用來表示判斷邏輯,而是需要構造一個表達式樹,一種數據結構。如果您還不清楚表達式樹是什麼,那麼可以看一下腦袋的寫的上手指南。這是.NET 3.5帶來的重要概念,在4.0中又得到了重要發展,如果您要在.NET方面前進,這是一條必經之路。

And、Or和Not之間,最容易處理的便是Not方法,於是我們從這個地方下手,直接來看它的實現:

public static class SpecExprExtensions
{
  public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> one)
  {
    var candidateExpr = one.Parameters[0];
    var body = Expression.Not(one.Body);

    return Expression.Lambda<Func<T, bool>>(body, candidateExpr);
  }
}

一個Expression<TDelegate>對象中主要有兩部分內容,一是參數,二是表達式體(Body)。對於Not方法來說,我們只要獲取它的參數表達式,再將它的Body外包一個Not表達式,便可以此構造一個新的表達式了。這部分邏輯非常簡單,看了腦袋的文章,了解了表達式樹的基本結構就能理解這裡的含義。那麼試驗一下:

Expression<Func<int, bool>> f = i => i % 2 == 0;
f = f.Not();

foreach (var i in new int[] { 1, 2, 3, 4, 5, 6 }.AsQueryable().Where(f))
{
  Console.WriteLine(i);
}

打印出來的自然是所有的奇數,即1、3、5。

而And和Or的處理上會有所麻煩,我們不能這樣像Not一樣簡單處理:

public static Expression<Func<T, bool>> And<T>(
  this Expression<Func<T, bool>> one, Expression<Func<T, bool>> another)
{
  var candidateExpr = one.Parameters[0];
  var body = Expression.And(one.Body, another.Body);
  return Expression.Lambda<Func<T, bool>>(body, candidateExpr);
}

這麼做雖然能夠編譯通過,但是在執行時便會出錯。原因在於one和another兩個表達式雖然都是同樣的形式(Expression<Func<T, bool>>),但是它們的“參數”不是同一個對象。也就是說,one.Body和another.Body並沒有公用一個 ParameterExpression實例,於是我們無論采用哪個表達式的參數,在Expression.Lambda方法調用的時候,都會告訴您新的 body中的某個參數對象並沒有出現在參數列表中。

於是,我們如果要實現And和Or,做的第一件事情便是統一兩個表達式樹的參數,於是我們准備一個ExpressionVisitor:

internal class ParameterReplacer : ExpressionVisitor
{
  public ParameterReplacer(ParameterExpression paramExpr)
  {
    this.ParameterExpression = paramExpr;
  }

  public ParameterExpression ParameterExpression { get; private set; }

  public Expression Replace(Expression expr)
  {
    return this.Visit(expr);
  }

  protected override Expression VisitParameter(ParameterExpression p)
  {
    return this.ParameterExpression;
  }
}

ExpressionVisitor幾乎是處理表達式樹這種數據結構的不二法門,它可以用於求值,變形(其實是生成新的結構,因為表達式樹是immutable的數據結構)等各種操作。例如,解決表達式樹的緩存時用它來求樹的散列值或讀寫前綴樹,快速計算表達式時用它來提取表達式樹的參數,並將不同的表達式樹“標准化”為相同的結構。

ExpressionVisitor基類的關鍵,就在於提供了遍歷表達式樹的標准方式,如果您直接繼承這個類並調用Visit方法,那麼最終返回的結果便是傳入的Expression參數本身。但是,如果您覆蓋的任意一個方法,返回了與傳入時不同的對象,那麼最終的結果就會是一個新的 Expression對象。ExpressionVisitor類中的每個方法都負責一類表達式,也都都遵循了類似的原則:它們會遞歸地調用Visit方法,如果Visit返回新對象,那麼它們也會構造新對象並返回。

ParameterReplacer類的作用是將一個表達式樹裡的所有ParameterExpression替換成我們指定的新對象,因此只需覆蓋VisitParameter方法就可以了。有了它之後,And和Or方法的實現輕而易舉:

public static Expression<Func<T, bool>> And<T>(
  this Expression<Func<T, bool>> one, Expression<Func<T, bool>> another)
{
  var candidateExpr = Expression.Parameter(typeof(T), "candidate");
  var parameterReplacer = new ParameterReplacer(candidateExpr);

  var left = parameterReplacer.Replace(one.Body);
  var right = parameterReplacer.Replace(another.Body);
  var body = Expression.And(left, right);

  return Expression.Lambda<Func<T, bool>>(body, candidateExpr);
}

public static Expression<Func<T, bool>> Or<T>(
  this Expression<Func<T, bool>> one, Expression<Func<T, bool>> another)
{
  var candidateExpr = Expression.Parameter(typeof(T), "candidate");
  var parameterReplacer = new ParameterReplacer(candidateExpr);

  var left = parameterReplacer.Replace(one.Body);
  var right = parameterReplacer.Replace(another.Body);
  var body = Expression.Or(left, right);

  return Expression.Lambda<Func<T, bool>>(body, candidateExpr);
}

於是,我們最終構造得到的Expression<Func<T, bool>>對象便可以傳入一個LINQ Provider,最終得到查詢結果:

Expression<Func<int, bool>> f = i => i % 2 == 0;
f = f.Not().And(i => i % 3 == 0).Or(i => i % 4 == 0);

foreach (var i in new int[] { 1, 2, 3, 4, 5, 6 }.AsQueryable().Where(f))
{
  Console.WriteLine(i);
}

輸出的結果是3和4。

這種做法是非常有實用價值的。因為有了LINQ,因此許多朋友都會選擇在數據訪問層暴露一個這樣的方法給上層調用:

class ProductDao
{
  public Product GetProduct(Expression<Func<Product, bool>> predicate)
  {
    ... 
  }
}

但是您有沒有想過這麼做的缺點是什麼呢?這麼做的缺點便是“過於自由”。由於GetProduct方法只將參數限制為一個 Expression<Func<Product, bool>>對象,因此在調用的時候,我們可以使用任意的形式進行傳遞。因此,外層完全有可能傳入一個目前LINQ Provider不支持的表達式樹形式,也完全有可能傳入一個雖然支持,但會導致查詢速度慢,影響項目整體性能的表達式樹。前者要在運行時才拋出異常,而後者則引發的性能問題則更難發現。因此我認為,數據訪問層不應該如此自由,它要做出限制。而限制的方式,便是使用Query Object模式,讓GetProduct方法接受一個受限的Criteria對象:

public abstract class ProductCriteria
{
  internal ProductCriteria(Expression<Func<Product, bool>> query)
  {
    this.Query = query;
  }

  public Expression<Func<Product, bool>> Query { get; private set; }
}

class ProductDao
{
  public Product GetProduct(ProductCriteria predicate)
  {
    ... 
  }
}

而在使用時,我們只提供有限的幾種條件,如:

public class ProductIdEqCriteria : ProductCriteria
{
  public ProductIdEqCriteria(int id)
    : base(p => p.ProductID == id)
  { }
}

public class ProductViewRangeCriteria : ProductCriteria
{
  public ProductViewRangeCriteria(int min, int max)
    : base(p => p.ViewCount > min && p.ViewCount < max)
  { }
}

再加上配套的擴展方法用於And,Or,Not,於是一切盡在掌握。現在再去瞅瞅原Query Object模式中復雜的實現,您是否會有一種滿足感?

文章來源:http://www.cnblogs.com/JeffreyZhao/archive/2009/09/29/specification-pattern-in-csharp-practice-answer-2.html

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved