Replace OneMany Distinctions with Composite

Prev don't be afraid of buying books Next

Replace One/Many Distinctions with Composite

A class processes single and multiple objects using separate pieces of code.



Use a Composite to produce one piece of code capable of handling single or multiple objects.



Motivation

When a class has a method for processing one object and a nearly identical method for processing a collection of the objects, a one/many distinction exists. Such a distinction can result in problems such as the following.

  • Duplicated code: Because the method that processes one object does the same thing as the method that processes a collection of the objects, duplicated code is often spread across the two methods. It's possible to reduce this duplication without implementing a Composite [DP] (see the Example section for details), yet even if duplication is reduced, there are still two methods performing the same kind of processing.

  • Nonuniform client code: Whether they have single objects or collections of objects, clients want their objects processed in one way. Yet the existence of two processing methods with different signatures forces clients to pass different kinds of data to the methods (i.e., one object or a collection of objects). This makes client code nonuniform, which reduces simplicity.

  • Merging of results: The best way to explain this is with an example. Say you want to find all products that are red and priced under $5.00 or blue and priced above $10.00. One way to find these products is to call a ProductRepository's selectBy(List specs) method, which returns a List of results. Here's an example call to selectBy(…):

     List redProductsUnderFiveDollars = new ArrayList(); redProductsUnderFiveDollars.add(new ColorSpec(Color.red)); redProductsUnderFiveDollars.add(new BelowPriceSpec(5.00)); List foundRedProductsUnderFiveDollars =    productRepository.selectBy(redProductsUnderFiveDollars); 

    The main problem with selectBy(List specs) is that it can't handle an OR condition. So if you want to find all products that are red and under $5.00 or blue and above $10.00, you have to make separate calls to selectBy(…) and then merge the results:

     List foundRedProductsUnderFiveDollars =    productRepository.selectBy(redProductsUnderFiveDollars); List foundBlueProductsAboveTenDollars =    productRepository.selectBy(blueProductsAboveTenDollars); List foundProducts = new ArrayList(); foundProducts.addAll(foundRedProductsUnderFiveDollars); foundProducts.addAll(foundBlueProductsAboveTenDollars); 

As you can see, this approach is awkward and verbose.

The Composite pattern provides a better way. It lets clients process one or many objects with a single method. This has many benefits.

  • There's no duplicated code across methods because only one method handles the objects, whether one or many.

  • Clients communicate with that method in a uniform way.

  • Clients can make one call to obtain the results of processing a tree of objects rather than having to make several calls and merging processed results. For example, to find red products under $5.00 or blue products above $10.00, a client creates and passes the following Composite to the processing method:

In short, replacing a one/many distinction with a Composite is a way to remove duplication, make client calls uniform, and support the processing of trees of objects. However, if these latter two particular benefits aren't all that important to your system and you can reduce most of the duplication in methods that have a one/many distinction, a Composite implementation could be overkill.

One common downside of the Composite pattern relates to type safety. To prevent clients from adding invalid objects to a Composite, the Composite code must contain runtime checks of the objects that clients attempt to add to it. This problem is also present with collections because clients can add invalid objects to collections as well.

Benefits and Liabilities

+

Removes duplicate code associated with handling one or many objects.

+

Provides a uniform way to process one or many objects.

+

Supports richer ways to process many objects (e.g., an OR expression).

May require runtime checks for type safety during construction of the Composite.







Mechanics

