Replace Implicit Language with Interpreter

Prev don't be afraid of buying books Next

Replace Implicit Language with Interpreter

Numerous methods on a class combine elements of an implicit language.



Define classes for elements of the implicit language so that instances may be combined to form interpretable expressions.





Motivation

An Interpreter [DP] is useful for interpreting simple languages. A simple language is one that has a grammar that may be modeled using a small number of classes. Sentences and expressions in simple languages are formed by combining instances of the grammar's classes, typically using a Composite [DP] structure.

Programmers divide into two camps with respect to the Interpreter pattern: those who are comfortable implementing it and those who aren't. However, whether or not you're comfortable with terms like parse trees and abstract syntax trees, terminal and nonterminal expressions, implementing an Interpreter is only slightly more complicated than implementing a Composite. The trick is knowing when you need an Interpreter.

You don't need an Interpreter for complex languages or for really simple ones. For complex languages, it's usually best to use a tool (such as JavaCC) that supports parsing, grammar definition, and interpretation. For example, on one project, my colleagues and I used a parser generator to produce a grammar that had more than 20 classes—too many to comfortably program by hand using the Interpreter pattern. On another project, our language's grammar was so simple and uniform that we didn't use any classes to interpret each expression in the language.

If a language's grammar requires fewer than a dozen classes to implement, it may be useful to model using the Interpreter pattern. Search expressions for objects or database values often have such grammars. Typical searches require the use of words like "and," "not," and "or" (called nonterminal expressions), as well as values like "$10.00," "small," and "blue" (called terminal expressions). For example:

  • Find products below $10.00.

  • Find products below $10.00 and not white.

  • Find products that are blue, small, and below $20.00.

Such search expressions are often programmed into systems without using an explicit language. Consider this class:

 ProductFinder...    public List byColor(Color colorOfProductToFind)...    public List byPrice(float priceLimit)...    public List bySize(int sizeToFind)...    public List belowPrice(float price) ...    public List belowPriceAvoidingAColor(float price)...    public List byColorAndBelowPrice(Color color, float price)...    public List byColorSizeAndBelowPrice(Color color, int size, float price)... 

Programmed in this way, a product-finding language is implicit: present, but unexpressed. Two problems result from this approach. First, you must create a new method for each new combination of product criteria. Second, the product-finding methods tend to duplicate a lot of product-finding code. An Interpreter solution (shown in the Example section) is better because it can support a large variety of product queries with a little over a half-dozen small classes and no duplication of code.

Refactoring to an Interpreter involves the start-up cost of defining classes for a grammar and altering client code to compose instances of the classes to represent language expressions. Is that price worth it? It is if the alternative is lots of duplicated code to handle a combinatorial explosion of implicit language expressions, such as the many finder methods in the ProductFinder class just shown.

Two patterns that make heavy use of Interpreter are Specification [Evans] and Query Object [Fowler, PEAA]. Both models search expressions using simple grammars and compositions of objects. These patterns provide a useful way to separate a search expression from its representation. For example, a Query Object models a query in a generic way, which allows you to convert it into a SQL representation (or some other representation) when you want to perform an actual database query.

Interpreters are often used within systems to allow for the runtime configuration of behavior. For example, a system may accept a user's query preferences through a user interface and then dynamically produce an interpretable object structure that represents the query. In this way, Interpreters can provide a level of power and flexibility that isn't possible when all behavior in a system is static and can't be configured dynamically.

Benefits and Liabilities

+

Supports combinations of language elements better than an implicit language does.

+

Requires no new code to support new combinations of language elements.

+

Allows for runtime configuration of behavior.

Has a start-up cost for defining a grammar and changing client code to use it.

Requires too much programming when your language is complex.

Complicates a design when a language is simple.







Mechanics

These mechanics are heavily weighted towards the use of Interpreter in the context of the Specification and Query Object patterns because most of the Interpreter implementations I've written or encountered have been implementations of those two patterns. In this context, an implicit language is modeled using numerous object selection methods, each of which iterates across a collection to select a specific set of objects.

1. Find an object selection method that relies on a single criterion argument (e.g., double targetPrice) to find a set of objects. Create a concrete specification class for the criterion argument, which accepts the argument's value in a constructor and provides a getter method for it. Within the object selection method, declare and instantiate a variable of type concrete specification and update the code so access to the criterion is obtained via the concrete specification's getter method.

Name your concrete specification by what it does (e.g., ColorSpec helps find products by a given color).

