© Edward Sciore 2019
Edward ScioreJava Program Designhttps://doi.org/10.1007/978-1-4842-4143-1_9

9. Composites

Edward Sciore1 
(1)
Newton, MA, USA
 

Chapter 8 examined decorators, which are wrappers that implement the same interface as the object they wrap. This chapter examines composite objects. A composite object is similar to a decorator except that it wraps multiple objects, each of which implements the same interface as itself. This seemingly small distinction makes a big difference in the structure of a composite and how it is used. Composite objects correspond to trees and composite methods tend to involve tree traversals.

This chapter presents three examples of composites: predicates, graphical user interfaces (GUIs), and cookbook recipes. These examples share a common class design known as the composite pattern. They also have some functional differences that illustrate the different choices a designer faces.

Predicates as Composites

A predicate is a mathematical expression that evaluates to true or false. Given two predicates, you can create another, larger predicate by applying the operator and or or to them. This larger predicate is called a composite and the two smaller predicates are its components. You can continue this process for as long as you like, building up larger and larger composite predicates. A noncomposite predicate is called a basic predicate.

For example, Listing 9-1 displays a composite predicate that is composed of three basic predicates. It returns true if n is less than 20 and divisible by 2 or 3.
n<20 and (n%2=0 or n%3=0)
Listing 9-1

A Composite Predicate

A composite predicate can be represented as a tree whose internal nodes are the operators {and, or} and whose leaves are basic predicates. Figure 9-1 depicts the predicate tree for Listing 9-1.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig1_HTML.png
Figure 9-1

The predicate tree for Listing 9-1

A Java predicate is an object that implements the interface Predicate, as discussed in Chapter 6. A basic predicate is typically created via a lambda expression. For example, Listing 9-2 gives Java statements to implement the three basic predicates in Listing 9-1.
Predicate<Integer> pred1 = n -> n < 20;
Predicate<Integer> pred2 = n -> n%2 == 0;
Predicate<Integer> pred3 = n -> n%3 == 0;
Listing 9-2

Basic Predicates in Java

One way to support composite predicates in Java is to create a class for each operator. Call these classes AndPredicate and OrPredicate. Each class wraps two component predicates and implements Predicate. The test method for AndPredicate returns true if both components return true, and the test method for OrPredicate returns true if at least one component returns true. For coding convenience I will also create the class CompositePredicate to be the common superclass of AndPredicate and OrPredicate that manages their wrapped objects. Listing 9-3 gives the code for CompositePredicate and Listing 9-4 gives the code for AndPredicate. The code for OrPredicate is similar and is omitted.
public abstract class CompositePredicate<T>
                      implements Predicate<T> {
   protected Predicate<T> p1, p2;
   protected CompositePredicate(Predicate<T> p1,
                                Predicate<T> p2) {
      this.p1 = p1;
      this.p2 = p2;
   }
   public abstract boolean test(T t);
}
Listing 9-3

The CompositePredicate Class

public class AndPredicate<T> extends CompositePredicate<T> {
   public AndPredicate(Predicate<T> p1, Predicate<T> p2) {
      super(p1, p2);
   }
   public boolean test(T t) {
      return p1.test(t) && p2.test(t);
   }
}
Listing 9-4

The AndPredicate Class

Figure 9-2 contains a class diagram that shows the relationship between these Predicate classes. The three “BasicPredicate” classes correspond to the anonymous classes created for pred1, pred2, and pred3 in Listing 9-2.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig2_HTML.jpg
Figure 9-2

The class diagram for Predicate

The class diagram looks very much like the decorator pattern. The difference is that the wrapper class CompositePredicate wraps two objects instead of one. To highlight this difference, the dependency arrow is annotated with the optional cardinality label “2.”

The class CompositePredicateTest in Listing 9-5 illustrates the use of composite predicates in Java. This code begins by creating the basic predicates pred1, pred2, and pred3 as in Listing 9-2. It then implements the composite predicate of Listing 9-1 in three different ways.
public class CompositePredicateTest {
   public static void main(String[] args) {
      Predicate<Integer> pred1 = n -> n < 20;
      Predicate<Integer> pred2 = n -> n%2 == 0;
      Predicate<Integer> pred3 = n -> n%3 == 0;
      // First: use AndPredicate and OrPredicate objects
      Predicate<Integer> pred4 =
                     new OrPredicate<Integer>(pred2, pred3);
      Predicate<Integer> pred5 =
                     new AndPredicate<Integer>(pred1, pred4);
      printUsing(pred5);
      // Second: use the 'or' and 'and' methods separately
      Predicate<Integer> pred6 = pred2.or(pred3);
      Predicate<Integer> pred7 = pred1.and(pred6);
      printUsing(pred7);
      // Third: compose the 'or' and 'and' methods
      Predicate<Integer> pred8 = pred1.and(pred2.or(pred3));
      printUsing(pred8);
   }
   private static void printUsing(Predicate<Integer> p) {
      for (int i=1; i<100; i++)
         if (p.test(i))
            System.out.print(i + " ");
      System.out.println();
   }
}
Listing 9-5

