Cでの仕様設計パターン#

プログラミングの「仕様」とは、ビジネスロジックルールの表現をブール論理演算で接続されたオブジェクトのチェーンに変換できる設計パターンです。


Evans DDDを読んでいるときに、この用語に慣れました。 Habréには、パターンの実用化と 実装プロセスで発生する問題を 説明する記事があります



要するに、「仕様」を使用する主な利点は、オブジェクトモデルのオブジェクトをフィルタリングするためのすべてのルールが集中している1つの理解できる場所があることです。



古典的なデザインパターンの実装は次のようになります。



public interface ISpecification { bool IsSatisfiedBy(object candidate); }
      
      





C#の何が問題になっていますか?



  1. Expression<Func<T, bool>>



    およびFunc<T, bool>>



    、その署名はIsSatisfiedByと同じです。
  2. 拡張メソッドがあります。 彼らの助けを借りてalexanderzaytsevはこのようにします:



     public class UserQueryExtensions { public static IQueryable<User> WhereGroupNameIs(this IQueryable<User> users, string name) { return users.Where(u => u.GroupName == name); } }
          
          





  3. そして、あなたはそのようなアドオンをLINQに実装することができます:



     public abstract class Specification<T> { public bool IsSatisfiedBy(T item) { return SatisfyingElementsFrom(new[] { item }.AsQueryable()).Any(); } public abstract IQueryable<T> SatisfyingElementsFrom(IQueryable<T> candidates); }
          
          





最終的に、疑問が生じます:Javaの世界で10年前のテンプレートであるC#を使用する価値はありますか?



このように価値があると判断しました。



 public interface IQueryableSpecification<T> where T: class { IQueryable<T> Apply(IQueryable<T> query); } public interface IQueryableOrderBy<T> { IOrderedQueryable<T> Apply(IQueryable<T> queryable); } public static bool Satisfy<T>(this T obj, Func<T, bool> spec) => spec(obj); public static bool SatisfyExpresion<T>(this T obj, Expression<Func<T, bool>> spec) => spec.AsFunc()(obj); public static bool IsSatisfiedBy<T>(this Func<T, bool> spec, T obj) => spec(obj); public static bool IsSatisfiedBy<T>(this Expression<Func<T, bool>> spec, T obj) => spec.AsFunc()(obj); public static IQueryable<T> Where<T>(this IQueryable<T> source, IQueryableSpecification<T> spec) where T : class => spec.Apply(source);
      
      





Func<T, bool>



どうですか



Func



からFunc



Expression



に切り替えるFunc



非常に困難です。 多くの場合、データベースへのクエリを作成するレベルにフィルタリングを転送する必要があります。そうしないと、何百万ものレコードを引き出してメモリ内でフィルタリングする必要がありますが、これは最適ではありません。



なぜExpression<Func<T, bool>>



ないのですか?



対照的に、 Expression



からFunc



への移行は簡単です: var func = expression.Compile()



。 ただし、式の作成は決して簡単な作業ではありません 。 式の条件付きアセンブリが必要な場合(たとえば、仕様に3つのパラメーターが含まれ、そのうちの2つがオプションである場合)、さらに快適ではありません。 しかし、 Expression<Func<T, bool>>



Expression<Func<T, bool>>



query.Where(x => someOtherQuery.Contains(x.Id))



ようなサブクエリを必要とする場合、非常に不十分です。



最終的に、この推論は、最も簡単な方法は、ターゲットのIQueryable



を変更して、流れるようなインターフェイスを介して渡すことであることを示唆しています。 追加のWhereメソッドにより、コードは通常のLINQ変換チェーンのように見えます。



このロジックに基づいて、ソートの抽象化を強調できます



 public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, IQueryableOrderBy<T> spec) where T : class => spec.Apply(source); public interface IQueryableOrderBy<T> { IOrderedQueryable<T> Apply(IQueryable<T> queryable); }
      
      





次に、 Dynamic Linqと少し特別な Reflection



ストリートマジックを追加することにより、基本的なオブジェクトを記述して、宣言的なスタイルで何かをフィルタリングできます。 以下のコードは、 AutoSpec



パブリックプロパティと、フィルタリングを適用するタイプを分析します。 一致が見つかり、 AutoSpec



後継AutoSpec



入力されている場合、 Queryable



はこのフィールドのフィルタールールを自動的に追加します。



 public class AutoSpec<TProjection> : IPaging, ILinqSpecification<TProjection>, ILinqOrderBy<TProjection> where TProjection : class, IHasId { public virtual IQueryable<TProjection> Apply(IQueryable<TProjection> query) => GetType() .GetPublicProperties() .Where(x => typeof(TProjection).GetPublicProperties().Any(y => x.Name == y.Name)) .Aggregate(query, (current, next) => { var val = next.GetValue(this); if (val == null) return current; return current.Where(next.PropertyType == typeof(string) ? $"{next.Name}.StartsWith(@0)" : $"{next.Name}=@0", val); }); IOrderedQueryable<TProjection> ILinqOrderBy<TProjection>.Apply(IQueryable<TProjection> queryable) => !string.IsNullOrEmpty(OrderBy) ? queryable.OrderBy(OrderBy) : queryable.OrderBy(x => x.Id); }
      
      





AutoSpec



は、 Expression



のみを使用してDynamic Linq



