The Specification Pattern (理解规格模式)

From: http://devlicio.us/blogs/jeff_perrin/archive/2006/12/13/the-specification-pattern.aspx

Hot on the heels of my devastatingly fantastic post on an implementation of the Snapshot Pattern, I give you my next piece du resistance. In this little post, I'd like to delve into the Specification Pattern.

So what the heck is it? Matt Berther provided a pretty good introduction where he states:

It's primary use is to select a subset of objects based on some criteria...

That pretty much sums it up. What we want to do is extract out a specification for a subset of objects we might be interested in. We do this by creating specification objects.

You'll see something like this in a lot of applications:

view plaincopy to clipboardprint?

  1. public List GetHighPricedSaleProducts(){  
  2.   List list = new ArrayList();  
  3. for(Product p in Products){  
  4. if(p.IsOnSale && p.Price > 100.0){  
  5.       list.Add(p);  
  6.     }  
  7.   }  
  8. return list;  

public List GetHighPricedSaleProducts(){ List list = new ArrayList(); for(Product p in Products){ if(p.IsOnSale && p.Price > 100.0){ list.Add(p); } } return list; }

This is fine in small doses, but your definition of a highly priced sale product might change over time, and we want to avoid having our logic for what IsOnSale and what a highly priced object actually is sprinkled throughout our code. One way to avoid this is to extract our logic into a Specification object like so:

view plaincopy to clipboardprint?

  1. public class ProductOnSaleSpecification : Specification<Product>  
  2. {  
  3. public override bool IsSatisfiedBy(Product product)  
  4.   {  
  5. return product.IsOnSale;  
  6.   }  
  7. }    

public class ProductOnSaleSpecification : Specification<Product> { public override bool IsSatisfiedBy(Product product) { return product.IsOnSale; } }

Now the first loop I wrote can be written like so:

view plaincopy to clipboardprint?

  1. public List GetHighPricedSaleProducts(){  
  2.   List list = new ArrayList();  
  3. for(Product p in Products){  
  4. if(new ProductOnSaleSpecification().isSatisfiedBy(p)   
  5.       && p.Price > 100.0){  
  6.       list.Add(p);  
  7.     }  
  8.   }  
  9. return list;  

public List GetHighPricedSaleProducts(){ List list = new ArrayList(); for(Product p in Products){ if(new ProductOnSaleSpecification().isSatisfiedBy(p) && p.Price > 100.0){ list.Add(p); } } return list; }

This is a slight improvement... There's actually more code to write, but now we can separately unit test each specification we create, without worrying about the loop:

view plaincopy to clipboardprint?

  1. [Test]  
  2. public void TestIsSatisfiedBy_ProductOnSaleSpecification()  
  3. {  
  4. bool isOnSale = true;  
  5.   Product saleProduct = new Product(isOnSale);  
  6.   Product notOnSaleProduct = new Product(!isOnSale);  
  7.   ProductOnSaleSpecification spec =   
  8. new ProductOnSaleSpecification();  
  9.   Assert.IsTrue(spec.IsSatisfiedBy(saleProduct));  
  10.   Assert.IsFalse(spec.IsSatisfiedBy(notOnSaleProduct));  

[Test] public void TestIsSatisfiedBy_ProductOnSaleSpecification() { bool isOnSale = true; Product saleProduct = new Product(isOnSale); Product notOnSaleProduct = new Product(!isOnSale); ProductOnSaleSpecification spec = new ProductOnSaleSpecification(); Assert.IsTrue(spec.IsSatisfiedBy(saleProduct)); Assert.IsFalse(spec.IsSatisfiedBy(notOnSaleProduct)); }

Ok, so that's interesting, but we haven't even gone halfway, here. Why don't we refine that loop I wrote to use the new Generic collections in .NET 2.0:

view plaincopy to clipboardprint?

  1. public List<Product> GetSaleProducts(){  
  2. return Products.FindAll(  
  3. new ProductOnSaleSpecification().IsSatisfiedBy);  

public List<Product> GetSaleProducts(){ return Products.FindAll( new ProductOnSaleSpecification().IsSatisfiedBy); }

Wow, now there's some serious savings on lines of code. "But you're missing the bit about the high priced products from the first example!?!" I hear you saying. Fear not, let's extract that into another specification like so:

view plaincopy to clipboardprint?

  1. public class ProductPriceGreaterThanSpecification : Specification<Product>  
  2. {  
  3. private readonly double _price;  
  4. public ProductPriceGreaterThanSpecification(double price)  
  5.     {  
  6.         _price = price;  
  7.     }  
  8. public override bool IsSatisfiedBy(Product product)  
  9.     {  
  10. return product.Price > _price;  
  11.     }  

public class ProductPriceGreaterThanSpecification : Specification<Product> { private readonly double _price; public ProductPriceGreaterThanSpecification(double price) { _price = price; } public override bool IsSatisfiedBy(Product product) { return product.Price > _price; } }

We're still left with one problem, though. How do we tell the generic list of all products that we want the products that are both on sale and over a certain price? Let's try extracting our functionality into a Specification superclass first. This is what our ProductOnSaleSpecification and ProductPriceGreaterThanSpecification will inherit from. Once that's over with, we can create a CompositeSpecification, which is abstract, and allows us to pass in the left and right sides of a specification "equation." We can then implement yet another subclass (this time of CompositeSpecification) that we'll call AndSpecification. Here it is:

view plaincopy to clipboardprint?

  1. public class AndSpecification<T> : CompositeSpecification<T>  
  2. {  
  3. public AndSpecification(Specification<T> leftSide,   
  4.     Specification<T> rightSide)  
  5.       : base(leftSide, rightSide)  
  6.   {}  
  7. public override bool IsSatisfiedBy(T obj)  
  8.   {  
  9. return _leftSide.IsSatisfiedBy(obj)  
  10.       && _rightSide.IsSatisfiedBy(obj);  
  11.   }  

public class AndSpecification<T> : CompositeSpecification<T> { public AndSpecification(Specification<T> leftSide, Specification<T> rightSide) : base(leftSide, rightSide) {} public override bool IsSatisfiedBy(T obj) { return _leftSide.IsSatisfiedBy(obj) && _rightSide.IsSatisfiedBy(obj); } }

Now our original loop that looks for highly priced products that are on sale looks like this:

view plaincopy to clipboardprint?

  1. public List<Product> GetSaleProducts(){  
  2.   AndSpecification spec = new AndSpecification(  
  3. new ProductOnSaleSpecification(),   
  4. new ProductPriceGreaterThanSpecification());  
  5. return Products.FindAll(spec.IsSatisfiedBy);  

public List<Product> GetSaleProducts(){ AndSpecification spec = new AndSpecification( new ProductOnSaleSpecification(), new ProductPriceGreaterThanSpecification()); return Products.FindAll(spec.IsSatisfiedBy); }

We're getting there, but we're still not done. The code we just wrote is soooo .NET 1.1. Let's get fluent with our interfaces and add some sweet sugar to our Specification base class...

view plaincopy to clipboardprint?

  1. public abstract class Specification<T>  
  2. {  
  3. public abstract bool IsSatisfiedBy(T obj);  
  4. public AndSpecification<T> And(Specification<T> specification)  
  5.   {  
  6. return new AndSpecification<T>(this, specification);  
  7.   }  
  8. public OrSpecification<T> Or(Specification<T> specification)  
  9.   {  
  10. return new OrSpecification<T>(this, specification);  
  11.   }  
  12. public NotSpecification<T> Not(Specification<T> specification)  
  13.   {  
  14. return new NotSpecification<T>(this, specification);  
  15.   }  

public abstract class Specification<T> { public abstract bool IsSatisfiedBy(T obj); public AndSpecification<T> And(Specification<T> specification) { return new AndSpecification<T>(this, specification); } public OrSpecification<T> Or(Specification<T> specification) { return new OrSpecification<T>(this, specification); } public NotSpecification<T> Not(Specification<T> specification) { return new NotSpecification<T>(this, specification); } }

I've just added some convenience methods to Specification that will let us chain together any specifications we create. Therefore, our original loop ascends to a new level of sexiness...

view plaincopy to clipboardprint?

  1. public List<Product> GetSaleProducts(){  
  2. return Products.FindAll(  
  3. new ProductOnSaleSpecification().And(  
  4. new ProductPriceGreaterThanSpecification(100)).IsSatisfiedBy);  

public List<Product> GetSaleProducts(){ return Products.FindAll( new ProductOnSaleSpecification().And( new ProductPriceGreaterThanSpecification(100)).IsSatisfiedBy); }

For the "slow" ones in the class I've put up a pretty picture for you to look at, while the rest of the class downloads the code, complete with unit tests.

 

综上: 利用 lamda 表达式的语法糖,制定出一些规则,然后统一进行调用。 业务驱动中核心部分之一,经常也是我们忽略,或用其它一些类来代替了。

posted @ 2013-02-05 09:28  张保维  阅读(293)  评论(0编辑  收藏  举报