The CompositePredicateTest Class

The first way uses the AndPredicate and OrPredicate classes. Predicate pred4 is an OrPredicate object and predicate pred5 is an AndPredicate object. The diagram of Figure 9-3 depicts these five Predicate objects in memory. The diagram is similar to the memory diagram of Figure 8-3, in that each object is represented by a rectangle and the values of its global variables are shown within its rectangle. Note how the object references form a tree that corresponds exactly to the predicate tree of Figure 9-1.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig3_HTML.jpg
Figure 9-3

The memory representation of a composite predicate

After creating the predicate pred5, the code of Listing 9-5 passes pred5 to its printUsing method, which calls the predicate’s test method on the integers from 1 to 100. Figure 9-4 depicts a sequence diagram that traces the execution of the expression pred5.test(9). Step 2 calls the test method of pred5’s first component, pred1, which returns true. Step 4 then calls test on its second component, pred4. In order to determine its response, pred4 calls test on its two components. Component pred2 returns false but pred3 returns true; thus pred4 can return true. Since both components of pred5 have now returned true, pred5 returns true.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig4_HTML.jpg
Figure 9-4

A sequence diagram for the expression pred5.test(9)

Note how the call to test(9) gets passed from the root of the predicate tree down to its leaves. In fact, this sequence of method calls corresponds to a postorder traversal of the tree.

The classes AndPredicate and OrPredicate are not part of the Java library. Instead, the Predicate interface has the default methods and and or, which make it possible to create composite predicates without having to create the composite objects yourself.

The use of these methods is illustrated in the second and third parts of Listing 9-5. The variable pred6 returns true if either pred2 or pred3 is true, and is functionally equivalent to pred4. Similarly, the variable pred7 is functionally equivalent to pred5. The calls to the and and or methods can also be composed, as shown by variable pred8.

Listing 9-6 shows how the and and or methods can be implemented. The and method creates and returns an AndPredicate object that wraps two objects: the current object and the object passed into the method. The implementation of the or method is similar.
public interface Predicate<T> {
   boolean test(T t);
   default Predicate<T> and(Predicate<T> other) {
      return new AndPredicate(this, other);
   }
   default Predicate<T> or(Predicate<T> other) {
      return new OrPredicate(this, other);
   }
}
Listing 9-6

A Reasonable Implementation of Predicate

The actual Java library implementation of these methods is slightly different from Listing 9-6, and appears in Listing 9-7. The lambda expressions define anonymous inner classes that are equivalent to AndPredicate and OrPredicate. This code is quite elegant, as it eliminates the need for explicit AndPredicate and OrPredicate classes .
public interface Predicate<T> {
   boolean test(T t);
   default Predicate<T> and(Predicate<T> other) {
      return t -> test(t) && other.test(t);
   }
   default Predicate<T> or(Predicate<T> other) {
      return t -> test(t) || other.test(t);
   }
}
Listing 9-7

The Actual Implementation of Predicate

Composite Objects in JavaFX

For a second example of composite objects, consider a library for building GUI applications. When you create an application window, you often structure its contents as a composite object. For an example, Figure 9-5 depicts a window that I created using the JavaFX library.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig5_HTML.jpg
Figure 9-5

A JavaFX window

In JavaFX, a window’s content is constructed of nodes. The JavaFX library has classes to implement several types of node. This example window uses two types of node: controls and panes.

A control is a node that can be manipulated by the user. All controls in JavaFX extend the abstract class Control. The controls in the example window belong to the classes Label, ChoiceBox, CheckBox, and Button.

A pane is a node that can contain other nodes, called its children. Each pane is responsible for determining where its child nodes are placed on the screen. This is called the pane’s layout strategy.

The JavaFX library has several pane classes, each with its own layout strategy. They all extend the class Pane. The example window uses two of them: HBox and VBox. An HBox pane lays out its children horizontally. A VBox pane lays out its children vertically.

The window of Figure 9-5 has nine nodes: five controls and four panes. Figure 9-6 depicts their layout.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig6_HTML.jpg
Figure 9-6

The nodes of Figure 9-5

An alternative way to depict the structure of a window is to use a tree whose interior nodes are the panes and whose leaf nodes are the controls. This tree is called the window’s node hierarchy . Figure 9-7 depicts the node hierarchy corresponding to Figure 9-6. The labels on these nodes correspond to the variable names in the JavaFX class AccountCreationWindow, which is the code that implements the window.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig7_HTML.jpg
Figure 9-7

The node hierarchy of Figure 9-6

