Reputation:
I have a database table with a list of products (clothing). The products belong to categories and are from different stores.
Sample categories: tops, bottoms, shoes
Sample stores: gap.com, macys.com, target.com
My customers can request to filter products in the following ways:
Right now I have ONE method in my "Products" class that returns the products depending on the type of filter requested by the user. I use a FilterBy enum to determine which products need to be returned.
For example, if the user wants to view all products in the "tops" category I call this function:
Products.GetProducts(FilterBy.Category, "tops", "");
I have the last parameter empty because it's the string that contains the "store" to filter by but in this case there is no store. However, if the user wants to filter by category AND store I'd call the method this way:
Product.GetProducts(FilterBy.CategoryAndStore, "tops", "macys.com");
My question is, what's a better way to do this? I just learned about the strategy design pattern. Could I use that to do this in a better (easier to extend and easier to maintain) way?
The reason I'm asking this question is because I figure this must be a pretty common problem that people are repeatedly solving (filtering products in various ways)
Upvotes: 13
Views: 8379
Reputation: 158171
Can't you just add Where stuff as you go here?
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
or something like that...
Upvotes: 0
Reputation: 23224
According to Eric Evan's "Domain Drive Design" you need the specification pattern. Something like this
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 } + "'";
}
}
Your repository can now be called with specifications
var specifications = new List<ISpecification<Product>>();
specifications.Add(
new ProductCategoryNameSpecification("Tops"));
specifications.Add(
new ProductColorSpecification("Blue"));
var products = ProductRepository.GetBySpecifications(specifications);
You could also create a generic CompositeSpecification class which would contain sub specifications and an indicator as to which logical operator to apply to them AND/OR
I'd be more inclined to combine LINQ expressions though.
Update - Example of LINQ at runtime
var product = Expression.Parameter(typeof(Product), "product");
var categoryNameExpression = Expression.Equal(
Expression.Property(product, "CategoryName"),
Expression.Constant("Tops"));
You can add an "and" like so
var colorExpression = Expression.Equal(
Expression.Property(product, "Color"),
Expression.Constant("Red"));
var andExpression = Expression.And(categoryNameExpression, colorExpression);
Finally you can convert this expression into a predicate and then execute it...
var predicate =
(Func<Product, bool>)Expression.Lambda(andExpression, product).Compile();
var query = Enumerable.Where(YourDataContext.Products, predicate);
foreach(Product currentProduct in query)
meh(currentProduct);
Probably wont compile because I have typed it directly into the browser, but I believe it is generally correct.
Another update :-)
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();
In this case you would be evaluating in memory because the source is a List, but if your source was a data context that supported Linq2SQL for example I think this would evaluate using SQL.
You could still use the Specification pattern in order to make your concepts explicit.
public class Specification<T>
{
IEnumerable<T> AppendToQuery(IEnumerable<T> query);
}
The main difference between the two approaches is that the latter builds a known query based on explicit properties, whereas the first one could be used to build a query of any structure (such as building a query entirely from XML for example.)
This should be enough to get you started :-)
Upvotes: 16
Reputation: 5166
I would go with something like a strategy for the filters themselves and write CategoryFilter and StoreFilter classes. Then I would use a composite or decorator to combine the filters.
Upvotes: 0
Reputation: 56123
I think I'd make a Category class and a Store class, instead of just strings:
class Category
{
public Category(string s)
{
...
}
...
}
And then maybe:
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
)
{
...
}
The Category
and Store
classes might be related (they might both be subclasses of a Filter
class).
Upvotes: 1
Reputation: 33474
I am answering this based on my little knowledge of patterns.
Decorator pattern might help here (considering you can add a filter & get results. Apply new filters on it & get new results)
Upvotes: 0
Reputation: 1063318
The strategy pattern doesn't necessarily knit well with the common interface-based repository approach. Personally, I'd probably go one of two ways here:
One search method that supports combinations of options:
IList<Product> GetProducts(string category, string store, ...);
(then selectively apply the combinations of filters (i.e. null
means "any") - either when building a command, or pass down to a SPROC that does something similar.
With LINQ, maybe a predicate expression?
IList<Product> GetProducts(Expression<Func<Product,bool>> predicate);
Of course, with LINQ you could also use composition by the caller, but that is harder to write a closed / fully-tested repository for:
`IQueryable<Product> Products {get;}`
(and have the caller use .Where(x=>x.Category == "foo")) - I'm not so sure about this last one long-term...
Upvotes: 2