用哪种设计模式来过滤查询?C#

13

我有一个包含服装产品列表的数据库表。这些产品属于不同的类别并来自不同的商店。

示例类别:上衣、裤子、鞋子

示例商店:gap.com,macys.com,target.com

我的客户可以按以下方式请求筛选产品:

  • 所有产品(无过滤器)
  • 按类别
  • 按商店
  • 按类别和商店

目前,我的“产品”类中只有一个方法,根据用户请求的筛选器返回产品。我使用FilterBy枚举来确定需要返回哪些产品。

例如,如果用户想查看“上衣”类别中的所有产品,我调用此函数:

Products.GetProducts(FilterBy.Category, "tops", ""); 

我的最后一个参数是空的,因为它是包含要筛选的“store”的字符串,但在这种情况下没有店铺。然而,如果用户想按类别和商店筛选,我会这样调用方法:

Product.GetProducts(FilterBy.CategoryAndStore, "tops", "macys.com");

我的问题是,有没有更好的方法来处理这个问题?我刚学习了策略设计模式。我能不能用它来以更好的方式(更易于扩展和维护)完成这个任务?

我问这个问题的原因是,我觉得这肯定是一个人们一遍又一遍解决的相当常见的问题(以各种方式过滤产品)。

6个回答

16
根据Eric Evan的“领域驱动设计”,您需要使用规约模式。像这样:
public interface ISpecification<T>
{
  bool Matches(T instance);
  string GetSql();
}

public class ProductCategoryNameSpecification : ISpecification<Product>
{
  readonly string CategoryName;
  public ProductCategoryNameSpecification(string categoryName)
  {
    CategoryName = categoryName;
  }

  public bool Matches(Product instance)
  {
    return instance.Category.Name == CategoryName;
  }

  public string GetSql()
  {
    return "CategoryName like '" + { escaped CategoryName } + "'";
  }
}

现在您可以使用规格来调用您的存储库

var specifications = new List<ISpecification<Product>>();
specifications.Add(
 new ProductCategoryNameSpecification("Tops"));
specifications.Add(
 new ProductColorSpecification("Blue"));

var products = ProductRepository.GetBySpecifications(specifications);

您还可以创建一个通用的CompositeSpecification类,其中包含子规范和指示要应用于它们的逻辑运算符AND/OR。

我更倾向于组合LINQ表达式。

更新 - 运行时LINQ示例

var product = Expression.Parameter(typeof(Product), "product");
var categoryNameExpression = Expression.Equal(
  Expression.Property(product, "CategoryName"),
  Expression.Constant("Tops"));

您可以这样添加“and”
var colorExpression = Expression.Equal(
  Expression.Property(product, "Color"),
  Expression.Constant("Red"));
var andExpression = Expression.And(categoryNameExpression, colorExpression);

最后,您可以将此表达式转换为谓词,然后执行它...
var predicate = 
  (Func<Product, bool>)Expression.Lambda(andExpression, product).Compile();
var query = Enumerable.Where(YourDataContext.Products, predicate);

foreach(Product currentProduct in query)
  meh(currentProduct);

可能无法编译,因为我直接在浏览器中键入了它,但我相信它基本上是正确的。

另一个更新 :-)

List<Product> products = new List<Product>();
products.Add(new Product { CategoryName = "Tops", Color = "Red" });
products.Add(new Product { CategoryName = "Tops", Color = "Gree" });
products.Add(new Product { CategoryName = "Trousers", Color = "Red" });
var query = (IEnumerable<Product>)products;
query = query.Where(p => p.CategoryName == "Tops");
query = query.Where(p => p.Color == "Red");
foreach (Product p in query)
    Console.WriteLine(p.CategoryName + " / " + p.Color);
Console.ReadLine();

在这种情况下,你将会在内存中进行评估,因为源是一个 List,但如果你的源是支持 Linq2SQL 的数据上下文,比如说,我认为这个将会使用 SQL 进行评估。
你仍然可以使用规约模式来使你的概念更加明确。
public class Specification<T>
{
  IEnumerable<T> AppendToQuery(IEnumerable<T> query);
}

两种方法的主要区别在于,后者基于显式属性构建已知查询,而前者可以用于构建任何结构的查询(例如完全从XML构建查询)。
这应该足以让你开始了 :-)

为您添加了一个示例,请查看并看看您的情况如何。 - Peter Morris

2
策略模式并不一定与常见的基于接口的存储库方法相匹配。个人而言,在这里可能会选择以下两种方式之一:
1. 一个支持选项组合的搜索方法: ``` IList GetProducts(string category, string store, ...); ``` 然后有选择地应用过滤器的组合(即 `null` 表示“任何”) - 在构建命令时或传递给执行类似操作的 SPROC。
2. 使用 LINQ,也许是一个谓词表达式? ``` IList GetProducts(Expression> predicate); ``` 当然,使用LINQ还可以通过调用者进行组合,但这更难编写封闭/完全测试的存储库。
 `IQueryable<Product> Products {get;}`

(并让调用者使用.Where(x => x.Category ==“foo”))-我对这个长期的不确定......

谓词形式正是我要发布的。它非常灵活,而且使用lambda表达式时与任何其他方式一样简洁。 - Jon Skeet
确实,通过成为Expression<...>,它可以编译以与基于对象的存储库一起使用。缺点是仍有LOLA风险,涉及不受支持的查询操作... - Marc Gravell
理论上喜欢IQueryable<T>(返回)选项,但它使得难以将DAL与UI隔离开来(即保证数据访问何时开始和结束)- 但是它仍然是一种适用于某些场景的可行选项。 - Marc Gravell
你为什么要选择IList<T>作为返回类型而不是IEnumerable<T>呢? - Peter Morris
举个例子,我只是希望在将数据传回之前能清楚地执行完存储库。不过,预获取和流式处理两种方法都是有效的。 - Marc Gravell

1
我认为我会创建一个类别类和一个存储类,而不仅仅是字符串:
class Category
{
  public Category(string s)
  {
    ...
  }
  ...
}

然后可能是:

Product.GetProducts(
  Category category, //if this is null then don't filter on category
  Store store //if this is null then don't filter on store
  )
{
  ...
}

CategoryStore类可能相关联(它们可能都是Filter类的子类)。


0
我基于我对模式的一点了解来回答这个问题。
装饰器模式可能有助于解决这个问题(考虑您可以添加一个过滤器并获取结果,在其上应用新的过滤器并获取新的结果)。

0

我会采用策略模式来构建过滤器自身,编写CategoryFilterStoreFilter类。然后我会使用组合或装饰器将这些过滤器结合起来。


0

你不能在这里随时添加Where条件吗?

var products = datacontext.Products;

if(!String.IsNullOrEmpty(type))
  products = products.Where(p => p.Type == type);

if(!String.IsNullOrEmpty(store))
  products = products.Where(p => p.Store == store);

foreach(var p in products)
  // Do whatever

或者类似这样的东西...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接