Listing 9-8 gives the code for AccountCreationWindow . As this code is your first introduction to a JavaFX program, it is worth examining in detail. JavaFX programs extend the library class Application and follow the template pattern. The template class is Application, which has the public method launch and the abstract strategy method start. The strategy class that implements start is AccountCreationWindow.
public class AccountCreationWindow extends Application {
   public void start(Stage stage) {
      Pane root = createNodeHierarchy();
      stage.setScene(new Scene(root));
      stage.setTitle("Bank Account Demo");
      stage.show();
   }
   public static void main(String[] args) {
      Application.launch(args);
   }
   private Pane createNodeHierarchy() {
      // see Listing 9-9
   }
}
Listing 9-8

The AccountCreationWindow Class

This technique is very similar to the way that Thread uses the template pattern (as you may recall from the end of Chapter 3). The difference is that unlike Thread, a client cannot create an Application object by simply calling the Application constructor. Instead, the static factory method launch is responsible for creating the Application object and running it in a new thread. The advantage of using a factory method is that it hides the application thread from the client, thereby shielding the thread from improper use.

The launch method also creates a Stage object, which manages the window’s frame. For example, the Stage method setTitle specifies the string to be displayed in the window’s title bar. The launch method then calls the application’s start method, passing the Stage object as the argument.

The start method of Listing 9-8 calls createNodeHierarchy to create the node hierarchy. It passes the root of that hierarchy to a new Scene object, and then sends that object to the stage via the setScene method.

Most of the code in AccountCreationWindow is devoted to creating the node hierarchy. The code for the createNodeHierarchy method appears in Listing 9-9.
private Pane createNodeHierarchy() {
   VBox p3 = new VBox(8);
   p3.setAlignment(Pos.CENTER);
   p3.setPadding(new Insets(10));
   p3.setBackground(
         new Background(
               new BackgroundFill(Color.SKYBLUE,
                      new CornerRadii(20), new Insets(0))));
   Label type = new Label("Select Account Type:");
   ChoiceBox<String> chbx  = new ChoiceBox<>();
   chbx.getItems().addAll("Savings", "Checking",
                          "Interest Checking");
   p3.getChildren().addAll(type, chbx);
   VBox p4 = new VBox(8);
   p4.setAlignment(Pos.CENTER);
   p4.setPadding(new Insets(10));
   CheckBox ckbx = new CheckBox("foreign owned?");
   Button btn  = new Button("CREATE ACCT");
   p4.getChildren().addAll(ckbx, btn);
   HBox p2 = new HBox(8);
   p2.setAlignment(Pos.CENTER);
   p2.setPadding(new Insets(10));
   p2.getChildren().addAll(p3, p4);
   VBox p1 = new VBox(8);
   p1.setAlignment(Pos.CENTER);
   p1.setPadding(new Insets(10));
   Label title = new Label("Create a New Bank Account");
   double size = title.getFont().getSize();
   title.setFont(new Font(size*2));
   title.setTextFill(Color.GREEN);
   p1.getChildren().addAll(title, p2);
   btn.setOnAction(event -> {
      String foreign = ckbx.isSelected() ? "Foreign " : "";
      String acct = chbx.getValue();
      title.setText(foreign + acct + " Account Created");
   });
   return p1;
}
Listing 9-9

The createNodeHierarchy Method

The controls behave as follows. A Label object displays a string. The string’s initial value is specified in the constructor, but its value can be changed at any time by calling the setText method. A CheckBox object displays a check box and a descriptive string. Its isSelected method returns true if the box is currently selected and false otherwise. A ChoiceBox object allows a user to select from a list of objects. The getItems method returns that list, and the getValue method returns the object chosen.

A Button object has a label and performs an action when fired. Its constructor specifies the label. The method setOnAction specifies its action. The argument to setOnAction is an EventHandler object. Chapter 10 will examine event handlers in more detail. For now, it suffices to know that this event handler is specified by a lambda expression whose body contains the code to execute when the button is clicked.

The lambda expression in Listing 9-9 calls the check box’s isSelected and the choice box’s getValue methods to obtain the type of the new account and its foreign ownership status. It then constructs a message describing these choices and sets the text of the title label to that message. In particular, if a user chooses the type “Checking,” checks “is foreign,” and clicks the button, the title label will display “Foreign Checking Account Created.”

You might be disappointed that clicking the button does not actually create an account. The problem is that the window cannot create an account without having a reference to a Bank object. Chapter 11 will discuss the proper way to connect banking information to a window, so you will need to wait until then.

Panes behave as follows. Each Pane object has the method getChildren, which returns the list of its child nodes. A client can modify the contents of the list at any time. Its setPadding method specifies the number of pixels in the margin around the pane.

A pane’s setBackground method specifies its background. Pane p3 of AccountCreationWindow demonstrates its use. The BackgroundFill object specifies a solid-colored background. (Another possibility is to use a BackgroundImage object, which specifies an image as the background.) The three arguments to BackgroundFill specify the color, the roundedness of the corners, and the size of the margin around the background.

The constructors shown for VBox and HBox take one argument, which is the number of pixels between their children. Their setAlignment method specifies how the children should be aligned. Since not all Pane subclasses support this method, it must be defined nontransparently in VBox and in HBox.