In this section and the Example section, a method that works with one object is called a one-object method while a method that works with a collection of objects is called a many-object method.

  1. The many-object method accepts a collection as a parameter. Create a new class that accepts the collection in a constructor and provides a getter method for it. This composite class will become what is known as a Composite in Design Patterns [DP].

    Within the many-object method, declare and instantiate an instance of your composite (i.e., the new class). In addition, find all references within the many-object method to the collection and update the code so access to the collection is obtained via the composite's getter method.

    • Compile and test.

  2. Apply Extract Method [F] on the code within the many-object method that works with the collection. Make the extracted method public. Then apply Move Method [F] on the extracted method to move it to your composite.

    • Compile and test.

  3. The many-object method will now be nearly identical to the one-object method. The main difference is that the many-object method instantiates your composite. If there are other differences, refactor to eliminate them.

    • Compile and test.

  4. Change the many-object method so it contains one line of code: a call to the one-object method that passes it your composite instance as an argument. You'll need to make the composite share the same interface or superclass as the type used by the one-object method.

    To do this, consider making the composite a subclass of the type used by the one-object method or create a new interface (using Extract Interface [F]) that the composite and all objects passed to the one-object method implement.

    • Compile and test.

  5. Because the many-object method now consists of just one line of code, it can be inlined by applying Inline Method [F].

    • Compile and test.

  6. Apply Encapsulate Collection [F] on your composite. This will produce an add(…) method on the composite, which clients will call instead of passing a collection to the composite's constructor. In addition, the getter method for the collection will now return an unmodifiable collection.

    • Compile and test.

Example

This example deals with Spec instances and how they are used to obtain a desired set of Product instances from a ProductRepository. The example also illustrates the Specification pattern [Evans] as described in Replace Implicit Language with Interpreter (269).

Let's begin by studying some test code for the ProductRepository. Before any test can run, a ProductRepository (called repository) must be created. For the test code, I fill a repository with toy Product instances:

 public class ProductRepositoryTest extends TestCase...    private ProductRepository repository;    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() {       repository = new ProductRepository();       repository.add(fireTruck);       repository.add(barbieClassic);       repository.add(frisbee);       repository.add(baseball);       repository.add(toyConvertible);    } 

The first test we'll study looks for Product instances of a certain color by means of a call to repository.selectBy(…):

 public class ProductRepositoryTest extends TestCase...    public void testFindByColor() {       List foundProducts = repository.selectBy(new ColorSpec(Color.red));       assertEquals("found 2 red products", 2, foundProducts.size());       assertTrue("found fireTruck", foundProducts.contains(fireTruck));       assertTrue(          "found Toy Porsche Convertible",          foundProducts.contains(toyConvertible));    } 

The repository.selectBy(…) method looks like this:

 public class ProductRepository...    private List products = new ArrayList();    public Iterator iterator() {       return products.iterator();    }    public List selectBy(Spec spec) {       List foundProducts = new ArrayList();       Iterator products = iterator();       while (products.hasNext()) {          Product product = (Product)products.next();          if (spec.isSatisfiedBy(product))             foundProducts.add(product);       }       return foundProducts;    } 

Let's now look at another test, which calls a different repository.selectBy(…) method. This test assembles a List of Spec instances in order to select specific kinds of products from the repository:

 public class ProductRepositoryTest extends TestCase...    public void testFindByColorSizeAndBelowPrice() {       List specs = new ArrayList();       specs.add(new ColorSpec(Color.red));       specs.add(new SizeSpec(ProductSize.SMALL));       specs.add(new BelowPriceSpec(10.00));       List foundProducts = repository.selectBy(specs);       assertEquals(          "small red products below $10.00",          0,          foundProducts.size());    } 