If your object selection method relies on multiple criteria for its object selection, apply this step and step 2 for each piece of the criterion. In step 4, you'll deal with composing concrete specifications into composite specifications.

  • Compile and test that object selection still works correctly.

2. Apply Extract Method [F] on the conditional statement in the object selection method to produce a method called isSatisfiedBy(), which should have a Boolean result. Now apply Move Method [F] to move this method to the concrete specification.

Create a specification superclass (if you haven't already created one) by applying Extract Superclass [F] on the concrete specification. Make this superclass abstract and have it declare a single abstract method for isSatisfiedBy(…).

  • Compile and test that object selection still works correctly.

3. Repeat steps 1 and 2 for similar object selection methods, including methods that rely on the criteria for object selection.

4. If you have an object selection method that relies on multiple concrete specifications (i.e., the method now instantiates more than one concrete specification for use in its object selection logic), apply a modified version of step 1 by creating a composite specification (a class composed of the concrete specifications instantiated inside the object selection method). You may pass the concrete specifications to the composite specification via its constructor or, if there are many concrete specifications, supply an add(…) method on the composite specification.

Then apply step 2 on the object selection method's conditional statement to move the logic to the composite specification's isSatisfiedBy(…) method. Make the composite specification extend from the specification superclass.

5. Each object selection method now works with one specification object (i.e., one concrete specification or one composite specification). In addition, the object selection methods are identical except for specification creation code. Remove duplicated code in the object selection methods by applying Extract Method [F] on the identical code from any object selection method. Name the extracted method something like selectBy(…) and have it accept one argument of type specification interface and return a collection of objects (e.g., public List selectBy(Spec spec)).

  • Compile and test.

    Adjust all object selection methods to call the selectBy(…) method.

  • Compile and test.

6. Apply Inline Method [F] on every object selection method.

  • Compile and test.

Example

The code sketch and the Motivation section already gave you an introduction to this example, which is inspired from an inventory management system. That system's Finder classes (AccountFinder, InvoiceFinder, ProductFinder, and so forth) eventually came to suffer from a Combinatorial Explosion smell (45), which necessitated the refactoring to Specification. It's worth noting that this does not reveal a problem with Finder classes: the point is that a time may come when a refactoring to Specification is justified.

I begin by studying the tests and code for a ProductFinder that is in need of this refactoring. I'll start with the test code. Before any test can run, I need a ProductRepository object that's filled with various Product objects and a ProductFinder object that knows about the ProductRepository:

 public class ProductFinderTests extends TestCase...    private ProductFinder finder;    private Product fireTruck =       new Product("f1234", "Fire Truck",          Color.red, 8.95f, ProductSize.MEDIUM);    private Product barbieClassic =       new Product("b7654", "Barbie Classic",          Color.yellow, 15.95f, ProductSize.SMALL);    private Product frisbee =       new Product("f4321", "Frisbee",          Color.pink, 9.99f, ProductSize.LARGE);    private Product baseball =       new Product("b2343", "Baseball",          Color.white, 8.95f, ProductSize.NOT_APPLICABLE);    private Product toyConvertible =       new Product("p1112", "Toy Porsche Convertible",          Color.red, 230.00f, ProductSize.NOT_APPLICABLE);    protected void setUp() {       finder = new ProductFinder(createProductRepository());    }    private ProductRepository createProductRepository() {       ProductRepository repository = new ProductRepository();       repository.add(fireTruck);       repository.add(barbieClassic);       repository.add(frisbee);       repository.add(baseball);       repository.add(toyConvertible);       return repository;    } 

The "toy" products above work fine for test code. Of course, the production code uses real product objects, which are obtained using object-relational mapping logic.

Now I look at a few simple tests and the implementation code that satisfies them. The testFindByColor() method checks whether the ProductFinder.byColor(…) method correctly finds red toys, while testFindByPrice() checks whether ProductFinder.byPrice(…) correctly finds toys at a given price:

 public class ProductFinderTests extends TestCase...    public void testFindByColor() {       List foundProducts = finder.byColor(Color.red);       assertEquals("found 2 red products", 2, foundProducts.size());       assertTrue("found fireTruck", foundProducts.contains(fireTruck));       assertTrue(          "found Toy Porsche Convertible",          foundProducts.contains(toyConvertible));    }    public void testFindByPrice() {       List foundProducts = finder.byPrice(8.95f);       assertEquals("found products that cost $8.95", 2, foundProducts.size());       for (Iterator i = foundProducts.iterator(); i.hasNext();) {          Product p = (Product) i.next();          assertTrue(p.getPrice() == 8.95f);       }    } 

Here's the implementation code that satisfies these tests:

 public class ProductFinder...    private ProductRepository repository;    public ProductFinder(ProductRepository repository) {       this.repository = repository;    }    public List byColor(Color colorOfProductToFind) {       List foundProducts = new ArrayList();       Iterator products = repository.iterator();       while (products.hasNext()) {          Product product = (Product) products.next();          if (product.getColor().equals(colorOfProductToFind))             foundProducts.add(product);       }       return foundProducts;    }    public List byPrice(float priceLimit) {       List foundProducts = new ArrayList();       Iterator products = repository.iterator();       while (products.hasNext()) {          Product product = (Product) products.next();          if (product.getPrice() == priceLimit)             foundProducts.add(product);       }       return foundProducts;    } 

There's plenty of duplicate code in these two methods. I'll be getting rid of that duplication during this refactoring. Meanwhile, I explore some more tests and code that are involved in the Combinatorial Explosion problem. Below, one test is concerned with finding Product instances by color, size, and below a certain price, while the other test is concerned with finding Product instances by color and above a certain price:

 public class ProductFinderTests extends TestCase...    public void testFindByColorSizeAndBelowPrice() {       List foundProducts =          finder.byColorSizeAndBelowPrice(Color.red, ProductSize.SMALL, 10.00f);       assertEquals(          "found no small red products below $10.00",          0,          foundProducts.size());       foundProducts =          finder.byColorSizeAndBelowPrice(Color.red, ProductSize.MEDIUM, 10.00f);       assertEquals(          "found firetruck when looking for cheap medium red toys",          fireTruck,          foundProducts.get(0));    }    public void testFindBelowPriceAvoidingAColor() {       List foundProducts =          finder.belowPriceAvoidingAColor(9.00f, Color.white);       assertEquals(          "found 1 non-white product < $9.00",          1,          foundProducts.size());       assertTrue("found fireTruck", foundProducts.contains(fireTruck));       foundProducts = finder.belowPriceAvoidingAColor(9.00f, Color.red);       assertEquals(          "found 1 non-red product < $9.00",          1,          foundProducts.size());       assertTrue("found baseball", foundProducts.contains(baseball));    } 

Here's how the implementation code looks for these tests:

 public class ProductFinder...    public List byColorSizeAndBelowPrice(Color color, int size, float price) {       List foundProducts = new ArrayList();       Iterator products = repository.iterator();       while (products.hasNext()) {          Product product = (Product) products.next();          if (product.getColor() == color             && product.getSize() == size             && product.getPrice() < price)             foundProducts.add(product);       }       return foundProducts;    }    public List belowPriceAvoidingAColor(float price, Color color) {       List foundProducts = new ArrayList();       Iterator products = repository.iterator();       while (products.hasNext()) {          Product product = (Product) products.next();          if (product.getPrice() < price && product.getColor() != color)             foundProducts.add(product);       }       return foundProducts;    } 

Again, I see plenty of duplicate code because each of the specific finder methods iterates over the same repository and selects just those Product instances that match the specified criteria. I'm now ready to begin the refactoring.

1. The first step is to find an object selection method that relies on a criterion argument for its selection logic. The ProductFinder method byColor(Color colorOfProductToFind) meets this requirement:

 public class ProductFinder...    public List byColor(Color colorOfProductToFind) {       List foundProducts = new ArrayList();       Iterator products = repository.iterator();       while (products.hasNext()) {          Product product = (Product) products.next();          if (product.getColor().equals(colorOfProductToFind))             foundProducts.add(product);       }       return foundProducts;    } 

I create a concrete specification class for the criterion argument, Color colorOfProductToFind. I call this class ColorSpec. It needs to hold onto a Color field and provide a getter method for it:

  public class ColorSpec {     private Color colorOfProductToFind;     public ColorSpec(Color colorOfProductToFind) {        this.colorOfProductToFind = colorOfProductToFind;     }     public Color getColorOfProductToFind() {        return colorOfProductToFind;     }  } 

Now I can add a variable of type ColorSpec to the byColor(…) method and replace the reference to the parameter, colorOfProductToFind, with a reference to the specification's getter method:

 public List byColor(Color colorOfProductToFind) {     ColorSpec spec = new ColorSpec(colorOfProductToFind);    List foundProducts = new ArrayList();    Iterator products = repository.iterator();    while (products.hasNext()) {       Product product = (Product) products.next();       if (product.getColor().equals( spec.getColorOfProductToFind()))          foundProducts.add(product);    }    return foundProducts; } 

After these changes, I compile and run my tests. Here's one such test:

 public void testFindByColor() {    List foundProducts = finder.byColor(Color.red);    assertEquals("found 2 red products", 2, foundProducts.size());    assertTrue("found fireTruck", foundProducts.contains(fireTruck));    assertTrue("found Toy Porsche Convertible", foundProducts.contains(toyConvertible)); } 

2. Now I'll apply Extract Method [F] to extract the conditional statement in the while loop to an isSatisfiedBy(…) method:

 public List byColor(Color colorOfProductToFind) {    ColorSpec spec = new ColorSpec(colorOfProductToFind);    List foundProducts = new ArrayList();    Iterator products = repository.iterator();    while (products.hasNext()) {       Product product = (Product) products.next();       if ( isSatisfiedBy(spec, product))          foundProducts.add(product);    }    return foundProducts; }  private boolean isSatisfiedBy(ColorSpec spec, Product product) {     return product.getColor().equals(spec.getColorOfProductToFind());  } 

The isSatisfiedBy(…) method can now be moved to ColorSpec by applying Move Method [F]:

 public class ProductFinder...    public List byColor(Color colorOfProductToFind) {       ColorSpec spec = new ColorSpec(colorOfProductToFind);       List foundProducts = new ArrayList();       Iterator products = repository.iterator();       while (products.hasNext()) {          Product product = (Product) products.next();          if ( spec.isSatisfiedBy(product))             foundProducts.add(product);       }       return foundProducts;    } public class ColorSpec...     boolean isSatisfiedBy(Product product) {        return product.getColor().equals(getColorOfProductToFind());     } 

Finally, I create a specification superclass by applying Extract Superclass [F] on ColorSpec:

  public abstract class Spec {     public abstract boolean isSatisfiedBy(Product product);  } 

I make ColorSpec extend this class:

 public class ColorSpec  extends Spec... 

I compile and test to see that Product instances can still be selected by a given color correctly. Everything works fine.

3. Now I repeat steps 1 and 2 for similar object selection methods. This includes methods that work with criteria (i.e., multiple pieces of criterion). For example, the byColorAndBelowPrice(…) method accepts two arguments that act as criteria for selecting Product instances out of the repository:

 public List byColorAndBelowPrice(Color color, float price) {    List foundProducts = new ArrayList();    Iterator products = repository.iterator();    while (products.hasNext()) {       Product product = (Product)products.next();       if (product.getPrice() < price && product.getColor() == color)          foundProducts.add(product);    }    return foundProducts; } 

By implementing steps 1 and 2, I end up with the BelowPriceSpec class:

  public class BelowPriceSpec extends Spec {     private float priceThreshold;     public BelowPriceSpec(float priceThreshold) {        this.priceThreshold = priceThreshold;     }     public boolean isSatisfiedBy(Product product) {        return product.getPrice() < getPriceThreshold();     }     public float getPriceThreshold() {        return priceThreshold;     }  } 

Now I can create a new version of byColorAndBelowPrice(…) that works with the two concrete specifications:

 public List byColorAndBelowPrice(Color color, float price) {     ColorSpec colorSpec = new ColorSpec(color);     BelowPriceSpec priceSpec = new BelowPriceSpec(price);    List foundProducts = new ArrayList();    Iterator products = repository.iterator();    while (products.hasNext()) {       Product product = (Product)products.next();       if ( colorSpec.isSatisfiedBy(product) &&           priceSpec.isSatisfiedBy(product))          foundProducts.add(product);    }    return foundProducts; } 

4. The byColorAndBelowPrice(…) method uses criteria (color and price) in its object selection logic. I'd like to make this method, and others like it, work with a composite specification rather than with individual specifications. To do that, I'll implement a modified version of step 1 and an unmodified version of step 2. Here's how byColorAndBelowPrice(…) looks after step 1:

 public List byColorAndBelowPrice(Color color, float price) {    ColorSpec colorSpec = new ColorSpec(color);    BelowPriceSpec priceSpec = new BelowPriceSpec(price);     AndSpec spec = new AndSpec(colorSpec, priceSpec);    List foundProducts = new ArrayList();    Iterator products = repository.iterator();    while (products.hasNext()) {       Product product = (Product)products.next();       if ( spec.getAugend().isSatisfiedBy(product) &&           spec.getAddend().isSatisfiedBy(product))          foundProducts.add(product);    }    return foundProducts; } 

The AndSpec class looks like this:

  public class AndSpec {     private ProductSpecification augend, addend;     public AndSpec(Spec augend, Spec addend) {        this.augend = augend;        this.addend = addend;     }     public Spec getAddend() {        return addend;     }     public Spec getAugend() {        return augend;     }  } 

After implementing step 2, the code now looks like this:

 public List byColorAndBelowPrice(Color color, float price) {    ...    AndSpec spec = new AndSpec(colorSpec, priceSpec);    while (products.hasNext()) {       Product product = (Product)products.next();       if ( spec.isSatisfiedBy(product))          foundProducts.add(product);    }    return foundProducts; } public class AndSpec  extends Spec...     public boolean isSatisfiedBy(Product product) {        return getAugend().isSatisfiedBy(product) &&           getAddend().isSatisfiedBy(product);     } 

I now have a composite specification that handles an AND operation to join two concrete specifications. In another object selection method called belowPriceAvoidingAColor(…), I have more complicated conditional logic:

 public class ProductFinder...    public List belowPriceAvoidingAColor(float price, Color color) {       List foundProducts = new ArrayList();       Iterator products = repository.iterator();       while (products.hasNext()) {          Product product = (Product) products.next();          if (product.getPrice() < price && product.getColor() != color)             foundProducts.add(product);       }       return foundProducts;    } 

This code requires two composite specifications (AndProductSpecification and NotProductSpecification) and two concrete specifications. The conditional logic in the method can be portrayed as shown in the diagram on the following page.

My first task is to produce a NotSpec:

  public class NotSpec extends Spec {     private Spec specToNegate;     public NotSpec(Spec specToNegate) {        this.specToNegate = specToNegate;     }     public boolean isSatisfiedBy(Product product) {        return !specToNegate.isSatisfiedBy(product);     }  } 

Then I modify the conditional logic to use AndSpec and NotSpec:

 public List belowPriceAvoidingAColor(float price, Color color) {     AndSpec spec =        new AndSpec(           new BelowPriceSpec(price),           new NotSpec(              new ColorSpec(color)           )        );    List foundProducts = new ArrayList();    Iterator products = repository.iterator();    while (products.hasNext()) {       Product product = (Product) products.next();       if ( spec.isSatisfiedBy(product))          foundProducts.add(product);    }    return foundProducts; } 

That takes care of the belowPriceAvoidingAColor(…) method. I continue replacing logic in the object selection methods until all of them use either one concrete specification or one composite specification.

5. The bodies of all object selection methods are now identical, except for the specification creation logic:

  Spec spec = ...create some spec List foundProducts = new ArrayList(); Iterator products = repository.iterator(); while (products.hasNext()) {    Product product = (Product) products.next();    if ( spec.isSatisfiedBy(product))       foundProducts.add(product); } return foundProducts; 

This means I can apply Extract Method [F] on everything except the specification creation logic in any object selection method to produce a selectBy(…) method. I decide to perform this step on the belowPrice(…) method:

 public List belowPrice(float price) {    BelowPriceSpec spec = new BelowPriceSpec(price);    return  selectBy(spec); }  private List selectBy(ProductSpecification spec) {     List foundProducts = new ArrayList();     Iterator products = repository.iterator();     while (products.hasNext()) {        Product product = (Product)products.next();        if (spec.isSatisfiedBy(product))           foundProducts.add(product);     }     return foundProducts;  } 

I compile and test to make sure this works. Now I make remaining ProductFinder object selection methods call the same selectBy(…) method. For example, here's the call to belowPriceAvoidingAColor(…):

 public List belowPriceAvoidingAColor(float price, Color color) {    ProductSpec spec =       new AndProduct(          new BelowPriceSpec(price),          new NotSpec(             new ColorSpec(color)          )       );    return  selectBy(spec); } 

6. Now every object selection method can be inlined using Inline Method [F]:

 public class ProductFinder...      public List byColor(Color colorOfProductToFind) {         ColorSpec spec = new ColorSpec(colorOfProductToFind));         return selectBy(spec);      } public class ProductFinderTests extends TestCase...    public void testFindByColor()...         List foundProducts = finder.byColor(Color.red);        ColorSpec spec = new ColorSpec(Color.red));        List foundProducts = finder.selectBy(spec); 

I compile and test to make sure that everything's working. Then I conclude this refactoring by repeating step 6 for every object selection method.

Amazon


Refactoring to Patterns (The Addison-Wesley Signature Series)
Refactoring to Patterns
ISBN: 0321213351
EAN: 2147483647
Year: 2003
Pages: 103

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net