26.8. Composite (GoF) and Other Design Principles
To raise yet another interesting requirements and design problem: How do we handle the case of multiple, conflicting pricing policies? For example, suppose a store has the following policies in effect today (Monday):
- 20% senior discount policy
- preferred customer discount of 15% off sales over $400
- on Monday, there is $50 off purchases over $500
- buy 1 case of Darjeeling tea, get 15% discount off of everything
Suppose a senior who is also a preferred customer buys 1 case of Darjeeling tea, and $600 of veggieburgers (clearly an enthusiastic vegetarian who loves chai). What pricing policy should be applied?To clarify: There are now pricing strategies that attach to the sale by virtue of three factors:
- time period (Monday)
- customer type (senior)
- a particular line item product (Darjeeling tea)
Name: | Composite |
Problem: | How to treat a group or composition structure of objects the same way (polymorphically) as a non-composite (atomic) object? |
Solution: (advice) | Define classes for composite and atomic objects so that they implement the same interface. |
Figure 26.14. The Composite pattern.
[View full size image]
Figure 26.15. Collaboration with a Composite.
[View full size image]
In Figure 26.15, please note a way to indicate objects that implement an interface, when we don't care to specify the exact implementation class.To clarify with some sample code in Java, the CompositePricingStrategy and one of its subclasses are defined as follows:
// superclass so all subclasses can inherit a List of strategies
public abstract class CompositePricingStrategy
implements ISalePricingStrategy
{
protected List strategies = new ArrayList();
public add( ISalePricingStrategy s )
{
strategies.add( s );
}
public abstract Money getTotal( Sale sale );
} // end of class
// a Composite Strategy that returns the lowest total
// of its inner SalePricingStrategies
public class CompositeBestForCustomerPricingStrategy
extends CompositePricingStrategy
{
public Money getTotal( Sale sale )
{
Money lowestTotal = new Money( Integer.MAX_VALUE );
// iterate over all the inner strategies
for( Iterator i = strategies.iterator(); i.hasNext(); )
{
ISalePricingStrategy strategy =
(ISalePricingStrategy)i.next();
Money total = strategy.getTotal( sale );
lowestTotal = total.min( lowestTotal );
}
return lowestTotal;
}
} // end of class
Figure 26.16. Abstract superclasses, abstract methods, and inheritance in the UML.
[View full size image]
Creating Multiple SalePricingStrategies
With the Composite pattern, we have made a group of multiple (and conflicting) pricing strategies look to the Sale object like a single pricing strategy. The composite object that contains the group also implements the ISalePricingStrategy interface. The more challenging (and interesting) part of this design problem is: When do we create these strategies?A desirable design will start by creating a Composite that contains the present moment's store discount policy (which could be set to 0% discount if none is active), such as some PercentageDiscountPricingStrategy . Then, if at a later step in the scenario, another pricing strategy is discovered to also apply (such as senior discount), it will be easy to add it to the composite, using the inherited CompositePricingStrategy.add method.There are three points in the scenario where pricing strategies may be added to the composite:
- Current store-defined discount, added when the sale is created.
- Customer type discount, added when the customer type is communicated to the POS.
- Product type discount (if bought Darjeeling tea, 15% off the overall sale), added when the product is entered to the sale.
Figure 26.17. Creating a composite strategy.
part 1.
part 2.
[View full size image]
Figure 26.18 and Figure 26.19 show an important UML 2 idea in interaction diagrams: Using the ref and sd frame to relate diagrams.
Considering GRASP and Other Principles in the Design
To review thinking in terms of some basic GRASP patterns: For this second case, why not have the Register send a message to the PricingStrategyFactory , to create this new pricing strategy and then pass it to the Sale ? One reason is to support Low Coupling. The Sale is already coupled to the factory; by making the Register also collaborate with it, the coupling in the design would increase. Furthermore, the Sale is the Information Expert that knows its current pricing strategy (which is going to be modified); so by Expert, it is also justified to delegate to the Sale .Observe in the design that customerID is transformed into a Customer object via the Register asking the Store for a Customer , given an ID. First, it is justifiable to give the getCustomer responsibility to the Store ; by Information Expert and the goal of low representational gap, the Store can know all the Customers . And the Register asks the Store , because the Register already has attribute visibility to the Store (from earlier design work); if the Sale had to ask the Store , the Sale would need a reference to the Store , increasing the coupling beyond its current levels, and therefore not supporting Low Coupling.IDs to Objects Second, why transform the customerID (an "ID"perhaps a number) into a Customer object? This is a common practice in object designto transform keys and IDs for things into true objects. This transformation often takes place shortly after an ID or key enters the domain layer of the Design Model from the UI layer. It doesn't have a pattern name, but it could be a candidate for a pattern because it is such a common idiom among experienced object designersperhaps IDs to Objects . Why bother? Having a true Customer object that encapsulates a set of information about the customer, and which can have behavior (related to Information Expert, for example), frequently becomes beneficial and flexible as the design grows, even if the designer does not originally perceive a need for a true object and thought instead that a plain number or ID would be sufficient. Note that in the earlier design, the transformation of the itemID into a ProductDescription object is another example of this IDs to Objects pattern.Pass Aggregate Object as Parameter Finally, note that in the addCustomerPricingStrategy(s:Sale) message we pass a Sale to the factory, and then the factory turns around and asks for the Customer and PricingStrategy from the Sale .Why not just extract these two objects from the Sale , and instead pass in the Customer and PricingStrategy to the factory? The answer is another common object design idiom: Avoid extracting child objects out of parent or aggregate objects, and then passing around the child objects. Rather, pass around the aggregate object that contains child objects.Following this principle increases flexibility, because then the factory can collaborate with the entire Sale in ways we may not have previously anticipated as necessary (which is very common), and as a corollary, it reduces the need to anticipate what the factory object needs; the designer just passes as a parameter the entire Sale , without knowing what more particular objects the factory may need. Although this idiom does not have a name, it is related to Low Coupling and Protected Variations. Perhaps it could be called the Pass Aggregate Object as Parameter pattern.
Summary
This design problem was squeezed for many tips in object design. A skilled object designer has many of these patterns committed to memory through studying their published explanations, and has internalized core principles, such as those described in the GRASP family.Please note that although this application of Composite was to a Strategy family, the Composite pattern can be applied to other kinds of objects, not just strategies. For example, it is common to create "macro commands"commands that contain other commandsthrough the use of Composite. The Command pattern is described in a subsequent chapter.Related Patterns Composite is often used with the Strategy and Command patterns. Composite is based on Polymorphism and provides Protected Variations to a client so that it is not impacted if its related objects are atomic or composite.