The List-based repository.selectBy(…) method looks like this:

 public class ProductRepository {    public List selectBy(List specs) {       List foundProducts = new ArrayList();       Iterator products = iterator();       while (products.hasNext()) {          Product product = (Product)products.next();          Iterator specifications = specs.iterator();          boolean satisfiesAllSpecs = true;          while (specifications.hasNext()) {             Spec productSpec = ((Spec)specifications.next());             satisfiesAllSpecs &= productSpec.isSatisfiedBy(product);          }          if (satisfiesAllSpecs)             foundProducts.add(product);       }       return foundProducts;    } 

As you can see, the List-based selectBy(…) method is more complicated than the one-Spec selectBy(…) method. If you compare the two methods, you'll notice a good deal of duplicate code. A Composite can help remove this duplication; however, there's another way to remove the duplication that doesn't involve a Composite. Consider this:

 public class ProductRepository...    public List selectBy(Spec spec) {        Spec[] specs = { spec };        return selectBy(Arrays.asList(specs));    }    public List selectBy(List specs)...        // same implementation as before 

This solution retains the more complicated List-based selectBy(…) method. However, it also completely simplifies the one-Spec selectBy(…) method, which greatly reduces the duplicated code. The only remaining duplication is the existence of the two selectBy(…) methods.

So, is it wise to use this solution instead of refactoring to Composite? Yes and no. It all depends on the needs of the code in question. For the system on which this example code was based, there is a need to support queries with OR, AND, and NOT conditions, like this one:

 product.getColor() != targetColor || product.getPrice() < targetPrice 

The List-based selectBy(…) method cannot support such queries. In addition, having just one selectBy(…) method is preferred so clients can call it in a uniform way. Therefore, I decide to refactor to the Composite pattern by implementing the following steps.

1. The List-based selectBy(…) method is the many-object method. It accepts the following parameter: List specs. My first step is to create a new class that will hold onto the value of the specs parameter and provide access to it via a getter method:

  public class CompositeSpec {     private List specs;     public CompositeSpec(List specs) {        this.specs = specs;     }     public List getSpecs() {        return specs;     }  } 

Next, I'll instantiate this class within the List-based selectBy(…) method and update code to call its getter method:

 public class ProductRepository...    public List selectBy(List specs) {        CompositeSpec spec = new CompositeSpec(specs);       List foundProducts = new ArrayList();       Iterator products = iterator();       while (products.hasNext()) {          Product product = (Product)products.next();          Iterator specifications =  spec.getSpecs().iterator();          boolean satisfiesAllSpecs = true;          while (specifications.hasNext()) {             Spec productSpec = ((Spec)specifications.next());             satisfiesAllSpecs &= productSpec.isSatisfiedBy(product);          }          if (satisfiesAllSpecs)             foundProducts.add(product);       }       return foundProducts;    } 

I compile and test to confirm that these changes work.

2. Now I apply Extract Method [F] on the selectBy(…) code that specifically deals with specs:

 public class ProductRepository...    public List selectBy(List specs) {       CompositeSpec spec = new CompositeSpec(specs);       List foundProducts = new ArrayList();       Iterator products = iterator();       while (products.hasNext()) {          Product product = (Product)products.next();          if ( isSatisfiedBy(spec, product))             foundProducts.add(product);       }       return foundProducts;    }     public boolean isSatisfiedBy(CompositeSpec spec, Product product) {        Iterator specifications = spec.getSpecs().iterator();        boolean satisfiesAllSpecs = true;        while (specifications.hasNext()) {           Spec productSpec = ((Spec)specifications.next());           satisfiesAllSpecs &= productSpec.isSatisfiedBy(product);        }        return satisfiesAllSpecs;     } 

The compiler and test code are happy with this change, so I can now apply Move Method [F] to move the isSatisfiedBy(…) method to the CompositeSpec class:

 public class ProductRepository...    public List selectBy(List specs) {       CompositeSpec spec = new CompositeSpec(specs);       List foundProducts = new ArrayList();       Iterator products = iterator();       while (products.hasNext()) {          Product product = (Product)products.next();          if ( spec.isSatisfiedBy(product))             foundProducts.add(product);       }       return foundProducts;    } public class CompositeSpec...     public boolean isSatisfiedBy(Product product) {        Iterator specifications = getSpecs().iterator();        boolean satisfiesAllSpecs = true;        while (specifications.hasNext()) {           Spec productSpec = ((Spec)specifications.next());           satisfiesAllSpecs &= productSpec.isSatisfiedBy(product);        }        return satisfiesAllSpecs;     } 

One again, I check that the compiler and test code are happy with this change. Both are.

3. The two selectBy(…) methods are now nearly identical. The only difference is that the List-based selectBy(…) method instantiates a CompositeSpec instance:

 public class ProductRepository...    public List selectBy(Spec spec) {       // same code    }    public List selectBy(List specs) {        CompositeSpec spec = new CompositeSpec(specs);       // same code    } 

The next step will help remove the duplicated code.

4. I now want to make the List-based selectBy(…) method call the one-Spec selectBy(…) method, like so:

 public class ProductRepository...    public List selectBy(Spec spec)...    public List selectBy(List specs) {        return selectBy(new CompositeSpec(specs));    } 

The compiler does not like this code because CompositeSpec does not share the same interface as Spec, the type used by the called selectBy(…) method. Spec is an abstract class that looks like this:

Since CompositeSpec already implements the isSatisfiedBy(…) method declared by Spec, it's trivial to make CompositeSpec a subclass of Spec:

 public class CompositeSpec  extends Spec... 

Now the compiler is happy, as is the test code.

5. Because the List-based selectBy(…) method is now only one line of code that calls the one-Spec selectBy(…) method, I inline it by applying Inline Method [F]. Client code that used to call the List-based selectBy(…) now calls the one-Spec selectBy(…) method. Here's an example of such a change:

 public class ProductRepositoryTest...    public void testFindByColorSizeAndBelowPrice() {       List specs = new ArrayList();       specs.add(new ColorSpec(Color.red));       specs.add(new SizeSpec(ProductSize.SMALL));       specs.add(new BelowPriceSpec(10.00));         List foundProducts = repository.selectBy(specs);       List foundProducts = repository.selectBy( new CompositeSpec(specs));       ... 

There's now only one selectBy(…) method that accepts Spec objects like ColorSpec, SizeSpec, or the new CompositeSpec. This is a useful start. However, to build Composite structures that support product searches like product.getColor() != targetColor || product.getPrice() < targetPrice, there is a need for classes like NotSpec and OrSpec. I won't show how they're created here; you can read about them in the refactoring Replace Implicit Language with Interpreter (269).

6. The final step involves applying Encapsulate Collection [F] on the collection inside of CompositeSpec. I do this to make CompositeSpec more type-safe (i.e., to prevent clients from adding objects to it that aren't a subclass of Spec).

I begin by defining the add(Spec spec) method:

 public class CompositeSpec extends Spec...    private List specs;     public void add(Spec spec) {        specs.add(spec);     } 

Next, I initialize specs to an empty list:

 public class CompositeSpec extends Spec...    private List specs  = new ArrayList(); 

Now comes the fun part. I find all callers of CompositeSpec's constructor and update them to call a new, default CompositeSpec constructor as well as the new add(…) method. Here is one such caller and the updates I make to it:

 public class ProductRepositoryTest...    public void testFindByColorSizeAndBelowPrice()...         List specs = new ArrayList();        CompositeSpec specs = new CompositeSpec();       specs.add(new ColorSpec(Color.red));       specs.add(new SizeSpec(ProductSize.SMALL));       specs.add(new BelowPriceSpec(10.00));       List foundProducts = repository.selectBy( specs);       ... 

I compile and test to confirm that the changes work. Once I've updated all other clients, there are no more callers to CompositeSpec's constructor that take a List. So I delete it:

 public class CompositeSpec extends Spec...      public CompositeSpec(List specs) {         this.specs = specs;      } 

Now I update CompositeSpec's getSpecs(…) method to return an unmodifiable version of specs:

 public class CompositeSpec extends Spec...    private List specs = new ArrayList();    public List getSpecs()       return  Collections.unmodifiableList(specs);    } 

I compile and test to confirm that my implementation of Encapsulate Collection works. It does. CompositeSpec is now a fine implementation of the Composite pattern:

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