なしで実装できますが、実装は10行に収まらず、コードを理解するのがはるかに難しくなります。



UPD



om2804xyzuvwは、 IQueryableSpec



がレイアウト要件を満たしていないことを正しく指摘しました。 事実、私は||を実行する必要性をほとんど処理する必要がなく、&&は単純なquery.Where(spec1).Where(spec2)



によって実現されますquery.Where(spec1).Where(spec2)



。 コードをきれいにするために少しリファクタリングすることにしました:



  //  IQueryableSpecification  IQueryableFilter public interface IQueryableFilter<T> where T: class { IQueryable<T> Apply(IQueryable<T> query); }
      
      





そのようなライブラリがありますLinqSpecs 。 くしゃみごとに別々のタイプの仕様を作成する必要があるという事実は嫌です。 私にとって十分なExpression<Func<T, bool>>







Pete Montgomeryの Predicate Builderを活用してください。



  /// <summary> /// Creates a predicate that evaluates to true. /// </summary> public static Expression<Func<T, bool>> True<T>() { return param => true; } /// <summary> /// Creates a predicate that evaluates to false. /// </summary> public static Expression<Func<T, bool>> False<T>() { return param => false; } /// <summary> /// Creates a predicate expression from the specified lambda expression. /// </summary> public static Expression<Func<T, bool>> Create<T>(Expression<Func<T, bool>> predicate) { return predicate; } /// <summary> /// Combines the first predicate with the second using the logical "and". /// </summary> public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) { return first.Compose(second, Expression.AndAlso); } /// <summary> /// Combines the first predicate with the second using the logical "or". /// </summary> public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) { return first.Compose(second, Expression.OrElse); }
      
      





Compose



メソッドの実装の詳細は、上記のリンクで説明されています。 次に、&&および||を使用できるように、構文糖を追加します。 IHasId



の一般的な制限IHasId



の仕様を作成することに興味がないためです。 この制限は必要ではありません、私にはちょうど良いようです。



  public static class SpecificationExtenions { public static Specification<T> AsSpec<T>(this Expression<Func<T, bool>> expr) where T : class, IHasId => new Specification<T>(expr); } public sealed class Specification<T> : IQueryableFilter<T> where T: class, IHasId { public Expression<Func<T, bool>> Expression { get; } public Specification(Expression<Func<T, bool>> expression) { Expression = expression; if (expression == null) throw new ArgumentNullException(nameof(expression)); } public static implicit operator Expression<Func<T, bool>>(Specification<T> spec) => spec.Expression; public static bool operator false(Specification<T> spec) { return false; } public static bool operator true(Specification<T> spec) { return false; } public static Specification<T> operator &(Specification<T> spec1, Specification<T> spec2) => new Specification<T>(spec1.Expression.And(spec2.Expression)); public static Specification<T> operator |(Specification<T> spec1, Specification<T> spec2) => new Specification<T>(spec1.Expression.Or(spec2.Expression)); public static Specification<T> operator !(Specification<T> spec) => new Specification<T>(spec.Expression.Not()); public IQueryable<T> Apply(IQueryable<T> query) => query.Where(Expression); public bool IsSatisfiedBy(T obj) => Expression.AsFunc()(obj); }
      
      





私は、関連するエンティティクラスの静的フィールドで「仕様式」を記述するのに慣れています。



  public class Category : HasIdBase<int> { public static readonly Expression<Func<Category, bool>> NiceRating = x => x.Rating > 50; public static readonly Expression<Func<Category, bool>> BadRating = x => x.Rating < 10; public static readonly Expression<Func<Category, bool>> Active= x => x.IsDeleted == false; //... } var niceCategories = db.Query<Category>.Where(Category.NiceRating);
      
      





上記のコードを指定すると、次のように書き換えることができます。



  public class Category : HasIdBase<int> { public static readonly Specification<Category> NiceRating = new Specification(x => x.Rating > 50); //... } var niceCategories = db.Query<Category> .Where((Category.NiceRating || Category.BadRating) && Category.IsActive);
      
      





次にDynamicLinq



ます。 式ツリーで少し作業をする必要があります。

 public enum Compose { And, Or } public static Spec<T> AsSpec<T>(this object obj, Compose compose = Compose.And) where T : class, IHasId { var filterProps = obj.GetType() .GetPublicProperties() .ToArray(); var filterPropNames = filterProps .Select(x => x.Name) .ToArray(); var props = typeof(T) .GetPublicProperties() .Where(x => filterPropNames.Contains(x.Name)) .Select(x => new { Property = x, Value = filterProps.Single(y => y.Name == x.Name).GetValue(obj) }) .Where(x => x.Value != null) .Select(x => { //     e => e.Prop == Val var parameter = Expression.Parameter(typeof (T)); var property = Expression.Property(parameter, x.Property); var body = Expression.Equal(property, Expression.Constant(x.Value)); var delegateType = typeof(Func<T, bool>); return (Expression<Func<T, bool>>) Expression.Lambda(delegateType, body, parameter); }) .ToArray(); if (!props.Any()) return new Spec<T>(x => true); //    ||  && var expr = compose == Compose.And ? props.Aggregate((c, n) => c.And(n)) : props.Aggregate((c, n) => c.Or(n)); return expr.AsSpec(); }
      
      






All Articles