Figure 9-8 shows the class diagram for the JavaFX Node classes described in this section. This diagram deliberately omits a lot of JavaFX classes, which makes it appear far simpler than it is in reality. This simplicity makes it easier to understand the design principles underlying JavaFX. A full discussion of the JavaFX node classes is outside the scope of this book.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig8_HTML.jpg
Figure 9-8

The Node class hierarchy

Note how this class diagram is similar to the Predicate class diagram of Figure 9-2. The base classes are the subclasses of Control. The wrapper class is Pane and its subclasses are the recursive classes. The dependency arrow from Pane to Node has the label “*” to indicate that a pane can wrap an arbitrary number of nodes.

The Node interface declares many methods; the class diagram of Figure 9-8 shows just three of them. Every node holds a string that can be used as its id. By default, the id is the empty string. The method setId sets the id and the method getId returns it.

Every node also needs to know its size and location. The method getLayoutBounds returns a value of type Bounds. A Bounds object contains the height and width of the node, as well as the coordinates of its top left corner.

Controls and panes calculate their sizes differently. The size of a control is determined by its properties. For example, a label’s size depends on the text to be displayed and the size and type of its font. The size of a pane is based on the sizes of its children plus any additional space determined by the layout algorithm (such as the space between the children).

The getLayoutBounds method can be implemented as a postorder traversal of the node hierarchy. The size of the root pane depends on the sizes of its children, which depend on the sizes of their children, and so on until the Control objects are reached.

For an illustration of the getLayoutBounds method , consider the class PrintNodeInformation. Its code appears in Listing 9-10.
public class PrintNodeInformation extends Application {
   private Label label;
   private ChoiceBox<String> chbx;
   private Button btn;
   private Pane p1, p2;
   public void start(Stage stage) {
      createNodeHierarchy();
      stage.setScene(new Scene(p1));
      stage.setTitle("Bank Account Demo");
      stage.show();
      System.out.println("NODE WID HT");
      printNodeSize(label);
      printNodeSize(chbx);
      printNodeSize(p2);
      printNodeSize(btn);
      printNodeSize(p1);
   }
   public static void main(String[] args) {
      Application.launch(args);
   }
   private void printNodeSize(Node n) {
      Bounds b = n.getLayoutBounds();
      int w = (int) b.getWidth();
      int h = (int) b.getHeight();
      System.out.println(n.getId() + " " + w + " " + h );
   }
   private void createNodeHierarchy() {
      p2 = new VBox(10);
      p2.setId("p2");
      label = new Label("Select Account Type:");
      label.setId("label");
      chbx = new ChoiceBox<>();
      chbx.setId("chbox");
      chbx.getItems().addAll("Savings", "Checking",
                             "Interest Checking");
      p2.getChildren().addAll(label, chbx);
      p1 = new HBox(10);
      p1.setId("p1");
      btn = new Button("CREATE ACCT");
      btn.setId("button");
      p1.setPadding(new Insets(10));
      p1.getChildren().addAll(p2, btn);
   }
}
Listing 9-10

The Class PrintNodeInformation

This code is a stripped-down version of AccountCreationWindow, containing only two panes and three controls. It creates the window shown in Figure 9-9.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig9_HTML.jpg
Figure 9-9

The window created by PrintNodeInformation

The start method calls the method printNodeSize for each node in the window. The printNodeSize method prints the id, height, and width of the given node, based on the value returned by getLayoutBounds. The output of the program appears in Listing 9-11.
NODE    WID HT
label   132 17
chbox   149 27
p2      149 54
button  108 27
p1      287 74
Listing 9-11

The Output of PrintNodeInformation

Let’s make sense of this output. First consider pane p2 and its children label and chbox. These controls calculate their own sizes. The program output asserts that chbox is a bit higher and wider than label, which is borne out by the screenshot. Pane p2 is a VBox, which means that its width should be the same as its widest child, which in this case is chbox. The height of p2 is the sum of the heights of its children plus 10 pixels to account for the space between them. These values are verified by the program output.

Now consider pane p1 and its children p2 and btn. Pane p1 has 10-pixel margins on all four sides. Thus its height and width will be an additional 20 pixels larger than the values calculated for its children. Pane p1 is an HBox, so its height will be the maximum height of its children (which in this case is the height of p2) plus 20 pixels for the margins. The width of p1 is the sum of the widths of its children plus 10 pixels for the space between them plus 20 pixels for the margins. These values are also verified by the program output.

The Composite Pattern

You have so far seen two examples of composite objects: Java predicates and JavaFX nodes. Although these objects come from wildly different domains, their class diagrams—as pictured in Figures 9-2 and 9-8—are remarkably similar. This similarity is known as the composite pattern.

