23.7. 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:

  1. time period (Monday)

  2. customer type (senior)

  3. a particular line item product (Darjeeling tea)

Another point of clarification: Three of the four example policies are really just “percentage discount” strategies, which simplifies our view of the problem.

Part of the answer to this problem requires defining the store's conflict resolution strategy. Usually, a store applies the “best for the customer” (lowest price) conflict resolution strategy, but this is not required, and it could change. For example, during a difficult financial period, the store may have to use a “highest price” conflict resolution strategy.

The first point to note is that there can exist multiple co-existing strategies, that is, one sale may have several pricing strategies. Another point to note is that a pricing strategy can be related to the type of customer (for example, a senior). This has creation design implications: The customer type must be known by the StrategyFactory at the time of creation of a pricing strategy for the customer.

Similarly, a pricing strategy can be related to the type of product being bought (for example, Darjeeling tea). This likewise has creation design implications: The ProductSpecification must be known by the StrategyFactory at the time of creation of a pricing strategy influenced by the product.

Is there a way to change the design so that the Sale object does not know if it is dealing with one or many pricing strategies, and also offer a design for the conflict resolution? Yes, with the Composite pattern.

Composite

Context/Problem

How to treat a group or composition structure of objects the same way (polymorphically) as a non-composite (atomic) object?

Solution

Define classes for composite and atomic objects so that they implement the same interface.


For example, a new class called CompositeBestForCustomerPricingStrategy (well, at least it's descriptive) can implement the ISalesPricingStrategy and itself contain other ISalesPricingStrategy objects. Figure 23.13 explains the design idea in detail.

Figure 23.13. The Composite pattern.


Observe that in this design, the composite classes such as CompositeBestForCustomerPricingStrategy inherit an attribute pricingStrategies that contains a list of more ISalePricingStrategy objects. This is a signature feature of a composite object: The outer composite object contains a list of inner objects, and both the outer and inner objects implement the same interface. That is, the composite class itself implements the ISalePricingStrategy interface.

Thus, we can attach either a composite CompositeBestForCustomerPricingStrategy object (which contains other strategies inside of it) or an atomic PercentDiscountPricingStrategy object to the Sale object, and the Sale does not know or care if its pricing strategy is an atomic or composite strategy—it looks the same to the Sale object. It is just another object that implements the ISalePricingStrategy interface and understands the getTotal message (Figure 23.14).

Figure 23.14. Collaboration with a Composite.


UML notation— In Figure 23.14, please note a way to indicate objects that implement an interface, when we don't care to specify the exact implementation class. Simply specifying the implementation class as Object communicates “no comment” on the specific class. This is a common need when diagramming.

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 pricingStrategies = new ArrayList();
						public add( ISalePricingStrategy s )
						{
						pricingStrategies.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 = pricingStrategies.iterator(); i.hasNext(); )
						{
						ISalePricingStrategy strategy =
						(ISalePricingStrategy)i.next();
						Money total = strategy.getTotal( sale );
						lowestTotal = total.min( lowestTotal );
						}
						return lowestTotal;
						}
						} // end of class
					

UML notation— Figure 23.13 introduced some new UML notation for class hierarchies and inheritance, which is explained in Figure 23.15.

Figure 23.15. Abstract superclasses, abstract methods, and inheritance in the UML.


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:

  1. Current store-defined discount, added when the sale is created.

  2. Customer type discount, added when the customer type is communicated to the POS.

  3. Product type discount (if bought Darjeeling tea, 15% off the overall sale), added when the product is entered to the sale.

The design of the first case is shown in Figure 23.16. As in the original design discussed earlier, the strategy class name to instantiate could be read as a system property, and a percentage value could be read from an external data store.

Figure 23.16. Creating a composite strategy.


For the second case of a customer type discount, first recall the use case extension which previously recognized this requirement:

Use Case UC1: Process Sale

...

Extensions (or Alternative Flows):

5b. Customer says they are eligible for a discount (e.g., employee, preferred customer)

  1. Cashier signals discount request.

  2. Cashier enters Customer identification.

  3. System presents discount total, based on discount rules.


This indicates a new system operation on the POS system, in addition to makeNewSale, enterItem, endSale, and makePayment. We will call this fifth system operation enterCustomerForDiscount; it may optionally occur after the endSale operation. It implies that some form of customer identification will have to come in through the user interface, the customerID. Perhaps it can be captured from a card reader, or via the keyboard.

The design of the second case is shown in Figure 23.17 and Figure 23.18. Not surprisingly, the factory object is responsible for the creation of the additional pricing strategy. It may make another PercentageDiscountPricingStrategy that represents, for example, a senior discount. But as with the original creation design, the choice of class will be read in as a system property, as will the specific percentage for the customer type, to provide Protected Variations with respect to changing the class or values. Note that by virtue of the Composite pattern, the Sale may have two or three conflicting pricing strategies attached to it, but it continues to look like a single strategy to the Sale object.

Figure 23.17. Creating the pricing strategy for a customer discount, part 1.


Figure 23.18. Creating the pricing strategy for a customer discount, part 2.


UML notation— Figure 23.17 and Figure 23.18 show an important UML idea in interaction diagrams: splitting one diagram into two, to keep each more readable.

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 design—to 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 designers—perhaps 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 ProductSpecification 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 commands—through the use of Composite. The Command pattern is described in a subsequent chapter.

Exercise Challenges

Exercise 1:

Buying a particular product causes a new discount to the entire sale. For example, if bought Darjeeling tea, 15% off the overall sale.

Exercise 2:

All the pricing policies considered so far apply to the overall sale total, sometimes called transaction level discounts. But the most interesting design challenge is to handle line item level discounts. For example:

  • Buy two suits, get one free.

  • Buy three X-computers, get the Y-printer at 50% off.

Is there an elegant way to design this with Strategy objects?


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.

Related Patterns


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.163.209