I've been sick the last two days (I got a cold during the hottest part of the year so far, go figure) so I had some time to play around with C# 2.0's generics implementation. What originally got me started was having an old project lying around that had out of date subversion information. Rather than going through each and every sub-directory and deleting .svn directories, I figured I'd write a little one-off app to do it for me. I started out by attempting to implement the specification pattern, which led to various attempts at generic implementations that were never quite right. Fortunately, while searching for information on generics I stumbled across Joe Walnes post on The power of closures in C# 2.0 which details the use of predicate generic delegates.
For those not familiar with the specification pattern, it basically just allows you to filter a list of objects while keeping your condition separate from your loop. As an example, we often see something like this:
1 public IList FindAllMaleCustomers()
2 {
3 IList matches = new ArrayList();
4 foreach(Customer customer in Customers)
5 {
6 if(customer.IsMale)
7 {
8 matches.Add(customer);
9 }
10 }
11 return matches;
12 }
The specification pattern removes that customer.IsMale check out into a specification object, so we can now write something like this instead:
42 public IList FindAllMaleCustomers()
43 {
44 return new CustomerFinder(Customers).FindAll(new MaleCustomerSpecification());
45 }
Therefore, if the definition of a "Male" ever changes (bad example, I know) we can just change our MaleCustomerSpecification object.
Fortunately for me, .NET 2.0 includes a new type of delegate called the predicate generic delegate, which
Represents the method that defines a set of criteria and determines whether the specified object meets those criteria.
Seems like it's exactly what we need. So rather than writing a specific specification object, we can just write a generic predicate like so:
1 public class Spec
2 {
3 public static Predicate<DirectoryInfo> SubversionDirectory
4 {
5 get
6 {
7 return delegate(DirectoryInfo dirInfo)
8 {
9 return dirInfo.Name == ".svn";
10 };
11 }
12 }
13 }
Which we then use like this:
1 new DirectoryFinder(directories).FindAll(Spec.SubversionDirectory);
Our DirectoryFinder object is a little more complex than a standard FindAll on a method like System.Collections.Generic.List would be, because we recurse all the way through the sub-directories of a given directory:
1 public class DirectoryFinder
2 {
3 private IList<DirectoryInfo> _directories;
4
5 public DirectoryFinder(IList<DirectoryInfo> directories)
6 {
7 _directories = directories;
8 }
9
10 public DirectoryFinder(DirectoryInfo directory)
11 {
12 _directories = directory.GetDirectories();
13 }
14
15 public IList<DirectoryInfo> FindAll(Predicate<DirectoryInfo> predicate)
16 {
17 return FindAll(predicate, _directories);
18 }
19
20 private IList<DirectoryInfo> FindAll(Predicate<DirectoryInfo> predicate, IList<DirectoryInfo> directories)
21 {
22 List<DirectoryInfo> matches = new List<DirectoryInfo>();
23 foreach (DirectoryInfo info in directories)
24 {
25 if (predicate(info))
26 {
27 matches.Add(info);
28 }
29 else
30 {
31 matches.AddRange(FindAll(predicate, info.GetDirectories()));
32 }
33 }
34 return matches;
35 }
36 }
I'm almost certain there's other ways of doing this, some are probably better as well. For another use of predicates, check out Jean-Paul Boodhoo's post on validation in the domain layer, where he uses them to flesh out business rules.