The composite pattern expresses the preferred way to create tree-structured objects. Its class diagram appears in Figure 9-10. A tree consists of objects of type Component. These components are either of type CompositeComponent or BaseComponent. A BaseComponent object has no children and will be a leaf of its tree. A CompositeComponent object can have any number of children (thus the “*” label on its dependency arrow) and will be in the interior of the tree.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig10_HTML.jpg
Figure 9-10

The composite pattern

The Component interface specifies the methods that all components will have; these methods are not shown in the diagram. CompositeComponent is an abstract class that contains methods to modify a composite object’s list of children.

The class diagram of the composite pattern is similar to that of the decorator pattern. Their only difference is that a decorator class wraps only one object, whereas a composite class can wrap multiple objects. Consequently, decorators form a chain and composites form a tree.

This difference has a profound impact on how decorators and composites are used. A decorator chain has a single endpoint, which is treated as the primary object of the chain. The remaining objects on the chain are “decorators” that enhance the behavior of this primary object. On the other hand, a composite tree has multiple leaves, none of which is primary. Instead, its primary object is the root of the tree. The root treats its children as “assistants,” relying on them to help compute the answer to its methods. That is why composite methods are often implemented as tree traversals.

The class CompositeComponent in Figure 9-10 contains two methods for managing the children of a composite object. This design is one out of many possible designs. For example, the JavaFX Pane class has the single method getChildren to manage its children.

Moreover, the Predicate hierarchy has no child-management methods. When a composite Predicate object is created, its children are assigned by the constructor and there is no way to change those children afterwards. Such a design is called static. Composite designs that have methods to add and remove children are called dynamic.

A designer of a dynamic composite can decide to place the child-management methods either in the composite interface or in the abstract wrapper class. The choice shown in Figures 9-8 and 9-10 is to place the methods in the wrapper class. This decision causes the methods to be nontransparent. For example, consider the getChildren method in JavaFX. This method is defined in Pane, which means that it cannot be called by variables of type Node. Note that variables p1, p2, p3, and p4 in Listing 9-9 belong to classes VBox and HBox, and not Node.

The alternative design is to move the modification methods to the Component interface . This design achieves transparency, but at the cost of safety. With such a design, a client could add a child to a base object even though doing so would have no legitimate meaning.

Such a design is occasionally adopted, but usually as a last resort. One such example occurs in the Java Swing library, which is a precursor to JavaFX. In order to support legacy software, the control classes in Swing were specified to be subclasses of Container, which is the class that defines the add method. Consequently, the following code is legal Java:
   JButton b1 = new JButton("push me");
   JButton b2 = new JButton("where am I?");
   b1.add(b2);

The add method places button b2 in the list of b1’s children. But since b1 (justifiably) ignores this list, b2 will never be displayed. This sort of bug can be quite difficult to uncover.

A Cookbook Example

For a third example of the composite pattern, consider the task of writing a program to manage the recipes in a cookbook. A recipe consists of a list of ingredients and some directions. An ingredient can be a “basic food” such as carrot, apple, or milk; alternatively, it can be the result of another recipe. Figure 9-11 displays an example recipe.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig11_HTML.jpg
Figure 9-11

An example recipe

The first order of business is to design the recipe classes. Since a recipe can include other recipes as well as basic foods, the composite pattern is indicated. Figure 9-12 gives a reasonable class diagram. It contains a class for basic foods and a class for recipes. Both classes implement the FoodItem interface . The Recipe class also has a dependency on FoodItem, which denotes its list of ingredients.
../images/470600_1_En_9_Chapter/470600_1_En_9_Fig12_HTML.jpg
Figure 9-12

The Cookbook class diagram

The FoodItem interface appears in Listing 9-12. It declares the three abstract methods that BasicFood and Recipe must implement. The first two methods denote properties of the food item: the method name returns the name of the item and the method isVegan returns true if the food contains no animal products. Each basic food has an explicit flag indicating whether it is vegan; a recipe is vegan if all of its ingredients are vegan.
public interface FoodItem extends Iterable<FoodItem> {
   String name();
   boolean isVegan();
   Iterator<FoodItem> childIterator();
   default Iterator<FoodItem> iterator() {
      return new FoodIterator(this);
   }
}
Listing 9-12

The FoodItem Interface

The last two methods of FoodItem enable clients to examine the components of a food item. The childIterator method returns an iterator that contains the children of the given food item. If the item is a recipe then the iterator contains its ingredients; if the item is a basic food then the iterator will be empty. The iterator method returns an iterator that performs a complete traversal of the tree rooted at a given object. The iterator method is implemented as a default method of the interface, in terms of the class FoodIterator. That class will be examined in the next section.

Listing 9-13 gives the code for BasicFood. The name of the food and its vegan flag are passed to its constructor and the name and isVegan methods return those values. The childIterator method returns an empty iterator because basic foods do not have children.
public class BasicFood implements FoodItem {
   private String name;
   private boolean isvegan;
   public BasicFood(String name, boolean isvegan) {
      this.name = name;
      this.isvegan = isvegan;
   }
   public String name() {
      return name;
   }
   public boolean isVegan() {
      return isvegan;
   }
   public Iterator<FoodItem> childIterator() {
      return Collections.emptyIterator();
   }
   public String toString() {
      String veg = isvegan ? " (vegan)" : "";
      return name + veg;
   }
}
Listing 9-13

