只為了有趣的練習 -- 複合 where 查詢

這只是一個純粹為了有趣的練習題,不需要太嚴肅看待。

前情提要

 假設我們有一個資料結構是這樣 (github link)

  public enum Gender
  {
      None, Male, Female
  }


  /// <summary>
  /// immmutable
  /// </summary>
  public class Person
  {
      public string Name { get; }
      public int Age { get; }

      public Gender Gender { get; }

      public Person(string name, int age, Gender gender)
      {
          Name = name;
          Age = age;
          Gender = gender;
      }
  }

依據這個資料形式產出一個 List<Person> people,接著依照以下條件查詢:

(1) 輸入:string name, int age, Gender gender,依照前置條件決定後設條件是否為查詢條件

(2) 如果 !string.IsNullOrWhiteSpace (name) people.Where((y) => y.Name.Contains (name))

(3) 如果 age > 0people.Where((y) => y.Age < age)

(4) 如果 gender != Gender.Nonepeople.Where((y) => y.Gender == gender)

題目說明

(1) 如果輸入 ("B", 24 , Gender.Male) 則會找出 people 中 Name 包含 "B" 、Age 小於 24 以及 Gender 為 Gender.Male 者

(2) 如果輸入 (default(string), 0, Gender.Female) 則會找中 people 中所有 Gender 為 Gender.Female 者,不論 Name 與 Age 為何

(3) 如果輸入 (default(string), 0, Gender.None) 則忽略所有條件回傳整個 people

題目解析

看起來好像很複雜,仔細想想其實整個邏輯滿簡單的。我們以 A 代表前置條件、B 代表後設條件,就能把公式轉換為:( !A1 || B1 ) && ( !A2 || B2 ) && ( !A3 || B3 )

根據這條公式,我們得到第一個解法 (github link),為了方便閱讀,我把所有的 Not 條件全倒過來寫。

例如 ! age > 0 改成 age < 1。

 public static class DirectSolution
 {
     public static IEnumerable<Person> Execute(this IEnumerable<Person> source, string name, int age, Gender gender)
     {
         return source.Where((x) =>  
                                     (string.IsNullOrWhiteSpace(name) || x.Name.Contains(name))
                                  && (age < 1 || x.Age < age)
                                  && ((gender == Gender.None) || (x.Gender == gender)));
                             
     }
 }
建立模式解題

這樣解題太無聊了,來做點有趣的事情。假設這樣的模式會一再出現,而且可能會應用在不同型別,上面那種直接的解題方式就會寫得讓人覺得很煩。依照我喜歡搞怪的個性,就來發展一下各式各樣的解題方法。

(1) 聯合條件直覺法:簡單說就是把前面的公式化成一條可以解決同樣形式問題的程式碼,也就是分組為 ( A1,B1 )、 ( A2,B2 ) 與 ( A3,B3 ),若三組條件都是 true,則表示符合條件需回傳。事實上這是我最喜歡的作法,簡單易懂、反璞歸真。(github link)

 public static class UnionCondition
 {
     public static bool NeedExecute<TCondition, TSource>(this IEnumerable<ConditionExpression<TCondition, TSource>> expressions, TCondition condition, TSource source)
     {
         return expressions.All((y) => !y.Precondition(condition) ||  y.Postcondition(source));
     } 
 }

(2) 聯合委派法:有點類似 (1),但聯合的是委派本身,而不是執行後的 bool 結果。(github link)

 public static class UnionDelegateExtension
 {
     public static Func<T, bool> CombineExpression<T>(this Func<T, bool> precondition, Func<T, bool> postcondition)
     {
         if (precondition == null && postcondition == null )
         {
             throw new ArgumentNullException();
         }

         if (precondition == null && postcondition != null)
         {
             return postcondition;
         }

         if (precondition != null && postcondition == null)
         {
             return precondition;
         }

         return (x) => precondition(x) && postcondition(x);
     }
 }

(3) 聯合委派法 by Aggregate :基本上這是 (2) 的變形,只是改用 Enumerable.Aggregate 替代使用 foreach 語法的結合方式 (github link)

  public static  class AggregateExtension
  {

      public static Func<T2, bool> AggregateExpression<T1, T2>(this IEnumerable<ConditionExpression<T1, T2>> expressions, T1 condition)
      {
          return expressions.Where((x) => x.Precondition(condition)).Select((y) => y.Postcondition).Aggregate((x, y) => (z => x(z) && y(z)));
      }
  }

(4) 遞迴結合法:利用遞迴運算的方式,一直回傳 Where 查詢後的結果再套入為下一個的資料來源,這方式我也覺得很有趣。(github link)

 public static class Recursive
 {
     public static IEnumerable<T2> Execute<T1, T2>(this IEnumerable<T2> source, List<ConditionExpression<T1, T2>> expressions, T1 condition)
     {
         if (expressions.Count > 0)
         {
             source = !expressions[0].Precondition(condition) ? source : source.Where(expressions[0].Postcondition);
             expressions.RemoveAt(0);
             return source.Execute(expressions, condition);
         }
         return source;
     }
 }

(5) 交集法:先取得各組條件的 IEnumerable<T>,最後使用 Enumerable.Intersect 取得所有 IEnumerable<T> 的交集 (github link)

 public static class Intersection
 {
     public static IEnumerable<T2> Execute<T1, T2>(this IEnumerable<T2> source, IEnumerable<ConditionExpression<T1, T2>> expressions, T1 condition)
     {
         return source.InnerExecute(expressions, condition).Aggregate((x, y) => x.Intersect(y));
     }

     private static IEnumerable<IEnumerable<T2>> InnerExecute<T1, T2>(this IEnumerable<T2> source, IEnumerable<ConditionExpression<T1, T2>> expressions, T1 condition)
     {
       return   expressions.Where((x) => x.Precondition(condition)).Select((y) => source.Where (y.Postcondition));         
     }
 }

剩下幾個方式比較無趣一點,就不多作介紹了。

(6) 範本方法模式 (github link)

(7) 策略模式 (github link)

(8) 委派型的策略模式 (github link)

就是個單純寫程式的樂趣,如果對這題目有興趣可以 fork LinqForFun,或許我還會想到奇怪的主意也不一定,有趣就好。