The BasicFood Class

Listing 9-14 gives the code for Recipe. A Recipe object is a composite whose children are the ingredients used in the recipe. The ingredients are held in a map. The key of the map is the FoodItem object and its value is the associated quantity. The method addIngredient adds the specified ingredient to the map. I chose to put this method in Recipe (and not FoodItem) because I preferred safety over transparency. The isVegan method computes its value by checking the recipe’s ingredients. If it finds an ingredient that is not vegan then it returns false; otherwise it returns true. Note how the recursion causes this method to perform a tree traversal though the recipe’s ingredient hierarchy. Finally, the childIterator method returns the iterator associated with the map’s keys.
public class Recipe implements FoodItem {
   private String name;
   private Map<FoodItem,Integer> ingredients = new HashMap<>();
   private String directions;
   public Recipe(String name, String directions) {
      this.name = name;
      this.directions = directions;
   }
   public void addIngredient(FoodItem item, int qty) {
      ingredients.put(item, qty);
   }
   public String name() {
      return name;
   }
   public boolean isVegan() {
      Iterator<FoodItem> iter = childIterator();
      while (iter.hasNext())
         if (!iter.next().isVegan())
            return false;
      return true;
   }
   public Iterator<FoodItem> childIterator() {
      return ingredients.keySet().iterator();
   }
   public int getQuantity(FoodItem item) {
      return ingredients.get(item);
   }
   public String toString() {
      String veg = isVegan() ? " (vegan)" : "";
      String result = "Recipe for " + name + veg + " ";
      result += "Ingredients:";
      for (FoodItem item : ingredients.keySet()) {
         int qty = ingredients.get(item);
         result += " " + qty + " " + item.name() + " ";
      }
      return result + "Directions: " + directions + " ";
   }
}
Listing 9-14

The Recipe Class

Listing 9-15 shows the code for a method addRecipes that illustrates recipe creation. To create a recipe you first call the Recipe constructor, passing in the recipe’s name and directions. Then you call the addIngredient method for each ingredient. Note that the ingredient can be either a BasicFood object or a Recipe object. The code assumes a global variable cbook, which maps a String object to its associated Recipe object.
private static void addRecipes() {
   Recipe dressing = new Recipe("dressing", "Mix well.");
   dressing.addIngredient(new BasicFood("oil", true), 4);
   dressing.addIngredient(new BasicFood("vinegar", true), 2);
   cbook.put("dressing", dressing);
   Recipe salad = new Recipe("salad",
          "Chop lettuce, add bacon. Pour dressing over it.");
   salad.addIngredient(new BasicFood("lettuce", true), 1);
   salad.addIngredient(new BasicFood("bacon", false), 6);
   salad.addIngredient(dressing, 1);
   cbook.put("salad", salad);
}
Listing 9-15

The addRecipes Method

Traversing a Composite Object

A composite object typically has methods that traverse the object’s components. Examples are the method test in Predicate, the method getLayoutBounds in Node, and the method isVegan in FoodItem. These methods are called internal tree traversals because the traversal occurs inside the methods without the knowledge or control of the client. The concept is analogous to the concept of internal iteration discussed in Chapter 6. These internal tree traversals, like internal iterators, are task specific.

This section is concerned with the question of whether the clients of a composite should be able to perform customized tree traversals, and if so, how. The Predicate interface , for example, was designed so that customized traversals are not possible. The designers omitted any method that would enable a client to examine the structure of a predicate, meaning that there is no way to determine the base predicates of a given Predicate object or even to tell if it is composite. The only way to traverse a Predicate object is to call its test method.

On the other hand, a JavaFX client can perform customized traversals of a Pane object by using its getchildren method. The class NodeTraversal in Listing 9-16 provides an example. The class first constructs the same JavaFX window as in Figure 9-9. It then calls two methods that traverse the window's hierarchy: printAllNodes, which prints the height and width of each node; and getWidestControl, which returns the node corresponding to the widest control.
public class NodeTraversal extends Application {
   ...
   public void start(Stage stage) {
      createNodeHierarchy(); // as in Listing 9-9 with root p1
      stage.setScene(new Scene(p1));
      stage.setTitle("Bank Account Demo");
      stage.show();
      System.out.println("NODE WID HT");
      printAllNodes(p1);
      Node n = getWidestControl(p1);
      System.out.println("The widest control is "+ n.getId());
   }
   ...
   private void printAllNodes(Node n) {
      // see listing 9-17
   }
   private Node getWidestControl(Node n) {
      // see listing 9-18
   }
}
Listing 9-16

The NodeTraversal Class

Listing 9-17 gives the code for printAllNodes . Its argument is a node n, and it prints every node in the composite hierarchy rooted at n. It does so by performing a preorder traversal of n. That is, it first prints the size of n; then, if n is a pane, it calls printAllNodes recursively on each of n’s children.
private void printAllNodes(Node n) {
   // first print the node
   printNodeSize(n);  // same as in Listing 9-10
   // then print its children, if any
   if (n instanceof Pane) {
      Pane p = (Pane) n;
      for (Node child : p.getChildren())
         printAllNodes(child);
   }
}
Listing 9-17

Printing the Components of a Node

Listing 9-18 gives the code for getWidestControl. The structure of this method is similar to printAllNodes. If the argument n is a control then it is clearly the only control in its tree and thus the widest. If n is a pane then the code calls getWidestControl recursively on its children and chooses the widest of the returned objects.
private Node getWidestControl(Node n) {
   if (n instanceof Control)
      return n;
   Node widest = null;
   double maxwidth = -1;
   Pane p = (Pane) n;
   for (Node child : p.getChildren()) {
      Node max = getWidestControl(child);
      double w = max.getLayoutBounds().getWidth();
      if (w > maxwidth) {
         widest = max;
         maxwidth = w;
      }
   }
   return widest;
}
Listing 9-18

Calculating a Node’s Widest Control

Although the getChildren method can be used in this way for customized traversals of Node objects, it is somewhat unsuited for that purpose. The method is defined in Pane, which means that it cannot be used transparently. The result is that the code in listings 9-17 and 9-18 require if-statements and awkward type casts.

The traversal methods childIterator and iterator in the cookbook example are defined in the FoodItem interface, and are thus better suited to the writing of customized tree traversals. The Cookbook code in Listing 9-19 illustrates the use of these methods. Its main method creates some Recipe objects and saves them in a map keyed by their name. It then calls methods that perform traversals of the recipes.
public class Cookbook {
   private static Map<String,Recipe> cbook = new HashMap<>();
   public static void main(String[] args) {
      addRecipes(); // from Listing 9-15
      System.out.println(" ---VEGAN RECIPES---");
      printRecipes(r->r.isVegan());
      System.out.println(" ---RECIPES USING 4+ ITEMS---");
      printRecipes(r -> foodsUsed1(r)>=4);
      printRecipes(r -> foodsUsed2(r)>=4);
      printRecipes(r -> foodsUsed3(r)>=4);
      System.out.println(" ---RECIPES COMPRISING SALAD---");
      printRecipesUsedIn1(cbook.get("salad"));
      printRecipesUsedIn2(cbook.get("salad"));
      System.out.println(" ---SHOPPING LIST FOR SALAD---");
      printShoppingList(cbook.get("salad"));
   }
   ... // the remaining methods are in listings 9-20 to 9-26
}
Listing 9-19

The Cookbook Class

Listing 9-20 shows the printRecipes method. For each recipe in the cookbook, it prints the recipe if it satisfies the given predicate. The Cookbook class calls printRecipes four times, each with a different predicate. The first predicate calls the recipe’s isVegan method, which performs an internal tree traversal. The remaining three predicates call variations of the method foodsUsed, which use an external tree traversal to count the basic foods used in a recipe. The code for those methods appears in Listings 9-21 to 9-23.
private static void printRecipes(Predicate<Recipe> pred) {
   for (Recipe r : cbook.values())
      if (pred.test(r))
         System.out.println(r);
}
Listing 9-20

The printRecipes Method

The code for method foodsUsed1 appears in Listing 9-21. It calls the childIterator method to explicitly examine the ingredients of the specified food item. If the ingredient is a basic food then it increments the count. If an ingredient is a recipe then it recursively calls foodsUsed1 on that recipe. Note how the transparency of the childIterator method simplifies the code compared with the methods in the JavaFX example.
private static int foodsUsed1(FoodItem r) {
   int count = 0;
   if (r instanceof BasicFood)
      count = 1;
   else {
      Iterator<FoodItem> iter = r.childIterator();
      while (iter.hasNext())
         count += foodsUsed1(iter.next());
   }
   return count;
}
Listing 9-21

The foodsUsed1 Method

The method foodsUsed2 uses the iterator method to examine the entire composite tree rooted at the specified recipe. This code is simpler than foodsUsed1 because the code can perform a single loop through the iterator with no need for recursion. Its code appears in Listing 9-22.
private static int foodsUsed2(FoodItem r) {
   int count = 0;
   Iterator<FoodItem> iter = r.iterator();
   while (iter.hasNext())
      if (iter.next() instanceof BasicFood)
         count++;
   return count;
}
Listing 9-22

The foodsUsed2 Method

The method foodsUsed3 is essentially the same as foodsUsed2. The difference is that the iterator method is called implicitly, via the for-each loop.
private static int foodsUsed3(FoodItem r) {
   int count = 0;
   for (FoodItem item : r)
      if (item instanceof BasicFood)
         count++;
   return count;
}
Listing 9-23

The foodsUsed3 Method

The two printRecipesUsedIn methods in the Cookbook class print the name of all the recipes needed to make a given recipe. For example, the recipes required to make a salad are “salad” and “dressing.” The code for both methods takes advantage of the fact that FoodItem is an iterable. The method printRecipesUsedIn1 uses the iterator method to loop through the recipe’s composite tree, printing the name of any food item that is a recipe. Its code appears in Listing 9-24.
private static void printRecipesUsedIn1(Recipe r) {
   for (FoodItem item : r) {
      if (item instanceof Recipe)
         System.out.println(item.name());
   }
}
Listing 9-24

The printRecipesUsedIn1 Method

The code for printRecipesUsedIn2 appears in Listing 9-25. It uses the method forEach and the visitor pattern.
private static void printRecipesUsedIn2(Recipe r) {
   r.forEach(item -> {
      if (item instanceof Recipe) {
         System.out.println(item.name());
   }});
}
Listing 9-25

The printRecipesUsedIn2 Method

Listing 9-26 gives the code for printShoppingList. This method prints the name and quantity needed for each basic item used in a recipe. The second argument to the method is the number of portions of the recipe that will be made. One complexity to the code is that the quantity of each item in the recipe must be multiplied by the number of portions of its recipe that are being made.

This method is unlike the others because its code needs to know the structure of the composite tree. In particular, the code needs to know which recipe an ingredient belongs to and how many portions of that recipe will be made. The code therefore must use the childIterator to manually traverse the ingredients of a recipe and perform the recursion for subrecipes. The iterator method is not useful here.
private static void printShoppingList(Recipe r, int howmany) {
   Iterator<FoodItem> iter = r.childIterator();
   while (iter.hasNext()) {
      FoodItem item = iter.next();
      int amt = r.getQuantity(item) * howmany;
      if (item instanceof BasicFood)
         System.out.println(item.name() + " " + amt);
      else
         printShoppingList((Recipe) item, amt);
   }
}
Listing 9-26

The printShoppingList Method

The final topic of this section concerns how to implement the iterator method. Recall from Listing 9-12 that the code for this method was declared in the FoodItem interface as follows:
   default Iterator<FoodItem> iterator() {
      return new FoodIterator(this);
   }
Listing 9-27 gives the code for the FoodIterator class. It implements Iterator<FoodItem>. The argument to its constructor is a food item f. Successive calls to next will return every item in the composite hierarchy rooted at f, beginning with f itself.
public class FoodIterator implements Iterator<FoodItem> {
   private Stack<Iterator<FoodItem>> s = new Stack<>();
   public FoodIterator(FoodItem f) {
      Collection<FoodItem> c = Collections.singleton(f);
      s.push(c.iterator());
   }
   public boolean hasNext() {
      return !s.isEmpty();
   }
   public FoodItem next() {
      FoodItem food = s.peek().next(); // return this value
      if (!s.peek().hasNext())
         s.pop();      // pop the iterator when it is empty
      Iterator<FoodItem> iter = food.childIterator();
      if (iter.hasNext())
         s.push(iter); // push the child iterator if non-empty
      return food;
   }
}
Listing 9-27

The FoodIterator Class

The next method essentially performs a nonrecursive tree traversal using a stack of iterators. Each call to next removes an item from the topmost iterator. If that iterator has no more elements then it is popped from the stack. If the retrieved item has children then its child iterator gets pushed onto the stack. The hasNext method returns true if the stack is not empty. The constructor primes the stack by adding an iterator containing the root of the composite hierarchy.

Summary

A composite object has a hierarchical structure. Each object in the hierarchy implements the same interface, called the composite interface . The composite pattern describes the preferred way to organize the classes of a composite object. These classes form two categories: base classes, whose objects are the leaves of the composite hierarchy, and recursive classes, whose objects form the interior of the hierarchy. Each recursive object wraps one or more objects that implement the composite interface. These wrapped objects are called its children.

Syntactically, a composite object is very similar to a decorator object; the only difference is that a composite can have multiple children whereas a decorator can have just one. This difference, however, completely changes their purpose. A decorator is a chain, where the recursive objects serve to enhance the methods of the base object at the end of the chain. A composite is a tree, whose nonroot objects combine to execute the methods of the root. A composite method is often implemented as a tree traversal.

When designing classes that conform to the composite pattern, you need to consider the transparency of their methods. A method that modifies a composite’s list of children should not be defined in the composite interface, as it will allow clients to perform meaningless (and potentially dangerous) operations on base objects. It is better to define such methods in the wrapper class and use them nontransparently. On the other hand, it is possible to design methods for the composite interface that enable clients to transparently traverse the composite hierarchy. This chapter presented two such methods: childIterator, which returns an iterator containing the object’s children, and iterator, which returns an iterator containing the entire composite hierarchy. Implementing the iterator method also lets the composite interface extend Iterable, which means that clients can use the forEach method and the for-each loop to examine composite objects.

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

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